initial code (#6)

* initial code commit
This commit is contained in:
l3uddz
2021-02-14 16:18:26 +00:00
committed by GitHub
parent 3f55336fbd
commit ce3807b819
53 changed files with 3694 additions and 0 deletions

34
media/client.go Normal file
View File

@@ -0,0 +1,34 @@
package media
import (
"fmt"
"github.com/l3uddz/nabarr/logger"
"github.com/l3uddz/nabarr/media/omdb"
"github.com/l3uddz/nabarr/media/trakt"
"github.com/rs/zerolog"
)
type Client struct {
trakt *trakt.Client
omdb *omdb.Client
log zerolog.Logger
}
func New(cfg *Config) (*Client, error) {
// trakt
if cfg.Trakt.ClientId == "" {
return nil, fmt.Errorf("trakt: no client_id specified")
}
t := trakt.New(&cfg.Trakt)
// omdb
o := omdb.New(&cfg.Omdb)
return &Client{
trakt: t,
omdb: o,
log: logger.New(cfg.Verbosity).With().Logger(),
}, nil
}

13
media/config.go Normal file
View File

@@ -0,0 +1,13 @@
package media
import (
"github.com/l3uddz/nabarr/media/omdb"
"github.com/l3uddz/nabarr/media/trakt"
)
type Config struct {
Trakt trakt.Config `yaml:"trakt"`
Omdb omdb.Config `yaml:"omdb"`
Verbosity string `yaml:"verbosity,omitempty"`
}

7
media/error.go Normal file
View File

@@ -0,0 +1,7 @@
package media
import "errors"
var (
ErrItemNotFound = errors.New("not found")
)

58
media/movie.go Normal file
View File

@@ -0,0 +1,58 @@
package media
import (
"errors"
"fmt"
"github.com/l3uddz/nabarr/media/trakt"
"strconv"
"time"
)
func (c *Client) GetMovieInfo(item *FeedItem) (*Item, error) {
// lookup on trakt
t, err := c.trakt.GetMovie(item.ImdbId)
if err != nil {
if errors.Is(err, trakt.ErrItemNotFound) {
return nil, fmt.Errorf("trakt: get movie: movie with imdbId %q: %w", item.ImdbId, ErrItemNotFound)
}
return nil, fmt.Errorf("trakt: get movie: movie with imdbId %q: %w", item.ImdbId, err)
}
// transform trakt info
date, err := time.Parse("2006-01-02", t.Released)
if err != nil {
date = time.Time{}
}
mi := &Item{
TvdbId: "",
TmdbId: strconv.Itoa(t.Ids.Tmdb),
ImdbId: t.Ids.Imdb,
Slug: t.Ids.Slug,
Title: t.Title,
FeedTitle: item.Title,
Summary: t.Overview,
Country: []string{t.Country},
Network: "",
Date: date,
Year: date.Year(),
Runtime: t.Runtime,
Rating: t.Rating,
Votes: t.Votes,
Status: t.Status,
Genres: t.Genres,
Languages: []string{t.Language},
}
// omdb
if oi, err := c.omdb.GetItem(t.Ids.Imdb); err != nil {
c.log.Debug().
Err(err).
Str("imdb_id", t.Ids.Imdb).
Msg("Item was not found on omdb")
} else if oi != nil {
mi.Omdb = *oi
}
return mi, nil
}

7
media/omdb/config.go Normal file
View File

@@ -0,0 +1,7 @@
package omdb
type Config struct {
ApiKey string `yaml:"api_key"`
Verbosity string `yaml:"verbosity,omitempty"`
}

72
media/omdb/media.go Normal file
View File

@@ -0,0 +1,72 @@
package omdb
import (
"encoding/json"
"errors"
"fmt"
"github.com/l3uddz/nabarr/util"
"github.com/lucperkins/rek"
"net/url"
"strings"
)
var (
ErrItemNotFound = errors.New("not found")
)
func (c *Client) GetItem(imdbId string) (*Item, error) {
// empty item when appropriate
if c.apiKey == "" || imdbId == "" {
return nil, nil
}
// prepare request
reqUrl, err := util.URLWithQuery(c.apiURL, url.Values{
"apikey": []string{c.apiKey},
"i": []string{imdbId}})
if err != nil {
return nil, fmt.Errorf("generate lookup request url: %w", err)
}
c.log.Trace().
Str("url", reqUrl).
Msg("Searching omdb")
// send request
c.rl.Take()
resp, err := rek.Get(reqUrl, rek.Timeout(c.apiTimeout))
if err != nil {
return nil, fmt.Errorf("request lookup: %w", err)
}
defer resp.Body().Close()
// validate response
if resp.StatusCode() != 200 {
return nil, fmt.Errorf("validate lookup response: %s", resp.Status())
}
// decode response
b := new(lookupResponse)
if err := json.NewDecoder(resp.Body()).Decode(b); err != nil {
return nil, fmt.Errorf("decode lookup response: %w", err)
}
if b.Title == "" {
return nil, fmt.Errorf("item with imdbId: %v: %w", imdbId, ErrItemNotFound)
}
// transform response
rt := 0
for _, rating := range b.Ratings {
if strings.EqualFold(rating.Source, "Rotten Tomatoes") {
rt = util.Atoi(strings.TrimSuffix(rating.Value, "%"), 0)
break
}
}
return &Item{
Metascore: util.Atoi(b.Metascore, 0),
RottenTomatoes: rt,
ImdbRating: util.Atof64(b.ImdbRating, 0.0),
}, nil
}

28
media/omdb/omdb.go Normal file
View File

@@ -0,0 +1,28 @@
package omdb
import (
"github.com/l3uddz/nabarr/logger"
"github.com/rs/zerolog"
"go.uber.org/ratelimit"
"time"
)
type Client struct {
apiKey string
log zerolog.Logger
rl ratelimit.Limiter
apiURL string
apiTimeout time.Duration
}
func New(cfg *Config) *Client {
return &Client{
apiKey: cfg.ApiKey,
log: logger.New(cfg.Verbosity).With().Logger(),
rl: ratelimit.New(1, ratelimit.WithoutSlack),
apiURL: "https://www.omdbapi.com",
apiTimeout: 30 * time.Second,
}
}

40
media/omdb/struct.go Normal file
View File

@@ -0,0 +1,40 @@
package omdb
type rating struct {
Source string `json:"Source,omitempty"`
Value string `json:"Value,omitempty"`
}
type lookupResponse struct {
Title string `json:"Title,omitempty"`
Year string `json:"Year,omitempty"`
Rated string `json:"Rated,omitempty"`
Released string `json:"Released,omitempty"`
Runtime string `json:"Runtime,omitempty"`
Genre string `json:"Genre,omitempty"`
Director string `json:"Director,omitempty"`
Writer string `json:"Writer,omitempty"`
Actors string `json:"Actors,omitempty"`
Plot string `json:"Plot,omitempty"`
Language string `json:"Language,omitempty"`
Country string `json:"Country,omitempty"`
Awards string `json:"Awards,omitempty"`
Poster string `json:"Poster,omitempty"`
Ratings []rating `json:"Ratings,omitempty"`
Metascore string `json:"Metascore,omitempty"`
ImdbRating string `json:"imdbRating,omitempty"`
ImdbVotes string `json:"imdbVotes,omitempty"`
ImdbID string `json:"imdbID,omitempty"`
Type string `json:"Type,omitempty"`
DVD string `json:"DVD,omitempty"`
BoxOffice string `json:"BoxOffice,omitempty"`
Production string `json:"Production,omitempty"`
Website string `json:"Website,omitempty"`
Response string `json:"Response,omitempty"`
}
type Item struct {
Metascore int `json:"Metascore,omitempty"`
RottenTomatoes int `json:"RottenTomatoes,omitempty"`
ImdbRating float64 `json:"ImdbRating,omitempty"`
}

53
media/show.go Normal file
View File

@@ -0,0 +1,53 @@
package media
import (
"errors"
"fmt"
"github.com/l3uddz/nabarr/media/trakt"
"strconv"
)
func (c *Client) GetShowInfo(item *FeedItem) (*Item, error) {
// lookup on trakt
t, err := c.trakt.GetShow(item.TvdbId)
if err != nil {
if errors.Is(err, trakt.ErrItemNotFound) {
return nil, fmt.Errorf("trakt: get show: show with tvdbId %q: %w", item.TvdbId, ErrItemNotFound)
}
return nil, fmt.Errorf("trakt: get show: show with tvdbId %q: %w", item.TvdbId, err)
}
// transform trakt info to MediaItem
mi := &Item{
TvdbId: strconv.Itoa(t.Ids.Tvdb),
TmdbId: strconv.Itoa(t.Ids.Tmdb),
ImdbId: t.Ids.Imdb,
Slug: t.Ids.Slug,
Title: t.Title,
FeedTitle: item.Title,
Summary: t.Overview,
Country: []string{t.Country},
Network: t.Network,
Date: t.FirstAired,
Year: t.FirstAired.Year(),
Runtime: t.Runtime,
Rating: t.Rating,
Votes: t.Votes,
Status: t.Status,
Genres: t.Genres,
Languages: []string{t.Language},
AiredEpisodes: t.AiredEpisodes,
}
// omdb
if oi, err := c.omdb.GetItem(t.Ids.Imdb); err != nil {
c.log.Debug().
Err(err).
Str("imdb_id", t.Ids.Imdb).
Msg("Item was not found on omdb")
} else if oi != nil {
mi.Omdb = *oi
}
return mi, nil
}

95
media/struct.go Normal file
View File

@@ -0,0 +1,95 @@
package media
import (
"encoding/xml"
"github.com/l3uddz/nabarr/media/omdb"
"github.com/pkg/errors"
"time"
)
type Item struct {
TvdbId string `json:"TvdbId,omitempty"`
TmdbId string `json:"TmdbId,omitempty"`
ImdbId string `json:"ImdbId,omitempty"`
Slug string `json:"Slug,omitempty"`
FeedTitle string `json:"FeedTitle,omitempty"`
Title string `json:"Title,omitempty"`
Summary string `json:"Summary,omitempty"`
Country []string `json:"Country,omitempty"`
Network string `json:"Network,omitempty"`
Date time.Time `json:"Date"`
Year int `json:"Year,omitempty"`
Runtime int `json:"Runtime,omitempty"`
Rating float64 `json:"Rating,omitempty"`
Votes int `json:"Votes,omitempty"`
Status string `json:"Status,omitempty"`
Genres []string `json:"Genres,omitempty"`
Languages []string `json:"Languages,omitempty"`
AiredEpisodes int `json:"AiredEpisodes,omitempty"`
// additional media provider data
Omdb omdb.Item `json:"Omdb,omitempty"`
}
type Rss struct {
Channel struct {
Items []FeedItem `xml:"item"`
} `xml:"channel"`
}
type FeedItem struct {
Title string `xml:"title,omitempty"`
Category string `xml:"category,omitempty"`
GUID string `xml:"guid,omitempty"`
PubDate Time `xml:"pubDate,omitempty"`
// set by processor
Feed string
// attributes
Language string
TvdbId string `xml:"tvdb,omitempty"`
TvMazeId string
ImdbId string `xml:"imdb,omitempty"`
Attributes []struct {
XMLName xml.Name
Name string `xml:"name,attr"`
Value string `xml:"value,attr"`
} `xml:"attr"`
}
// Time credits: https://github.com/mrobinsn/go-newznab/blob/cd89d9c56447859fa1298dc9a0053c92c45ac7ef/newznab/structs.go#L150
type Time struct {
time.Time
}
func (t *Time) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if err := e.EncodeToken(start); err != nil {
return errors.Wrap(err, "failed to encode xml token")
}
if err := e.EncodeToken(xml.CharData([]byte(t.UTC().Format(time.RFC1123Z)))); err != nil {
return errors.Wrap(err, "failed to encode xml token")
}
if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil {
return errors.Wrap(err, "failed to encode xml token")
}
return nil
}
func (t *Time) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var raw string
err := d.DecodeElement(&raw, &start)
if err != nil {
return err
}
date, err := time.Parse(time.RFC1123Z, raw)
if err != nil {
return err
}
*t = Time{date}
return nil
}

7
media/trakt/config.go Normal file
View File

@@ -0,0 +1,7 @@
package trakt
type Config struct {
ClientId string `yaml:"client_id"`
Verbosity string `yaml:"verbosity,omitempty"`
}

96
media/trakt/media.go Normal file
View File

@@ -0,0 +1,96 @@
package trakt
import (
"encoding/json"
"errors"
"fmt"
"github.com/l3uddz/nabarr/util"
"github.com/lucperkins/rek"
"net/url"
)
var (
ErrItemNotFound = errors.New("not found")
)
func (c *Client) GetShow(tvdbId string) (*Show, error) {
// prepare request
reqUrl, err := util.URLWithQuery(util.JoinURL(c.apiURL, fmt.Sprintf("/search/tvdb/%s", tvdbId)),
url.Values{
"type": []string{"show"},
"extended": []string{"full"}})
if err != nil {
return nil, fmt.Errorf("generate lookup show request url: %w", err)
}
c.log.Trace().
Str("url", reqUrl).
Msg("Searching trakt")
// send request
c.rl.Take()
resp, err := rek.Get(reqUrl, rek.Headers(c.getAuthHeaders()), rek.Timeout(c.apiTimeout))
if err != nil {
return nil, fmt.Errorf("request show: %w", err)
}
defer resp.Body().Close()
// validate response
if resp.StatusCode() != 200 {
return nil, fmt.Errorf("validate show response: %s", resp.Status())
}
// decode response
b := new([]struct{ Show Show })
if err := json.NewDecoder(resp.Body()).Decode(b); err != nil {
return nil, fmt.Errorf("decode show response: %w", err)
}
if len(*b) < 1 {
return nil, ErrItemNotFound
}
// translate response
return &(*b)[0].Show, nil
}
func (c *Client) GetMovie(imdbId string) (*Movie, error) {
// prepare request
reqUrl, err := util.URLWithQuery(util.JoinURL(c.apiURL, fmt.Sprintf("/search/imdb/%s", imdbId)),
url.Values{
"type": []string{"movie"},
"extended": []string{"full"}})
if err != nil {
return nil, fmt.Errorf("generate lookup movie request url: %w", err)
}
c.log.Trace().
Str("url", reqUrl).
Msg("Searching trakt")
// send request
c.rl.Take()
resp, err := rek.Get(reqUrl, rek.Headers(c.getAuthHeaders()), rek.Timeout(c.apiTimeout))
if err != nil {
return nil, fmt.Errorf("request movie: %w", err)
}
defer resp.Body().Close()
// validate response
if resp.StatusCode() != 200 {
return nil, fmt.Errorf("validate movie response: %s", resp.Status())
}
// decode response
b := new([]struct{ Movie Movie })
if err := json.NewDecoder(resp.Body()).Decode(b); err != nil {
return nil, fmt.Errorf("decode movie response: %w", err)
}
if len(*b) < 1 {
return nil, ErrItemNotFound
}
// translate response
return &(*b)[0].Movie, nil
}

67
media/trakt/struct.go Normal file
View File

@@ -0,0 +1,67 @@
package trakt
import (
"time"
)
type ShowIds struct {
Trakt int `json:"trakt"`
Slug string `json:"slug"`
Tvdb int `json:"tvdb"`
Imdb string `json:"imdb"`
Tmdb int `json:"tmdb"`
}
type MovieIds struct {
Trakt int `json:"trakt"`
Slug string `json:"slug"`
Imdb string `json:"imdb"`
Tmdb int `json:"tmdb"`
}
type Show struct {
Type string `json:"type"`
Title string `json:"title"`
Year int `json:"year"`
Ids ShowIds `json:"ids"`
Overview string `json:"overview"`
FirstAired time.Time `json:"first_aired"`
Runtime int `json:"runtime"`
Certification string `json:"certification"`
Network string `json:"network"`
Country string `json:"country"`
Trailer string `json:"trailer"`
Homepage string `json:"homepage"`
Status string `json:"status"`
Rating float64 `json:"rating"`
Votes int `json:"votes"`
CommentCount int `json:"comment_count"`
Language string `json:"language"`
AvailableTranslations []string `json:"available_translations"`
Genres []string `json:"genres"`
AiredEpisodes int `json:"aired_episodes"`
Character string `json:"character"`
}
type Movie struct {
Type string `json:"type"`
Title string `json:"title"`
Year int `json:"year"`
Ids MovieIds `json:"ids"`
Tagline string `json:"tagline"`
Overview string `json:"overview"`
Released string `json:"released"`
Runtime int `json:"runtime"`
Country string `json:"country"`
Trailer string `json:"trailer"`
Homepage string `json:"homepage"`
Status string `json:"status"`
Rating float64 `json:"rating"`
Votes int `json:"votes"`
CommentCount int `json:"comment_count"`
Language string `json:"language"`
AvailableTranslations []string `json:"available_translations"`
Genres []string `json:"genres"`
Certification string `json:"certification"`
Character string `json:"character"`
}

35
media/trakt/trakt.go Normal file
View File

@@ -0,0 +1,35 @@
package trakt
import (
"github.com/l3uddz/nabarr/logger"
"github.com/rs/zerolog"
"go.uber.org/ratelimit"
"time"
)
type Client struct {
clientId string
log zerolog.Logger
rl ratelimit.Limiter
apiURL string
apiTimeout time.Duration
}
func New(cfg *Config) *Client {
return &Client{
clientId: cfg.ClientId,
log: logger.New(cfg.Verbosity).With().Logger(),
rl: ratelimit.New(1, ratelimit.WithoutSlack),
apiURL: "https://api.trakt.tv",
apiTimeout: 30 * time.Second,
}
}
func (c *Client) getAuthHeaders() map[string]string {
return map[string]string{
"trakt-api-key": c.clientId,
"trakt-api-version": "2",
}
}