mirror of
https://github.com/sbondCo/Watcharr.git
synced 2026-08-07 07:14:44 +00:00
organize tmdb api request logic into tmdb package AND support inline search filters
This commit is contained in:
Vendored
+6
@@ -53,6 +53,12 @@ func CreateCacheKey(name string, u ...any) string {
|
||||
}
|
||||
case int:
|
||||
appnd(strconv.Itoa(vv))
|
||||
case bool:
|
||||
if vv {
|
||||
appnd("1")
|
||||
} else {
|
||||
appnd("0")
|
||||
}
|
||||
default:
|
||||
// This should never happen, but incase of unknown
|
||||
// value passed, hopefully this should make it easier
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"time"
|
||||
|
||||
gocache "github.com/robfig/go-cache"
|
||||
"github.com/sbondCo/Watcharr/cache"
|
||||
"github.com/sbondCo/Watcharr/config"
|
||||
"github.com/sbondCo/Watcharr/database/entity"
|
||||
"github.com/sbondCo/Watcharr/media/tmdb"
|
||||
@@ -160,7 +159,7 @@ func (s *Service) saveContent(c *entity.Content, onlyUpdate bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) cacheContentTv(content tmdb.TMDBShowDetails, onlyUpdate bool) (entity.Content, error) {
|
||||
func (s *Service) CacheContentTv(content tmdb.TMDBShowDetails, onlyUpdate bool) (entity.Content, error) {
|
||||
slog.Debug("cacheContentTv", "content", content)
|
||||
var (
|
||||
releaseDate time.Time
|
||||
@@ -200,7 +199,7 @@ func (s *Service) cacheContentTv(content tmdb.TMDBShowDetails, onlyUpdate bool)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (s *Service) cacheContentMovie(content tmdb.TMDBMovieDetails, onlyUpdate bool) (entity.Content, error) {
|
||||
func (s *Service) CacheContentMovie(content tmdb.TMDBMovieDetails, onlyUpdate bool) (entity.Content, error) {
|
||||
var (
|
||||
releaseDate time.Time
|
||||
)
|
||||
@@ -259,7 +258,7 @@ func (s *Service) GetOrCacheContent(contentType entity.ContentType, tmdbId int)
|
||||
slog.Error("Failed to unmarshal movie details", "error", err)
|
||||
return entity.Content{}, errors.New("failed to process movie details response")
|
||||
}
|
||||
content, err = s.cacheContentMovie(*c, false)
|
||||
content, err = s.CacheContentMovie(*c, false)
|
||||
if err != nil {
|
||||
slog.Error("GetOrCacheContent: failed to cache movie content", "type", contentType, "content_id", tmdbId, "err", err)
|
||||
return entity.Content{}, errors.New("failed to cache content")
|
||||
@@ -271,7 +270,7 @@ func (s *Service) GetOrCacheContent(contentType entity.ContentType, tmdbId int)
|
||||
slog.Error("Failed to unmarshal tv details", "error", err)
|
||||
return entity.Content{}, errors.New("failed to process tv details response")
|
||||
}
|
||||
content, err = s.cacheContentTv(*c, false)
|
||||
content, err = s.CacheContentTv(*c, false)
|
||||
if err != nil {
|
||||
slog.Error("GetOrCacheContent: failed to cache tv content", "type", contentType, "content_id", tmdbId, "err", err)
|
||||
return entity.Content{}, errors.New("failed to cache content")
|
||||
@@ -280,425 +279,3 @@ func (s *Service) GetOrCacheContent(contentType entity.ContentType, tmdbId int)
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// TMDB Multi Search.
|
||||
func (s *Service) SearchContent(
|
||||
query string,
|
||||
pageNum int,
|
||||
) (tmdb.TMDBSearchMultiResponse, error) {
|
||||
resp := new(tmdb.TMDBSearchMultiResponse)
|
||||
if pageNum == 0 {
|
||||
pageNum = 1
|
||||
}
|
||||
cacheKey := cache.CreateCacheKey("SearchContent", query, pageNum)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("SearchContent: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := s.tmdb.Request(
|
||||
"/search/multi",
|
||||
map[string]string{
|
||||
"query": query,
|
||||
"page": strconv.Itoa(pageNum),
|
||||
},
|
||||
&resp)
|
||||
if err != nil {
|
||||
slog.Error("Failed to complete multi search request!",
|
||||
"error", err.Error())
|
||||
return tmdb.TMDBSearchMultiResponse{},
|
||||
errors.New("failed to complete multi search request")
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) SearchMovies(
|
||||
query string,
|
||||
pageNum int,
|
||||
) (tmdb.TMDBSearchMoviesResponse, error) {
|
||||
resp := new(tmdb.TMDBSearchMoviesResponse)
|
||||
if pageNum == 0 {
|
||||
pageNum = 1
|
||||
}
|
||||
cacheKey := cache.CreateCacheKey("SearchMovies", query, pageNum)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("SearchMovies: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := s.tmdb.Request(
|
||||
"/search/movie",
|
||||
map[string]string{
|
||||
"query": query,
|
||||
"page": strconv.Itoa(pageNum),
|
||||
},
|
||||
&resp)
|
||||
if err != nil {
|
||||
slog.Error("Failed to complete movie search request!",
|
||||
"error", err.Error())
|
||||
return tmdb.TMDBSearchMoviesResponse{},
|
||||
errors.New("failed to complete movie search request")
|
||||
}
|
||||
for i := range resp.Results {
|
||||
resp.Results[i].MediaType = "movie"
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) SearchTv(
|
||||
query string,
|
||||
pageNum int,
|
||||
) (tmdb.TMDBSearchShowsResponse, error) {
|
||||
resp := new(tmdb.TMDBSearchShowsResponse)
|
||||
if pageNum == 0 {
|
||||
pageNum = 1
|
||||
}
|
||||
cacheKey := cache.CreateCacheKey("SearchTv", query, pageNum)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("SearchTv: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := s.tmdb.Request(
|
||||
"/search/tv",
|
||||
map[string]string{
|
||||
"query": query,
|
||||
"page": strconv.Itoa(pageNum),
|
||||
},
|
||||
&resp)
|
||||
if err != nil {
|
||||
slog.Error("Failed to complete tv search request!",
|
||||
"error", err.Error())
|
||||
return tmdb.TMDBSearchShowsResponse{},
|
||||
errors.New("failed to complete tv search request")
|
||||
}
|
||||
for i := range resp.Results {
|
||||
resp.Results[i].MediaType = "tv"
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) SearchPeople(
|
||||
query string,
|
||||
pageNum int,
|
||||
) (tmdb.TMDBSearchPeopleResponse, error) {
|
||||
resp := new(tmdb.TMDBSearchPeopleResponse)
|
||||
if pageNum == 0 {
|
||||
pageNum = 1
|
||||
}
|
||||
cacheKey := cache.CreateCacheKey("SearchPeople", query, pageNum)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("SearchPeople: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := s.tmdb.Request(
|
||||
"/search/person",
|
||||
map[string]string{
|
||||
"query": query,
|
||||
"page": strconv.Itoa(pageNum),
|
||||
},
|
||||
&resp)
|
||||
if err != nil {
|
||||
slog.Error("Failed to complete people search request!",
|
||||
"error", err.Error())
|
||||
return tmdb.TMDBSearchPeopleResponse{},
|
||||
errors.New("failed to complete people search request")
|
||||
}
|
||||
for i := range resp.Results {
|
||||
resp.Results[i].MediaType = "person"
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
// Search for content by an external id (imdb, etc).
|
||||
// Defaults to imdb if no source if provided (probably most common).
|
||||
func (s *Service) SearchByExternalId(
|
||||
id string,
|
||||
source string,
|
||||
) (tmdb.TMDBSearchMultiResponse, error) {
|
||||
resp := new(tmdb.TMDBFindByExternalIdResponse)
|
||||
if source == "" {
|
||||
source = "imdb"
|
||||
}
|
||||
cacheKey := cache.CreateCacheKey("SearchByExternalId", id, source)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("SearchByExternalId: Got cache.")
|
||||
} else {
|
||||
// If not found in cache, request data from tmdb.
|
||||
err := s.tmdb.Request(
|
||||
"/find/"+id,
|
||||
map[string]string{"external_source": source + "_id"},
|
||||
&resp)
|
||||
if err != nil {
|
||||
slog.Error("Failed to complete find/external_id request!",
|
||||
"error", err.Error())
|
||||
return tmdb.TMDBSearchMultiResponse{},
|
||||
errors.New("failed to complete find/external_id request")
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
}
|
||||
comb := []tmdb.TMDBSearchMultiResult{}
|
||||
comb = append(comb, resp.MovieResults...)
|
||||
comb = append(comb, resp.TvResults...)
|
||||
comb = append(comb, resp.PersonResults...)
|
||||
comb = append(comb, resp.TvSeasonResults...)
|
||||
comb = append(comb, resp.TvEpisodeResults...)
|
||||
return tmdb.TMDBSearchMultiResponse{
|
||||
TMDBSearchResponse: tmdb.TMDBSearchResponse[tmdb.TMDBSearchMultiResult]{
|
||||
Results: comb,
|
||||
TMDBPageFields: tmdb.TMDBPageFields{
|
||||
TotalResults: len(comb),
|
||||
// Just providing these so we don't break frontend pagination logic.
|
||||
TotalPages: 1,
|
||||
Page: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
nil
|
||||
}
|
||||
|
||||
func (s *Service) MovieDetails(
|
||||
id string,
|
||||
country string,
|
||||
rParams map[string]string,
|
||||
) (tmdb.TMDBMovieDetails, error) {
|
||||
resp := new(tmdb.TMDBMovieDetails)
|
||||
cacheKey := cache.CreateCacheKey("MovieDetails", id, country, rParams)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("MovieDetails: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := s.tmdb.Request("/movie/"+id, rParams, &resp)
|
||||
if err != nil {
|
||||
slog.Error("Failed to complete movie details request!",
|
||||
"error", err.Error())
|
||||
return tmdb.TMDBMovieDetails{},
|
||||
errors.New("failed to complete movie details request")
|
||||
}
|
||||
resp.WatchProvidersTransformed = transformProviders(&resp.WatchProviders, country)
|
||||
resp.WatchProviders = nil // We don't want this to linger around (in cache) since we have the transformed version now..
|
||||
go s.cacheContentMovie(*resp, true)
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) MovieCredits(id string) (tmdb.TMDBContentCredits, error) {
|
||||
resp := new(tmdb.TMDBContentCredits)
|
||||
err := s.tmdb.Request("/movie/"+id+"/credits", map[string]string{}, &resp)
|
||||
if err != nil {
|
||||
slog.Error("Failed to complete movie cast request!", "error", err.Error())
|
||||
return tmdb.TMDBContentCredits{}, errors.New("failed to complete movie cast request")
|
||||
}
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) TvDetails(
|
||||
id string,
|
||||
country string,
|
||||
rParams map[string]string,
|
||||
) (tmdb.TMDBShowDetails, error) {
|
||||
cacheKey := cache.CreateCacheKey("TvDetails", id, country, rParams)
|
||||
resp := new(tmdb.TMDBShowDetails)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("TvDetails: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := s.tmdb.Request("/tv/"+id, rParams, &resp)
|
||||
if err != nil {
|
||||
slog.Error("Failed to complete tv details request!", "error", err.Error())
|
||||
return tmdb.TMDBShowDetails{}, errors.New("failed to complete tv details request")
|
||||
}
|
||||
resp.WatchProvidersTransformed = transformProviders(&resp.WatchProviders, country)
|
||||
resp.WatchProviders = nil // We don't want this to linger around (in cache) since we have the transformed version now..
|
||||
go s.cacheContentTv(*resp, true)
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) TvCredits(id string) (tmdb.TMDBContentCredits, error) {
|
||||
resp := new(tmdb.TMDBContentCredits)
|
||||
err := s.tmdb.Request("/tv/"+id+"/credits", map[string]string{}, &resp)
|
||||
if err != nil {
|
||||
slog.Error("Failed to complete tv cast request!", "error", err.Error())
|
||||
return tmdb.TMDBContentCredits{}, errors.New("failed to complete tv cast request")
|
||||
}
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
// This method is manually cached, so it can be easily used in other places (on the server) with cache benefits
|
||||
func (s *Service) SeasonDetails(tvId string, seasonNumber string) (tmdb.TMDBSeasonDetails, error) {
|
||||
cacheKey := cache.CreateCacheKey("SeasonDetails", tvId, seasonNumber)
|
||||
resp := new(tmdb.TMDBSeasonDetails)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("SeasonDetails: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := s.tmdb.Request("/tv/"+tvId+"/season/"+seasonNumber, map[string]string{}, &resp)
|
||||
if err != nil {
|
||||
slog.Error("SeasonDetails: Failed to complete season details request!", "error", err.Error())
|
||||
return tmdb.TMDBSeasonDetails{}, errors.New("failed to complete season details request")
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) PersonDetails(id string) (tmdb.TMDBPersonDetails, error) {
|
||||
resp := new(tmdb.TMDBPersonDetails)
|
||||
err := s.tmdb.Request("/person/"+id, map[string]string{}, &resp)
|
||||
if err != nil {
|
||||
slog.Error("Failed to complete person details request!", "error", err.Error())
|
||||
return tmdb.TMDBPersonDetails{}, errors.New("failed to complete person details request")
|
||||
}
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) PersonCredits(id string) (tmdb.TMDBPersonCombinedCredits, error) {
|
||||
cacheKey := cache.CreateCacheKey("PersonCredits", id)
|
||||
resp := new(tmdb.TMDBPersonCombinedCredits)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("PersonCredits: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := s.tmdb.Request("/person/"+id+"/combined_credits", map[string]string{}, &resp)
|
||||
if err != nil {
|
||||
slog.Error("Failed to complete person details request!", "error", err.Error())
|
||||
return tmdb.TMDBPersonCombinedCredits{}, errors.New("failed to complete person details request")
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) Trending(t tmdb.TrendingType, pageNum int, region string) (tmdb.TMDBTrendingCombined, error) {
|
||||
resp := new(tmdb.TMDBTrendingCombined)
|
||||
if t != tmdb.TrendingTypeAll &&
|
||||
t != tmdb.TrendingTypeMovie &&
|
||||
t != tmdb.TrendingTypeShow &&
|
||||
t != tmdb.TrendingTypePerson {
|
||||
slog.Error("Trending: Invalid type provided", "provided_t", t)
|
||||
return *resp, errors.New("invalid type")
|
||||
}
|
||||
if pageNum <= 0 {
|
||||
pageNum = 1
|
||||
}
|
||||
cacheKey := cache.CreateCacheKey("Trending", string(t), region, pageNum)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("Trending: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := s.tmdb.Request("/trending/"+string(t)+"/day", map[string]string{
|
||||
"page": strconv.Itoa(pageNum),
|
||||
"region": region,
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
slog.Error("Failed to complete all trending request!", "error", err.Error())
|
||||
return *resp, errors.New("failed to complete all trending request")
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) DiscoverMovies(
|
||||
o tmdb.DiscoverOptions,
|
||||
pageNum int,
|
||||
region string,
|
||||
) (tmdb.TMDBDiscoverMovies, error) {
|
||||
resp := new(tmdb.TMDBDiscoverMovies)
|
||||
reqParams := map[string]string{
|
||||
"page": strconv.Itoa(pageNum),
|
||||
"region": region,
|
||||
}
|
||||
s.applyDiscoverOptionsToMap(true, o, reqParams)
|
||||
cacheKey := cache.CreateCacheKey("DiscoverMovies", pageNum, reqParams)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("DiscoverMovies: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := s.tmdb.Request("/discover/movie", reqParams, &resp)
|
||||
if err != nil {
|
||||
slog.Error("DiscoverMovies: Failed to complete request!", "error", err.Error())
|
||||
return tmdb.TMDBDiscoverMovies{}, errors.New("failed to complete discover movies request")
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) DiscoverTv(
|
||||
o tmdb.DiscoverOptions,
|
||||
pageNum int,
|
||||
region string,
|
||||
) (tmdb.TMDBDiscoverShows, error) {
|
||||
resp := new(tmdb.TMDBDiscoverShows)
|
||||
reqParams := map[string]string{
|
||||
"page": strconv.Itoa(pageNum),
|
||||
"region": region,
|
||||
}
|
||||
s.applyDiscoverOptionsToMap(false, o, reqParams)
|
||||
cacheKey := cache.CreateCacheKey("DiscoverTv", pageNum, reqParams)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("DiscoverTv: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := s.tmdb.Request("/discover/tv", reqParams, &resp)
|
||||
if err != nil {
|
||||
slog.Error("DiscoverTv: Failed to complete request!", "error", err.Error())
|
||||
return tmdb.TMDBDiscoverShows{}, errors.New("failed to complete discover tv request")
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) applyDiscoverOptionsToMap(
|
||||
// Some properties are named differently for sorting the same thing as far
|
||||
// as we care, so we need to differenciate to name them properly.
|
||||
forMovie bool,
|
||||
o tmdb.DiscoverOptions,
|
||||
m map[string]string,
|
||||
) {
|
||||
releaseDateMinKey := "release_date.gte"
|
||||
releaseDateMaxKey := "release_date.lte"
|
||||
withReleaseTypeKey := "with_release_type"
|
||||
if !forMovie {
|
||||
// Replace with names for equivalent tv filters
|
||||
releaseDateMinKey = "first_air_date.gte"
|
||||
releaseDateMaxKey = "first_air_date.lte"
|
||||
withReleaseTypeKey = "with_type"
|
||||
}
|
||||
if !o.ReleaseDateMin.IsZero() {
|
||||
m[releaseDateMinKey] = o.ReleaseDateMin.Format("2006-01-02")
|
||||
}
|
||||
if !o.ReleaseDateMax.IsZero() {
|
||||
m[releaseDateMaxKey] = o.ReleaseDateMax.Format("2006-01-02")
|
||||
}
|
||||
if o.WithReleaseType != "" {
|
||||
m[withReleaseTypeKey] = o.WithReleaseType
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) PopularPeople(pageNum int) (tmdb.TMDBPopularPeople, error) {
|
||||
cacheKey := cache.CreateCacheKey("PopularPeople", pageNum)
|
||||
resp := new(tmdb.TMDBPopularPeople)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("PopularPeople: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := s.tmdb.Request("/person/popular",
|
||||
map[string]string{"page": strconv.Itoa(pageNum)},
|
||||
&resp)
|
||||
if err != nil {
|
||||
slog.Error("PopularPeople: Failed to complete request!", "error", err.Error())
|
||||
return tmdb.TMDBPopularPeople{}, errors.New("failed to complete request")
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) Regions() (tmdb.TMDBRegions, error) {
|
||||
resp := new(tmdb.TMDBRegions)
|
||||
err := s.tmdb.Request("/watch/providers/regions", map[string]string{}, &resp)
|
||||
if err != nil {
|
||||
slog.Error("Failed to complete regions request!", "error", err.Error())
|
||||
return tmdb.TMDBRegions{}, errors.New("failed to complete regions request")
|
||||
}
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/sbondCo/Watcharr/domain"
|
||||
"github.com/sbondCo/Watcharr/feature/auth/authmiddleware"
|
||||
"github.com/sbondCo/Watcharr/feature/watched/addedtocontent"
|
||||
"github.com/sbondCo/Watcharr/media/tmdb"
|
||||
"github.com/sbondCo/Watcharr/router"
|
||||
"github.com/sbondCo/Watcharr/util"
|
||||
)
|
||||
@@ -23,16 +24,23 @@ type WatchedProvider interface {
|
||||
}
|
||||
|
||||
type Router struct {
|
||||
br *router.BaseRouter
|
||||
cs *Service
|
||||
wp WatchedProvider
|
||||
br *router.BaseRouter
|
||||
cs *Service
|
||||
wp WatchedProvider
|
||||
tmdb *tmdb.TMDB
|
||||
}
|
||||
|
||||
func NewRouter(br *router.BaseRouter, cs *Service, wp WatchedProvider) *Router {
|
||||
func NewRouter(
|
||||
br *router.BaseRouter,
|
||||
cs *Service,
|
||||
wp WatchedProvider,
|
||||
tmdb *tmdb.TMDB,
|
||||
) *Router {
|
||||
return &Router{
|
||||
br: br,
|
||||
cs: cs,
|
||||
wp: wp,
|
||||
br: br,
|
||||
cs: cs,
|
||||
wp: wp,
|
||||
tmdb: tmdb,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +76,7 @@ func (r *Router) GetMovieDetails(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, router.ErrorResponse{Error: "an id was not provided"})
|
||||
return
|
||||
}
|
||||
content, err := r.cs.MovieDetails(
|
||||
content, err := r.tmdb.MovieDetails(
|
||||
c.Param("id"),
|
||||
c.MustGet("userCountry").(string),
|
||||
map[string]string{
|
||||
@@ -111,7 +119,7 @@ func (r *Router) GetMovieCredits(c *gin.Context) {
|
||||
c.Status(400)
|
||||
return
|
||||
}
|
||||
content, err := r.cs.MovieCredits(c.Param("id"))
|
||||
content, err := r.tmdb.MovieCredits(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, router.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
@@ -126,7 +134,7 @@ func (r *Router) GetTvDetails(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
// 1. Get details
|
||||
content, err := r.cs.TvDetails(
|
||||
content, err := r.tmdb.ShowDetails(
|
||||
c.Param("id"),
|
||||
c.MustGet("userCountry").(string),
|
||||
map[string]string{
|
||||
@@ -169,7 +177,7 @@ func (r *Router) GetTvCredits(c *gin.Context) {
|
||||
c.Status(400)
|
||||
return
|
||||
}
|
||||
content, err := r.cs.TvCredits(c.Param("id"))
|
||||
content, err := r.tmdb.ShowCredits(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, router.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
@@ -184,7 +192,7 @@ func (r *Router) GetSeasonDetails(c *gin.Context) {
|
||||
c.Status(400)
|
||||
return
|
||||
}
|
||||
content, err := r.cs.SeasonDetails(c.Param("id"), c.Param("num"))
|
||||
content, err := r.tmdb.SeasonDetails(c.Param("id"), c.Param("num"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, router.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
@@ -221,7 +229,7 @@ func (r *Router) GetPerson(c *gin.Context) {
|
||||
c.Status(400)
|
||||
return
|
||||
}
|
||||
content, err := r.cs.PersonDetails(c.Param("id"))
|
||||
content, err := r.tmdb.PersonDetails(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, router.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
@@ -235,7 +243,7 @@ func (r *Router) GetPersonCredits(c *gin.Context) {
|
||||
c.Status(400)
|
||||
return
|
||||
}
|
||||
content, err := r.cs.PersonCredits(c.Param("id"))
|
||||
content, err := r.tmdb.PersonCredits(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, router.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
@@ -264,7 +272,7 @@ func (r *Router) GetPersonCredits(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (r *Router) GetRegions(c *gin.Context) {
|
||||
re, err := r.cs.Regions()
|
||||
re, err := r.tmdb.Regions()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, router.ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
|
||||
@@ -11,28 +11,21 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ContentProvider interface {
|
||||
Trending(t tmdb.TrendingType, pageNum int, region string) (tmdb.TMDBTrendingCombined, error)
|
||||
DiscoverMovies(o tmdb.DiscoverOptions, pageNum int, region string) (tmdb.TMDBDiscoverMovies, error)
|
||||
DiscoverTv(o tmdb.DiscoverOptions, pageNum int, region string) (tmdb.TMDBDiscoverShows, error)
|
||||
PopularPeople(pageNum int) (tmdb.TMDBPopularPeople, error)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
cfg *config.ServerConfig
|
||||
contentProvider ContentProvider
|
||||
db *gorm.DB
|
||||
cfg *config.ServerConfig
|
||||
tmdb *tmdb.TMDB
|
||||
}
|
||||
|
||||
func NewService(
|
||||
db *gorm.DB,
|
||||
cfg *config.ServerConfig,
|
||||
contentProvider ContentProvider,
|
||||
tmdb *tmdb.TMDB,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db,
|
||||
cfg,
|
||||
contentProvider,
|
||||
tmdb,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +160,7 @@ func (s *Service) discoverMultiTrending(
|
||||
meta domain.DiscoverRequestMeta,
|
||||
resp *domain.DiscoverResponse,
|
||||
) error {
|
||||
tmdbRes, err := s.contentProvider.Trending(t, meta.PageParams.Page, meta.Region)
|
||||
tmdbRes, err := s.tmdb.Trending(t, meta.PageParams.Page, meta.Region)
|
||||
if err != nil {
|
||||
slog.Error("discoverMulti: Failed to search tmdb!", "error", err)
|
||||
return errors.New("content request failed")
|
||||
@@ -188,7 +181,7 @@ func (s *Service) discoverMovieInTheatres(
|
||||
meta domain.DiscoverRequestMeta,
|
||||
resp *domain.DiscoverResponse,
|
||||
) error {
|
||||
tmdbRes, err := s.contentProvider.DiscoverMovies(
|
||||
tmdbRes, err := s.tmdb.DiscoverMovies(
|
||||
tmdb.DiscoverOptions{
|
||||
ReleaseDateMin: time.Now().AddDate(0, 0, -40),
|
||||
ReleaseDateMax: time.Now().AddDate(0, 0, 2),
|
||||
@@ -218,7 +211,7 @@ func (s *Service) discoverMovieUpcoming(
|
||||
meta domain.DiscoverRequestMeta,
|
||||
resp *domain.DiscoverResponse,
|
||||
) error {
|
||||
tmdbRes, err := s.contentProvider.DiscoverMovies(
|
||||
tmdbRes, err := s.tmdb.DiscoverMovies(
|
||||
tmdb.DiscoverOptions{
|
||||
ReleaseDateMin: time.Now(),
|
||||
ReleaseDateMax: time.Now().AddDate(0, 1, 0),
|
||||
@@ -248,7 +241,7 @@ func (s *Service) discoverMoviePopular(
|
||||
meta domain.DiscoverRequestMeta,
|
||||
resp *domain.DiscoverResponse,
|
||||
) error {
|
||||
tmdbRes, err := s.contentProvider.DiscoverMovies(
|
||||
tmdbRes, err := s.tmdb.DiscoverMovies(
|
||||
tmdb.DiscoverOptions{},
|
||||
meta.PageParams.Page,
|
||||
meta.Region,
|
||||
@@ -274,7 +267,7 @@ func (s *Service) discoverTvUpcoming(
|
||||
meta domain.DiscoverRequestMeta,
|
||||
resp *domain.DiscoverResponse,
|
||||
) error {
|
||||
tmdbRes, err := s.contentProvider.DiscoverTv(
|
||||
tmdbRes, err := s.tmdb.DiscoverShows(
|
||||
tmdb.DiscoverOptions{
|
||||
ReleaseDateMin: time.Now(),
|
||||
ReleaseDateMax: time.Now().AddDate(0, 1, 0),
|
||||
@@ -304,7 +297,7 @@ func (s *Service) discoverTvPopular(
|
||||
meta domain.DiscoverRequestMeta,
|
||||
resp *domain.DiscoverResponse,
|
||||
) error {
|
||||
tmdbRes, err := s.contentProvider.DiscoverTv(
|
||||
tmdbRes, err := s.tmdb.DiscoverShows(
|
||||
tmdb.DiscoverOptions{},
|
||||
meta.PageParams.Page,
|
||||
meta.Region,
|
||||
@@ -330,7 +323,7 @@ func (s *Service) discoverPeoplePopular(
|
||||
meta domain.DiscoverRequestMeta,
|
||||
resp *domain.DiscoverResponse,
|
||||
) error {
|
||||
tmdbRes, err := s.contentProvider.PopularPeople(
|
||||
tmdbRes, err := s.tmdb.PopularPeople(
|
||||
meta.PageParams.Page,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -30,10 +30,6 @@ type WatchedEpisodeProvider interface {
|
||||
AddWatchedEpisodes(userId uint, ar episode.WatchedEpisodeAddRequest) (episode.WatchedEpisodeAddResponse, error)
|
||||
}
|
||||
|
||||
type ContentProvider interface {
|
||||
SearchByExternalId(id string, source string) (tmdb.TMDBSearchMultiResponse, error)
|
||||
}
|
||||
|
||||
type TagProvider interface {
|
||||
AddTag(userId uint, tr domain.TagAddRequest) (entity.Tag, error)
|
||||
GetTagByNameAndColor(userId uint, tagName string, tagColor string, tagBgColor string) (entity.Tag, error)
|
||||
@@ -48,7 +44,7 @@ type Service struct {
|
||||
wp WatchedProvider
|
||||
wsp WatchedSeasonProvider
|
||||
wep WatchedEpisodeProvider
|
||||
cp ContentProvider
|
||||
tmdb *tmdb.TMDB
|
||||
activityProvider domain.ActivityAddProvider
|
||||
tagProvider TagProvider
|
||||
searchProvider SearchProvider
|
||||
@@ -59,7 +55,7 @@ func NewService(
|
||||
wp WatchedProvider,
|
||||
wsp WatchedSeasonProvider,
|
||||
wep WatchedEpisodeProvider,
|
||||
cp ContentProvider,
|
||||
tmdb *tmdb.TMDB,
|
||||
activityProvider domain.ActivityAddProvider,
|
||||
tagProvider TagProvider,
|
||||
searchProvider SearchProvider,
|
||||
@@ -69,7 +65,7 @@ func NewService(
|
||||
wp,
|
||||
wsp,
|
||||
wep,
|
||||
cp,
|
||||
tmdb,
|
||||
activityProvider,
|
||||
tagProvider,
|
||||
searchProvider,
|
||||
|
||||
@@ -151,7 +151,7 @@ func (s *Service) importWithIMDBID(
|
||||
userId uint,
|
||||
ar *domain.ImportRequest,
|
||||
) (domain.ImportResponse, error) {
|
||||
if imdbResp, err := s.cp.SearchByExternalId(ar.ImdbID, "imdb"); err == nil {
|
||||
if imdbResp, err := s.tmdb.SearchByExternalId(ar.ImdbID, "imdb"); err == nil {
|
||||
if len(imdbResp.Results) == 1 {
|
||||
onlyResult := imdbResp.Results[0]
|
||||
if onlyResult.MediaType == string(entity.MOVIE) || onlyResult.MediaType == string(entity.SHOW) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// All accepted query filters for any media search.
|
||||
type AllParsableQueryFilters struct {
|
||||
Year int
|
||||
FirstYear int
|
||||
Adult bool
|
||||
}
|
||||
|
||||
// Takes query in and parses out any inline filters into a struct.
|
||||
// Takes out anything in supported format: `a:b`.
|
||||
// Returns:
|
||||
// - query string with any parsed filters removed.
|
||||
// - the parsed filters;
|
||||
func parseQueryFilters(query string) (string, AllParsableQueryFilters) {
|
||||
const segSplitStr = " "
|
||||
|
||||
m := AllParsableQueryFilters{}
|
||||
segs /* hehe */ := strings.SplitSeq(query, segSplitStr)
|
||||
// Segments of the query that aren't filters are added back to this slice
|
||||
// and filters are not, so when we join this slice back into a string at the
|
||||
// end, we are left with only the query and not any filters.
|
||||
finalQuery := []string{}
|
||||
notParsing := func(s string) {
|
||||
finalQuery = append(finalQuery, s)
|
||||
}
|
||||
for seg := range segs {
|
||||
split := strings.Split(seg, ":")
|
||||
if len(split) != 2 || (split[0] == "" || split[1] == "") {
|
||||
// We only support filtername:value, so:
|
||||
// - more or less than len of 2 = wrong; and
|
||||
// - k or v being empty = wrong.
|
||||
notParsing(seg)
|
||||
continue
|
||||
}
|
||||
// Add filter to struct.
|
||||
fkey := strings.ToLower(split[0])
|
||||
switch fkey {
|
||||
case "year", "y":
|
||||
i, _ := strconv.Atoi(split[1])
|
||||
m.Year = i
|
||||
case "fyear", "fy":
|
||||
i, _ := strconv.Atoi(split[1])
|
||||
m.FirstYear = i
|
||||
case "adult":
|
||||
if split[1] != "" {
|
||||
m.Adult = true
|
||||
}
|
||||
default:
|
||||
// If no key matches a supported one, then don't parse this either.
|
||||
notParsing(seg)
|
||||
}
|
||||
}
|
||||
slog.Debug("ParseQueryFilters: Done job.",
|
||||
"query", query,
|
||||
"finalQuery", finalQuery,
|
||||
"parsed_filters", m)
|
||||
return strings.Join(finalQuery, segSplitStr), m
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type Expectation struct {
|
||||
Query string
|
||||
Struct AllParsableQueryFilters
|
||||
}
|
||||
|
||||
func TestParseQueryFilters(t *testing.T) {
|
||||
testSet := map[string]Expectation{
|
||||
// No effect on query without any filters.
|
||||
"Joker": {
|
||||
Query: "Joker",
|
||||
Struct: AllParsableQueryFilters{},
|
||||
},
|
||||
// Parses a lone filter correctly.
|
||||
"Joker year:2024": {
|
||||
Query: "Joker",
|
||||
Struct: AllParsableQueryFilters{
|
||||
Year: 2024,
|
||||
},
|
||||
},
|
||||
// Multiple filters should parse successfully.
|
||||
"Harvest MOON year:2024 adult:true": {
|
||||
Query: "Harvest MOON",
|
||||
Struct: AllParsableQueryFilters{
|
||||
Year: 2024,
|
||||
Adult: true,
|
||||
},
|
||||
},
|
||||
// Filter before or after the title should still be parsed.
|
||||
"y:1999 Joker 2 adult:1": {
|
||||
Query: "Joker 2",
|
||||
Struct: AllParsableQueryFilters{
|
||||
Year: 1999,
|
||||
Adult: true,
|
||||
},
|
||||
},
|
||||
// Test that the `2:` doesnt get parsed, since it's common for media to
|
||||
// have names that use colons like that.
|
||||
"y:t Joker 2: The sun rises! adult:1": {
|
||||
Query: "Joker 2: The sun rises!",
|
||||
Struct: AllParsableQueryFilters{
|
||||
Year: 0,
|
||||
Adult: true,
|
||||
},
|
||||
},
|
||||
// Test that a non whitelisted key "man", doesn't get parsed and removed
|
||||
// from the query.
|
||||
"spider man:new": {
|
||||
Query: "spider man:new",
|
||||
Struct: AllParsableQueryFilters{},
|
||||
},
|
||||
}
|
||||
|
||||
for query, exp := range testSet {
|
||||
q, m := parseQueryFilters(query)
|
||||
if q != exp.Query {
|
||||
t.Errorf("query '%v' doesn't match expected query '%v'", q, exp.Query)
|
||||
}
|
||||
if m != exp.Struct {
|
||||
t.Errorf("%v doesn't match expected %v", m, exp.Struct)
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
-270
@@ -7,8 +7,6 @@ package search
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/sbondCo/Watcharr/config"
|
||||
"github.com/sbondCo/Watcharr/database/entity"
|
||||
@@ -18,16 +16,6 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ContentProvider interface {
|
||||
SearchContent(query string, pageNum int) (tmdb.TMDBSearchMultiResponse, error)
|
||||
SearchMovies(query string, pageNum int) (tmdb.TMDBSearchMoviesResponse, error)
|
||||
SearchTv(query string, pageNum int) (tmdb.TMDBSearchShowsResponse, error)
|
||||
SearchPeople(query string, pageNum int) (tmdb.TMDBSearchPeopleResponse, error)
|
||||
SearchByExternalId(id string, source string) (tmdb.TMDBSearchMultiResponse, error)
|
||||
MovieDetails(id string, country string, rParams map[string]string) (tmdb.TMDBMovieDetails, error)
|
||||
TvDetails(id string, country string, rParams map[string]string) (tmdb.TMDBShowDetails, error)
|
||||
}
|
||||
|
||||
type ServiceWatchedProvider interface {
|
||||
GetWatchedPage(userId uint, pp util.PaginationParams, wr domain.WatchedGetPageRequest, extraProps *domain.WatchedGetPageExtraProps) (util.PaginationResponse[entity.Watched, util.None], error)
|
||||
}
|
||||
@@ -35,20 +23,20 @@ type ServiceWatchedProvider interface {
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
cfg *config.ServerConfig
|
||||
contentProvider ContentProvider
|
||||
tmdb *tmdb.TMDB
|
||||
watchedProvider ServiceWatchedProvider
|
||||
}
|
||||
|
||||
func NewService(
|
||||
db *gorm.DB,
|
||||
cfg *config.ServerConfig,
|
||||
contentProvider ContentProvider,
|
||||
tmdb *tmdb.TMDB,
|
||||
watchedProvider ServiceWatchedProvider,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db,
|
||||
cfg,
|
||||
contentProvider,
|
||||
tmdb,
|
||||
watchedProvider,
|
||||
}
|
||||
}
|
||||
@@ -82,21 +70,52 @@ func (s *Service) Search(
|
||||
}
|
||||
}
|
||||
|
||||
// Parse query filters.
|
||||
query, qfilters := parseQueryFilters(r.Query)
|
||||
|
||||
switch r.Type {
|
||||
case domain.SearchTypeMulti:
|
||||
if err := s.searchMulti(r.Query, pp.Page, &resp); err != nil {
|
||||
sreq := tmdb.SearchUniversalOptions{
|
||||
Query: query,
|
||||
Page: pp.Page,
|
||||
Adult: qfilters.Adult,
|
||||
}
|
||||
if err := s.searchMulti(sreq, &resp); err != nil {
|
||||
return resp, errors.New("multi search failed")
|
||||
}
|
||||
case domain.SearchTypeMovie:
|
||||
if err := s.searchMovie(r.Query, pp.Page, &resp); err != nil {
|
||||
sreq := tmdb.SearchMoviesOptions{
|
||||
SearchUniversalOptions: tmdb.SearchUniversalOptions{
|
||||
Query: query,
|
||||
Page: pp.Page,
|
||||
Adult: qfilters.Adult,
|
||||
},
|
||||
Year: qfilters.Year,
|
||||
PrimaryYear: qfilters.FirstYear,
|
||||
}
|
||||
if err := s.searchMovie(sreq, &resp); err != nil {
|
||||
return resp, errors.New("movie search failed")
|
||||
}
|
||||
case domain.SearchTypeShow:
|
||||
if err := s.searchTv(r.Query, pp.Page, &resp); err != nil {
|
||||
return resp, errors.New("tv search failed")
|
||||
sreq := tmdb.SearchShowsOptions{
|
||||
SearchUniversalOptions: tmdb.SearchUniversalOptions{
|
||||
Query: query,
|
||||
Page: pp.Page,
|
||||
Adult: qfilters.Adult,
|
||||
},
|
||||
Year: qfilters.Year,
|
||||
PrimaryYear: qfilters.FirstYear,
|
||||
}
|
||||
if err := s.searchShow(sreq, &resp); err != nil {
|
||||
return resp, errors.New("show search failed")
|
||||
}
|
||||
case domain.SearchTypePerson:
|
||||
if err := s.searchPeople(r.Query, pp.Page, &resp); err != nil {
|
||||
sreq := tmdb.SearchUniversalOptions{
|
||||
Query: query,
|
||||
Page: pp.Page,
|
||||
Adult: qfilters.Adult,
|
||||
}
|
||||
if err := s.searchPeople(sreq, &resp); err != nil {
|
||||
return resp, errors.New("person search failed")
|
||||
}
|
||||
case domain.SearchTypeGame:
|
||||
@@ -112,13 +131,12 @@ func (s *Service) Search(
|
||||
// TODO either with a header OR a result added to array of type error
|
||||
// SearchMulti is TMDB Multi search but with game data added to first page.
|
||||
func (s *Service) searchMulti(
|
||||
query string,
|
||||
page int,
|
||||
req tmdb.SearchUniversalOptions,
|
||||
resp *domain.SearchResponse,
|
||||
) error {
|
||||
slog.Debug("searchMulti: Running.", "query", query, "page", page)
|
||||
slog.Debug("searchMulti: Running.", "req", req)
|
||||
// TMDB
|
||||
tmdbRes, err := s.contentProvider.SearchContent(query, page)
|
||||
tmdbRes, err := s.tmdb.SearchMulti(req)
|
||||
if err != nil {
|
||||
slog.Error("SearchMulti: Failed to search tmdb!", "error", err)
|
||||
return errors.New("content request failed")
|
||||
@@ -130,8 +148,8 @@ func (s *Service) searchMulti(
|
||||
)
|
||||
}
|
||||
// IGDB (we will only get results for the first page)
|
||||
if page == 1 && s.cfg.TwitchEnabled() {
|
||||
igdbRes, err := s.cfg.TWITCH.Search(query)
|
||||
if req.Page == 1 && s.cfg.TwitchEnabled() {
|
||||
igdbRes, err := s.cfg.TWITCH.Search(req.Query)
|
||||
if err != nil {
|
||||
slog.Error("SearchMulti: Failed to search igdb!", "error", err)
|
||||
return errors.New("content request failed")
|
||||
@@ -150,12 +168,11 @@ func (s *Service) searchMulti(
|
||||
}
|
||||
|
||||
func (s *Service) searchMovie(
|
||||
query string,
|
||||
page int,
|
||||
req tmdb.SearchMoviesOptions,
|
||||
resp *domain.SearchResponse,
|
||||
) error {
|
||||
slog.Debug("searchMovie: Running.", "query", query, "page", page)
|
||||
tmdbRes, err := s.contentProvider.SearchMovies(query, page)
|
||||
slog.Debug("searchMovie: Running.", "req", req)
|
||||
tmdbRes, err := s.tmdb.SearchMovies(req)
|
||||
if err != nil {
|
||||
slog.Error("SearchMovie: Failed to search tmdb!", "error", err)
|
||||
return errors.New("content request failed")
|
||||
@@ -177,7 +194,7 @@ func (s *Service) searchMovieById(
|
||||
resp *domain.SearchResponse,
|
||||
) error {
|
||||
slog.Debug("searchMovieById: Running.", "id", id)
|
||||
details, err := s.contentProvider.MovieDetails(id, "", map[string]string{})
|
||||
details, err := s.tmdb.MovieDetails(id, "", map[string]string{})
|
||||
if err != nil {
|
||||
slog.Error("searchMovieById: Failed to search tmdb!", "error", err)
|
||||
return errors.New("content request failed")
|
||||
@@ -192,15 +209,14 @@ func (s *Service) searchMovieById(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) searchTv(
|
||||
query string,
|
||||
page int,
|
||||
func (s *Service) searchShow(
|
||||
req tmdb.SearchShowsOptions,
|
||||
resp *domain.SearchResponse,
|
||||
) error {
|
||||
slog.Debug("searchTv: Running.", "query", query, "page", page)
|
||||
tmdbRes, err := s.contentProvider.SearchTv(query, page)
|
||||
slog.Debug("searchTv: Running.", "req", req)
|
||||
tmdbRes, err := s.tmdb.SearchShows(req)
|
||||
if err != nil {
|
||||
slog.Error("searchTv: Failed to search tmdb!", "error", err)
|
||||
slog.Error("searchShow: Failed to search tmdb!", "error", err)
|
||||
return errors.New("content request failed")
|
||||
}
|
||||
for _, v := range tmdbRes.Results {
|
||||
@@ -220,7 +236,7 @@ func (s *Service) searchTvById(
|
||||
resp *domain.SearchResponse,
|
||||
) error {
|
||||
slog.Debug("searchTvById: Running.", "id", id)
|
||||
details, err := s.contentProvider.TvDetails(id, "", map[string]string{})
|
||||
details, err := s.tmdb.ShowDetails(id, "", map[string]string{})
|
||||
if err != nil {
|
||||
slog.Error("searchTvById: Failed to search tmdb!", "error", err)
|
||||
return errors.New("content request failed")
|
||||
@@ -236,12 +252,11 @@ func (s *Service) searchTvById(
|
||||
}
|
||||
|
||||
func (s *Service) searchPeople(
|
||||
query string,
|
||||
page int,
|
||||
req tmdb.SearchUniversalOptions,
|
||||
resp *domain.SearchResponse,
|
||||
) error {
|
||||
slog.Debug("searchPeople: Running.", "query", query, "page", page)
|
||||
tmdbRes, err := s.contentProvider.SearchPeople(query, page)
|
||||
slog.Debug("searchPeople: Running.", "req", req)
|
||||
tmdbRes, err := s.tmdb.SearchPeople(req)
|
||||
if err != nil {
|
||||
slog.Error("searchPeople: Failed to search tmdb!", "error", err)
|
||||
return errors.New("content request failed")
|
||||
@@ -353,231 +368,3 @@ func (s *Service) searchMyList(
|
||||
resp.TotalResults = internalRes.TotalResults
|
||||
return nil
|
||||
}
|
||||
|
||||
// Perform "special" direct search if possible using search query.
|
||||
// Eg: Search term is in provider:id format or is a supported url.
|
||||
func (s *Service) searchExtProviderById(
|
||||
query string,
|
||||
resp *domain.SearchResponse,
|
||||
) bool {
|
||||
queryLower := strings.ToLower(query)
|
||||
|
||||
provider, providerID := s.getExtProviderFromQuery(queryLower)
|
||||
|
||||
if provider == "" || providerID == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
slog.Debug("searchExtProviderById: Processing.",
|
||||
"provider", provider,
|
||||
"provider_id", providerID)
|
||||
|
||||
switch provider {
|
||||
case "movie":
|
||||
if err := s.searchMovieById(providerID, resp); err == nil {
|
||||
return true
|
||||
}
|
||||
case "tv":
|
||||
if err := s.searchTvById(providerID, resp); err == nil {
|
||||
return true
|
||||
}
|
||||
case "igdb":
|
||||
if err := s.searchGameById(providerID, resp); err == nil {
|
||||
return true
|
||||
}
|
||||
case "igdb-slug":
|
||||
if err := s.searchGameBySlug(providerID, resp); err == nil {
|
||||
return true
|
||||
}
|
||||
default:
|
||||
// By default, if provider name isn't caught in above cases, just send
|
||||
// it to tmdb external id search.
|
||||
tmdbRes, err := s.contentProvider.SearchByExternalId(
|
||||
providerID,
|
||||
provider,
|
||||
)
|
||||
if err != nil {
|
||||
slog.Error("searchExtProviderById: Failed to search tmdb!", "error", err)
|
||||
return false
|
||||
}
|
||||
resLen := len(tmdbRes.Results)
|
||||
if resLen <= 0 {
|
||||
return false
|
||||
}
|
||||
for _, v := range tmdbRes.Results {
|
||||
resp.Results = append(
|
||||
resp.Results,
|
||||
v.AsMedia(),
|
||||
)
|
||||
}
|
||||
resp.Page = 1
|
||||
resp.TotalPages = 1
|
||||
resp.TotalResults = int64(resLen)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Takes in query and returns (Provider, ProviderID) if found.
|
||||
func (s *Service) getExtProviderFromQuery(queryLower string) (string, string) {
|
||||
var provider string
|
||||
|
||||
// Before checking for provider:providerid format, check if query is
|
||||
// a supported url.
|
||||
if p, i := s.getExtProviderFromURL(queryLower); p != "" && i != "" {
|
||||
slog.Debug("getExtProviderFromQuery: Returning from parsed url.")
|
||||
return p, i
|
||||
}
|
||||
|
||||
querySplit := strings.Split(queryLower, ":")
|
||||
|
||||
if len(querySplit) != 2 {
|
||||
slog.Debug("getExtProviderFromQuery: querySplit len != 2")
|
||||
return "", ""
|
||||
}
|
||||
|
||||
switch querySplit[0] {
|
||||
case "movie", // TMDB ID target
|
||||
"tv", // TMDB ID target
|
||||
"igdb", // IGDB ID target
|
||||
// The rest below are sent as is to tmdbs find by (external) id api.
|
||||
"imdb",
|
||||
"tvdb",
|
||||
"youtube",
|
||||
"wikidata",
|
||||
"facebook",
|
||||
"instagram",
|
||||
"twitter",
|
||||
"tiktok":
|
||||
provider = querySplit[0]
|
||||
// Any aliases we want to support
|
||||
case "i":
|
||||
case "imd":
|
||||
provider = "imdb"
|
||||
case "wd":
|
||||
case "wdt":
|
||||
provider = "wikidata"
|
||||
case "yt":
|
||||
provider = "youtube"
|
||||
case "thetvdb":
|
||||
provider = "tvdb"
|
||||
case "game":
|
||||
provider = "igdb"
|
||||
case "series":
|
||||
provider = "tv"
|
||||
default:
|
||||
slog.Debug("getExtProviderFromQuery: No provider found.")
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return provider, querySplit[1]
|
||||
}
|
||||
|
||||
// Takes in what may be a url. If it is and is a supported url
|
||||
// Returns (Provider, ProviderID).
|
||||
func (s *Service) getExtProviderFromURL(maybeaurl string) (string, string) {
|
||||
u, err := url.Parse(maybeaurl)
|
||||
if err != nil || u.Host == "" {
|
||||
slog.Debug("getExtProviderFromURL: Doesn't look like a url.")
|
||||
return "", ""
|
||||
}
|
||||
|
||||
hostLower := strings.ToLower(u.Host)
|
||||
slog.Debug("getExtProviderFromURL: Looks like a url.",
|
||||
"host", hostLower,
|
||||
"path", u.Path)
|
||||
|
||||
// Using HasSuffix so for ex: www.imdb.com AND imdb.com will match.
|
||||
if strings.HasSuffix(hostLower, "imdb.com") {
|
||||
return s.getExtProviderIDFromIMDBURL(u)
|
||||
} else if strings.HasSuffix(hostLower, "themoviedb.org") {
|
||||
return s.getExtProviderIDFromTMDBURL(u)
|
||||
} else if strings.HasSuffix(hostLower, "igdb.com") {
|
||||
return s.getExtProviderIDFromIGDBURL(u)
|
||||
}
|
||||
|
||||
return "", " "
|
||||
}
|
||||
|
||||
// Extract id from IMDB url.
|
||||
// Returns (Provider, ProviderID).
|
||||
func (s *Service) getExtProviderIDFromIMDBURL(u *url.URL) (string, string) {
|
||||
segments := strings.Split(
|
||||
// Trim start/end '/' to avoid empty items at start/end
|
||||
// of final slice.
|
||||
strings.Trim(u.Path, "/"),
|
||||
"/",
|
||||
)
|
||||
segmentsLen := len(segments)
|
||||
slog.Debug("getExtProviderIDFromIMDBURL: Parsing path.",
|
||||
"segments", segments,
|
||||
"segments_len", segmentsLen)
|
||||
|
||||
if segmentsLen < 2 ||
|
||||
segments[0] != "title" ||
|
||||
!strings.HasPrefix(segments[1], "tt") {
|
||||
slog.Debug("getExtProviderIDFromIMDBURL: path provided not supported.")
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return "imdb", segments[1]
|
||||
}
|
||||
|
||||
// Extract id from TMDB url.
|
||||
// Returns (Provider, ProviderID).
|
||||
func (s *Service) getExtProviderIDFromTMDBURL(u *url.URL) (string, string) {
|
||||
// Split path by '/'
|
||||
segments := strings.Split(
|
||||
// Trim start/end '/' to avoid empty items at start/end
|
||||
// of final slice.
|
||||
strings.Trim(u.Path, "/"),
|
||||
"/",
|
||||
)
|
||||
segmentsLen := len(segments)
|
||||
slog.Debug("getExtProviderIDFromTMDBURL: Parsing path.",
|
||||
"segments", segments,
|
||||
"segments_len", segmentsLen)
|
||||
|
||||
// Check if segments of the path are valid as a tv/movie page.
|
||||
if segmentsLen < 2 ||
|
||||
(segments[0] != "tv" && segments[0] != "movie") ||
|
||||
segments[1] == "" {
|
||||
slog.Debug("getExtProviderIDFromTMDBURL: path provided not supported.")
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// Extract id from second segment.
|
||||
segs2 := strings.SplitN(segments[1], "-", 2)
|
||||
slog.Debug("getExtProviderIDFromTMDBURL: Parsing media path segment.",
|
||||
"segments", segs2)
|
||||
if len(segs2) != 2 {
|
||||
slog.Warn("getExtProviderIDFromTMDBURL: segs2 doesn't have len of 2.")
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return segments[0], segs2[0]
|
||||
}
|
||||
|
||||
// Extract slug from IGDB url.
|
||||
// Returns (Provider, ProviderID).
|
||||
func (s *Service) getExtProviderIDFromIGDBURL(u *url.URL) (string, string) {
|
||||
segments := strings.Split(
|
||||
// Trim start/end '/' to avoid empty items at start/end
|
||||
// of final slice.
|
||||
strings.Trim(u.Path, "/"),
|
||||
"/",
|
||||
)
|
||||
segmentsLen := len(segments)
|
||||
slog.Debug("getExtProviderIDFromIMDBURL: Parsing path.",
|
||||
"segments", segments,
|
||||
"segments_len", segmentsLen)
|
||||
|
||||
if segmentsLen < 2 ||
|
||||
segments[0] != "games" {
|
||||
slog.Debug("getExtProviderIDFromIMDBURL: path provided not supported.")
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return "igdb-slug", segments[1]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/sbondCo/Watcharr/domain"
|
||||
)
|
||||
|
||||
// Perform "special" direct search if possible using search query.
|
||||
// Eg: Search term is in provider:id format or is a supported url.
|
||||
func (s *Service) searchExtProviderById(
|
||||
query string,
|
||||
resp *domain.SearchResponse,
|
||||
) bool {
|
||||
queryLower := strings.ToLower(query)
|
||||
|
||||
provider, providerID := s.getExtProviderFromQuery(queryLower)
|
||||
|
||||
if provider == "" || providerID == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
slog.Debug("searchExtProviderById: Processing.",
|
||||
"provider", provider,
|
||||
"provider_id", providerID)
|
||||
|
||||
switch provider {
|
||||
case "movie":
|
||||
if err := s.searchMovieById(providerID, resp); err == nil {
|
||||
return true
|
||||
}
|
||||
case "tv":
|
||||
if err := s.searchTvById(providerID, resp); err == nil {
|
||||
return true
|
||||
}
|
||||
case "igdb":
|
||||
if err := s.searchGameById(providerID, resp); err == nil {
|
||||
return true
|
||||
}
|
||||
case "igdb-slug":
|
||||
if err := s.searchGameBySlug(providerID, resp); err == nil {
|
||||
return true
|
||||
}
|
||||
default:
|
||||
// By default, if provider name isn't caught in above cases, just send
|
||||
// it to tmdb external id search.
|
||||
tmdbRes, err := s.tmdb.SearchByExternalId(
|
||||
providerID,
|
||||
provider,
|
||||
)
|
||||
if err != nil {
|
||||
slog.Error("searchExtProviderById: Failed to search tmdb!",
|
||||
"error", err)
|
||||
return false
|
||||
}
|
||||
resLen := len(tmdbRes.Results)
|
||||
if resLen <= 0 {
|
||||
return false
|
||||
}
|
||||
for _, v := range tmdbRes.Results {
|
||||
resp.Results = append(
|
||||
resp.Results,
|
||||
v.AsMedia(),
|
||||
)
|
||||
}
|
||||
resp.Page = 1
|
||||
resp.TotalPages = 1
|
||||
resp.TotalResults = int64(resLen)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Takes in query and returns (Provider, ProviderID) if found.
|
||||
func (s *Service) getExtProviderFromQuery(queryLower string) (string, string) {
|
||||
var provider string
|
||||
|
||||
// Before checking for provider:providerid format, check if query is
|
||||
// a supported url.
|
||||
if p, i := s.getExtProviderFromURL(queryLower); p != "" && i != "" {
|
||||
slog.Debug("getExtProviderFromQuery: Returning from parsed url.")
|
||||
return p, i
|
||||
}
|
||||
|
||||
querySplit := strings.Split(queryLower, ":")
|
||||
|
||||
if len(querySplit) != 2 {
|
||||
slog.Debug("getExtProviderFromQuery: querySplit len != 2")
|
||||
return "", ""
|
||||
}
|
||||
|
||||
switch querySplit[0] {
|
||||
case "movie", // TMDB ID target
|
||||
"tv", // TMDB ID target
|
||||
"igdb", // IGDB ID target
|
||||
// The rest below are sent as is to tmdbs find by (external) id api.
|
||||
"imdb",
|
||||
"tvdb",
|
||||
"youtube",
|
||||
"wikidata",
|
||||
"facebook",
|
||||
"instagram",
|
||||
"twitter",
|
||||
"tiktok":
|
||||
provider = querySplit[0]
|
||||
// Any aliases we want to support
|
||||
case "i":
|
||||
case "imd":
|
||||
provider = "imdb"
|
||||
case "wd":
|
||||
case "wdt":
|
||||
provider = "wikidata"
|
||||
case "yt":
|
||||
provider = "youtube"
|
||||
case "thetvdb":
|
||||
provider = "tvdb"
|
||||
case "game":
|
||||
provider = "igdb"
|
||||
case "series":
|
||||
provider = "tv"
|
||||
default:
|
||||
slog.Debug("getExtProviderFromQuery: No provider found.")
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return provider, querySplit[1]
|
||||
}
|
||||
|
||||
// Takes in what may be a url. If it is and is a supported url
|
||||
// Returns (Provider, ProviderID).
|
||||
func (s *Service) getExtProviderFromURL(maybeaurl string) (string, string) {
|
||||
u, err := url.Parse(maybeaurl)
|
||||
if err != nil || u.Host == "" {
|
||||
slog.Debug("getExtProviderFromURL: Doesn't look like a url.")
|
||||
return "", ""
|
||||
}
|
||||
|
||||
hostLower := strings.ToLower(u.Host)
|
||||
slog.Debug("getExtProviderFromURL: Looks like a url.",
|
||||
"host", hostLower,
|
||||
"path", u.Path)
|
||||
|
||||
// Using HasSuffix so for ex: www.imdb.com AND imdb.com will match.
|
||||
if strings.HasSuffix(hostLower, "imdb.com") {
|
||||
return s.getExtProviderIDFromIMDBURL(u)
|
||||
} else if strings.HasSuffix(hostLower, "themoviedb.org") {
|
||||
return s.getExtProviderIDFromTMDBURL(u)
|
||||
} else if strings.HasSuffix(hostLower, "igdb.com") {
|
||||
return s.getExtProviderIDFromIGDBURL(u)
|
||||
}
|
||||
|
||||
return "", " "
|
||||
}
|
||||
|
||||
// Extract id from IMDB url.
|
||||
// Returns (Provider, ProviderID).
|
||||
func (s *Service) getExtProviderIDFromIMDBURL(u *url.URL) (string, string) {
|
||||
segments := strings.Split(
|
||||
// Trim start/end '/' to avoid empty items at start/end
|
||||
// of final slice.
|
||||
strings.Trim(u.Path, "/"),
|
||||
"/",
|
||||
)
|
||||
segmentsLen := len(segments)
|
||||
slog.Debug("getExtProviderIDFromIMDBURL: Parsing path.",
|
||||
"segments", segments,
|
||||
"segments_len", segmentsLen)
|
||||
|
||||
if segmentsLen < 2 ||
|
||||
segments[0] != "title" ||
|
||||
!strings.HasPrefix(segments[1], "tt") {
|
||||
slog.Debug("getExtProviderIDFromIMDBURL: path provided not supported.")
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return "imdb", segments[1]
|
||||
}
|
||||
|
||||
// Extract id from TMDB url.
|
||||
// Returns (Provider, ProviderID).
|
||||
func (s *Service) getExtProviderIDFromTMDBURL(u *url.URL) (string, string) {
|
||||
// Split path by '/'
|
||||
segments := strings.Split(
|
||||
// Trim start/end '/' to avoid empty items at start/end
|
||||
// of final slice.
|
||||
strings.Trim(u.Path, "/"),
|
||||
"/",
|
||||
)
|
||||
segmentsLen := len(segments)
|
||||
slog.Debug("getExtProviderIDFromTMDBURL: Parsing path.",
|
||||
"segments", segments,
|
||||
"segments_len", segmentsLen)
|
||||
|
||||
// Check if segments of the path are valid as a tv/movie page.
|
||||
if segmentsLen < 2 ||
|
||||
(segments[0] != "tv" && segments[0] != "movie") ||
|
||||
segments[1] == "" {
|
||||
slog.Debug("getExtProviderIDFromTMDBURL: path provided not supported.")
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// Extract id from second segment.
|
||||
segs2 := strings.SplitN(segments[1], "-", 2)
|
||||
slog.Debug("getExtProviderIDFromTMDBURL: Parsing media path segment.",
|
||||
"segments", segs2)
|
||||
if len(segs2) != 2 {
|
||||
slog.Warn("getExtProviderIDFromTMDBURL: segs2 doesn't have len of 2.")
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return segments[0], segs2[0]
|
||||
}
|
||||
|
||||
// Extract slug from IGDB url.
|
||||
// Returns (Provider, ProviderID).
|
||||
func (s *Service) getExtProviderIDFromIGDBURL(u *url.URL) (string, string) {
|
||||
segments := strings.Split(
|
||||
// Trim start/end '/' to avoid empty items at start/end
|
||||
// of final slice.
|
||||
strings.Trim(u.Path, "/"),
|
||||
"/",
|
||||
)
|
||||
segmentsLen := len(segments)
|
||||
slog.Debug("getExtProviderIDFromIMDBURL: Parsing path.",
|
||||
"segments", segments,
|
||||
"segments_len", segmentsLen)
|
||||
|
||||
if segmentsLen < 2 ||
|
||||
segments[0] != "games" {
|
||||
slog.Debug("getExtProviderIDFromIMDBURL: path provided not supported.")
|
||||
return "", ""
|
||||
}
|
||||
|
||||
return "igdb-slug", segments[1]
|
||||
}
|
||||
@@ -53,10 +53,6 @@ type WatchedSeasonProvider interface {
|
||||
AddWatchedSeason(userId uint, ar season.WatchedSeasonAddRequest) (season.WatchedSeasonAddResponse, error)
|
||||
}
|
||||
|
||||
type ContentProvider interface {
|
||||
SeasonDetails(tvId string, seasonNumber string) (tmdb.TMDBSeasonDetails, error)
|
||||
}
|
||||
|
||||
type UserProvider interface {
|
||||
UserGetSettings(userId uint) (entity.UserSettings, error)
|
||||
}
|
||||
@@ -65,7 +61,7 @@ type Service struct {
|
||||
db *gorm.DB
|
||||
wp WatchedProvider
|
||||
wsp WatchedSeasonProvider
|
||||
cp ContentProvider
|
||||
tmdb *tmdb.TMDB
|
||||
activityProvider domain.ActivityAddProvider
|
||||
userProvider UserProvider
|
||||
}
|
||||
@@ -74,7 +70,7 @@ func NewService(
|
||||
db *gorm.DB,
|
||||
wp WatchedProvider,
|
||||
wsp WatchedSeasonProvider,
|
||||
cp ContentProvider,
|
||||
tmdb *tmdb.TMDB,
|
||||
activityProvider domain.ActivityAddProvider,
|
||||
userProvider UserProvider,
|
||||
) *Service {
|
||||
@@ -82,7 +78,7 @@ func NewService(
|
||||
db,
|
||||
wp,
|
||||
wsp,
|
||||
cp,
|
||||
tmdb,
|
||||
activityProvider,
|
||||
userProvider,
|
||||
}
|
||||
@@ -363,7 +359,7 @@ func (s *Service) hookEpisodeStatusChanged(userId uint, watchedId uint, seasonNu
|
||||
// to Watching just above. I think this might never happen to anyone so um ye.
|
||||
tmdbIdStr := strconv.Itoa(watchedShow.Content.TmdbID)
|
||||
seasonNumStr := strconv.Itoa(seasonNum)
|
||||
seasonDetails, err := s.cp.SeasonDetails(tmdbIdStr, seasonNumStr)
|
||||
seasonDetails, err := s.tmdb.SeasonDetails(tmdbIdStr, seasonNumStr)
|
||||
if err != nil {
|
||||
slog.Error("hookEpisodeStatusChanged: Failed to get season details!", "error", err)
|
||||
hookResponse.Errors = append(hookResponse.Errors, "failed to get season details for show")
|
||||
|
||||
@@ -9,25 +9,18 @@ import (
|
||||
"github.com/sbondCo/Watcharr/database/entity"
|
||||
"github.com/sbondCo/Watcharr/domain"
|
||||
"github.com/sbondCo/Watcharr/feature/auth/authmiddleware"
|
||||
"github.com/sbondCo/Watcharr/media/tmdb"
|
||||
"github.com/sbondCo/Watcharr/router"
|
||||
"github.com/sbondCo/Watcharr/util"
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
br *router.BaseRouter
|
||||
t *tmdb.TMDB
|
||||
s *Service
|
||||
}
|
||||
|
||||
func NewRouter(
|
||||
br *router.BaseRouter,
|
||||
t *tmdb.TMDB,
|
||||
service *Service,
|
||||
) *Router {
|
||||
func NewRouter(br *router.BaseRouter, service *Service) *Router {
|
||||
return &Router{
|
||||
br: br,
|
||||
t: t,
|
||||
s: service,
|
||||
}
|
||||
}
|
||||
|
||||
+10
-12
@@ -1,42 +1,40 @@
|
||||
package content
|
||||
package tmdb
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/sbondCo/Watcharr/media/tmdb"
|
||||
)
|
||||
|
||||
// Getting only region needed from api is not a feature yet
|
||||
// https://trello.com/c/75tR4cpF/106-add-watch-provider-region-filtering
|
||||
// When it is, this can be removed for that instead.
|
||||
func transformProviders(c *any, country string) tmdb.WatchProviders {
|
||||
func transformProviders(c *any, country string) WatchProviders {
|
||||
slog.Debug("transformProviders called", "country", country)
|
||||
resp := tmdb.WatchProviders{}
|
||||
resp := WatchProviders{}
|
||||
|
||||
cmap, ok := (*c).(map[string]any)
|
||||
if !ok {
|
||||
slog.Error("transformProviders: Assertion failed")
|
||||
return tmdb.WatchProviders{}
|
||||
return WatchProviders{}
|
||||
}
|
||||
|
||||
rmap, ok := cmap["results"].(map[string]any)
|
||||
if !ok {
|
||||
slog.Warn("transformProviders: Couldn't find results property..")
|
||||
return tmdb.WatchProviders{}
|
||||
return WatchProviders{}
|
||||
}
|
||||
|
||||
val, ok := rmap[country]
|
||||
if !ok {
|
||||
slog.Warn("transformProviders: Couldn't find country..",
|
||||
"country", country)
|
||||
return tmdb.WatchProviders{}
|
||||
return WatchProviders{}
|
||||
}
|
||||
slog.Debug("transformProviders: Found country..", "obj", val)
|
||||
|
||||
rvmap, ok := val.(map[string]any)
|
||||
if !ok {
|
||||
slog.Warn("transformProviders: Couldn't assert country obj")
|
||||
return tmdb.WatchProviders{}
|
||||
return WatchProviders{}
|
||||
}
|
||||
|
||||
// Turning any into a type safe object we can use later.
|
||||
@@ -57,8 +55,8 @@ func transformProviders(c *any, country string) tmdb.WatchProviders {
|
||||
func transformProvidersType(
|
||||
ptype string,
|
||||
rvmap map[string]any,
|
||||
providers []tmdb.WatchProvider,
|
||||
) []tmdb.WatchProvider {
|
||||
providers []WatchProvider,
|
||||
) []WatchProvider {
|
||||
tm, ok := rvmap[ptype].([]any)
|
||||
if !ok {
|
||||
slog.Warn("transformProvidersType: Assertion failed")
|
||||
@@ -74,7 +72,7 @@ func transformProvidersType(
|
||||
continue
|
||||
}
|
||||
providers = append(providers,
|
||||
tmdb.WatchProvider{
|
||||
WatchProvider{
|
||||
ProviderName: providerName,
|
||||
})
|
||||
}
|
||||
@@ -7,14 +7,25 @@ import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
gocache "github.com/robfig/go-cache"
|
||||
"github.com/sbondCo/Watcharr/database/entity"
|
||||
)
|
||||
|
||||
// TODO rewrite tmdb to work like how igdb package was made
|
||||
// TODO The *WithWatched structs likely need to go in the watched package (or with go 1.25 can we
|
||||
// fix needing so many extra structs for the *WithWatched types and functions)
|
||||
|
||||
var ContentStore = gocache.New(time.Hour*24, time.Minute)
|
||||
|
||||
type ContentProvider interface {
|
||||
CacheContentTv(content TMDBShowDetails, onlyUpdate bool) (entity.Content, error)
|
||||
CacheContentMovie(content TMDBMovieDetails, onlyUpdate bool) (entity.Content, error)
|
||||
}
|
||||
|
||||
type TMDB struct {
|
||||
Key string
|
||||
Key string
|
||||
contentProvider ContentProvider
|
||||
}
|
||||
|
||||
func NewTMDB(key string) *TMDB {
|
||||
@@ -23,6 +34,10 @@ func NewTMDB(key string) *TMDB {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TMDB) AddContentProvider(contentProvider ContentProvider) {
|
||||
t.contentProvider = contentProvider
|
||||
}
|
||||
|
||||
func (t *TMDB) GetKey() string {
|
||||
if t.Key != "" {
|
||||
return t.Key //Config.TMDB_KEY
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package tmdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/sbondCo/Watcharr/cache"
|
||||
)
|
||||
|
||||
func (t *TMDB) DiscoverMovies(
|
||||
o DiscoverOptions,
|
||||
pageNum int,
|
||||
region string,
|
||||
) (TMDBDiscoverMovies, error) {
|
||||
resp := new(TMDBDiscoverMovies)
|
||||
reqParams := map[string]string{
|
||||
"page": strconv.Itoa(pageNum),
|
||||
"region": region,
|
||||
}
|
||||
t.applyDiscoverOptionsToMap(true, o, reqParams)
|
||||
cacheKey := cache.CreateCacheKey(
|
||||
"DiscoverMovies",
|
||||
pageNum,
|
||||
reqParams)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("DiscoverMovies: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := t.Request("/discover/movie", reqParams, &resp)
|
||||
if err != nil {
|
||||
slog.Error("DiscoverMovies: Request failed!", "error", err)
|
||||
return TMDBDiscoverMovies{}, errors.New("request failed")
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (t *TMDB) DiscoverShows(
|
||||
o DiscoverOptions,
|
||||
pageNum int,
|
||||
region string,
|
||||
) (TMDBDiscoverShows, error) {
|
||||
resp := new(TMDBDiscoverShows)
|
||||
reqParams := map[string]string{
|
||||
"page": strconv.Itoa(pageNum),
|
||||
"region": region,
|
||||
}
|
||||
t.applyDiscoverOptionsToMap(false, o, reqParams)
|
||||
cacheKey := cache.CreateCacheKey(
|
||||
"DiscoverShows",
|
||||
pageNum,
|
||||
reqParams)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("DiscoverShows: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := t.Request("/discover/tv", reqParams, &resp)
|
||||
if err != nil {
|
||||
slog.Error("DiscoverShows: Request failed!", "error", err)
|
||||
return TMDBDiscoverShows{}, errors.New("request failed")
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (t *TMDB) applyDiscoverOptionsToMap(
|
||||
// Some properties are named differently for sorting the same thing as far
|
||||
// as we care, so we need to differenciate to name them properly.
|
||||
forMovie bool,
|
||||
o DiscoverOptions,
|
||||
m map[string]string,
|
||||
) {
|
||||
releaseDateMinKey := "release_date.gte"
|
||||
releaseDateMaxKey := "release_date.lte"
|
||||
withReleaseTypeKey := "with_release_type"
|
||||
if !forMovie {
|
||||
// Replace with names for equivalent tv filters
|
||||
releaseDateMinKey = "first_air_date.gte"
|
||||
releaseDateMaxKey = "first_air_date.lte"
|
||||
withReleaseTypeKey = "with_type"
|
||||
}
|
||||
if !o.ReleaseDateMin.IsZero() {
|
||||
m[releaseDateMinKey] = o.ReleaseDateMin.Format("2006-01-02")
|
||||
}
|
||||
if !o.ReleaseDateMax.IsZero() {
|
||||
m[releaseDateMaxKey] = o.ReleaseDateMax.Format("2006-01-02")
|
||||
}
|
||||
if o.WithReleaseType != "" {
|
||||
m[withReleaseTypeKey] = o.WithReleaseType
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package tmdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/sbondCo/Watcharr/cache"
|
||||
)
|
||||
|
||||
func (t *TMDB) MovieDetails(
|
||||
id string,
|
||||
country string,
|
||||
rParams map[string]string,
|
||||
) (TMDBMovieDetails, error) {
|
||||
resp := new(TMDBMovieDetails)
|
||||
cacheKey := cache.CreateCacheKey("MovieDetails", id, country, rParams)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("MovieDetails: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := t.Request("/movie/"+id, rParams, &resp)
|
||||
if err != nil {
|
||||
slog.Error("MovieDetails: Request failed!", "error", err)
|
||||
return TMDBMovieDetails{}, errors.New("request failed")
|
||||
}
|
||||
resp.WatchProvidersTransformed = transformProviders(&resp.WatchProviders, country)
|
||||
resp.WatchProviders = nil // We don't want this to linger around (in cache) since we have the transformed version now..
|
||||
go t.contentProvider.CacheContentMovie(*resp, true)
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (t *TMDB) MovieCredits(id string) (TMDBContentCredits, error) {
|
||||
resp := new(TMDBContentCredits)
|
||||
err := t.Request("/movie/"+id+"/credits", map[string]string{}, &resp)
|
||||
if err != nil {
|
||||
slog.Error("MovieCredits: Request failed!", "error", err)
|
||||
return TMDBContentCredits{}, errors.New("request failed")
|
||||
}
|
||||
return *resp, nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package tmdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/sbondCo/Watcharr/cache"
|
||||
)
|
||||
|
||||
func (t *TMDB) PersonDetails(id string) (TMDBPersonDetails, error) {
|
||||
resp := new(TMDBPersonDetails)
|
||||
err := t.Request("/person/"+id, map[string]string{}, &resp)
|
||||
if err != nil {
|
||||
slog.Error("PersonDetails: Request failed!", "error", err)
|
||||
return TMDBPersonDetails{}, errors.New("request failed")
|
||||
}
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (t *TMDB) PersonCredits(id string) (TMDBPersonCombinedCredits, error) {
|
||||
cacheKey := cache.CreateCacheKey("PersonCredits", id)
|
||||
resp := new(TMDBPersonCombinedCredits)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("PersonCredits: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := t.Request(
|
||||
"/person/"+id+"/combined_credits",
|
||||
map[string]string{},
|
||||
&resp)
|
||||
if err != nil {
|
||||
slog.Error("PersonCredits: Request failed!", "error", err)
|
||||
return TMDBPersonCombinedCredits{}, errors.New("request failed")
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (t *TMDB) PopularPeople(pageNum int) (TMDBPopularPeople, error) {
|
||||
cacheKey := cache.CreateCacheKey("PopularPeople", pageNum)
|
||||
resp := new(TMDBPopularPeople)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("PopularPeople: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := t.Request(
|
||||
"/person/popular",
|
||||
map[string]string{"page": strconv.Itoa(pageNum)},
|
||||
&resp)
|
||||
if err != nil {
|
||||
slog.Error("PopularPeople: Request failed!", "error", err)
|
||||
return TMDBPopularPeople{}, errors.New("request failed")
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package tmdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/sbondCo/Watcharr/cache"
|
||||
)
|
||||
|
||||
// This file has all tmdb search methods.
|
||||
// Each helper has their own Options struct.
|
||||
// Each Options struct has an `AsParamsMap` function attached that converts
|
||||
// the properties of the struct into a query params map we can pass to the
|
||||
// requests (in a tmdb supported fashion).
|
||||
|
||||
// **NOTE:** Ensure any new options are also added to the cache keys of the
|
||||
// search functions using them!
|
||||
|
||||
// Options that all the Search structs support.
|
||||
type SearchUniversalOptions struct {
|
||||
Query string
|
||||
Page int
|
||||
Adult bool
|
||||
}
|
||||
|
||||
// Check if SearchUniversalOptions is valid.
|
||||
// Fixes `Page` to equal `1` if it is `0` (unset).
|
||||
func (o *SearchUniversalOptions) Valid() bool {
|
||||
if o.Query == "" {
|
||||
// A query is necessary!
|
||||
return false
|
||||
}
|
||||
if o.Page == 0 {
|
||||
o.Page = 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (o *SearchUniversalOptions) AsParamsMap() map[string]string {
|
||||
m := map[string]string{
|
||||
"query": o.Query,
|
||||
"page": strconv.Itoa(o.Page),
|
||||
}
|
||||
if o.Adult {
|
||||
m["include_adult"] = "true"
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (t *TMDB) SearchMulti(
|
||||
o SearchUniversalOptions,
|
||||
) (TMDBSearchMultiResponse, error) {
|
||||
resp := new(TMDBSearchMultiResponse)
|
||||
if !o.Valid() {
|
||||
return *resp, errors.New("request is invalid")
|
||||
}
|
||||
cacheKey := cache.CreateCacheKey(
|
||||
"SearchMulti",
|
||||
o.Query,
|
||||
o.Page,
|
||||
o.Adult)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("SearchMulti: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := t.Request(
|
||||
"/search/multi",
|
||||
o.AsParamsMap(),
|
||||
&resp)
|
||||
if err != nil {
|
||||
slog.Error("SearchMulti: Request failed!", "error", err)
|
||||
return TMDBSearchMultiResponse{}, errors.New("request failed")
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
type SearchMoviesOptions struct {
|
||||
SearchUniversalOptions
|
||||
Year int
|
||||
PrimaryYear int
|
||||
}
|
||||
|
||||
func (o *SearchMoviesOptions) AsParamsMap() map[string]string {
|
||||
m := o.SearchUniversalOptions.AsParamsMap()
|
||||
if o.Year != 0 {
|
||||
m["year"] = strconv.Itoa(o.Year)
|
||||
}
|
||||
if o.PrimaryYear != 0 {
|
||||
m["primary_release_year"] = strconv.Itoa(o.PrimaryYear)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (t *TMDB) SearchMovies(
|
||||
o SearchMoviesOptions,
|
||||
) (TMDBSearchMoviesResponse, error) {
|
||||
resp := new(TMDBSearchMoviesResponse)
|
||||
if !o.Valid() {
|
||||
return *resp, errors.New("request is invalid")
|
||||
}
|
||||
cacheKey := cache.CreateCacheKey(
|
||||
"SearchMovies",
|
||||
o.Query,
|
||||
o.Page,
|
||||
o.Adult,
|
||||
o.Year,
|
||||
o.PrimaryYear)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("SearchMovies: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := t.Request(
|
||||
"/search/movie",
|
||||
o.AsParamsMap(),
|
||||
&resp)
|
||||
if err != nil {
|
||||
slog.Error("SearchMovies: Request failed!", "error", err)
|
||||
return TMDBSearchMoviesResponse{}, errors.New("request failed")
|
||||
}
|
||||
for i := range resp.Results {
|
||||
resp.Results[i].MediaType = "movie"
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
type SearchShowsOptions struct {
|
||||
SearchUniversalOptions
|
||||
Year int
|
||||
PrimaryYear int
|
||||
}
|
||||
|
||||
func (o *SearchShowsOptions) AsParamsMap() map[string]string {
|
||||
m := o.SearchUniversalOptions.AsParamsMap()
|
||||
if o.Year != 0 {
|
||||
m["year"] = strconv.Itoa(o.Year)
|
||||
}
|
||||
if o.PrimaryYear != 0 {
|
||||
m["first_air_date_year"] = strconv.Itoa(o.PrimaryYear)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (t *TMDB) SearchShows(
|
||||
o SearchShowsOptions,
|
||||
) (TMDBSearchShowsResponse, error) {
|
||||
resp := new(TMDBSearchShowsResponse)
|
||||
if !o.Valid() {
|
||||
return *resp, errors.New("request is invalid")
|
||||
}
|
||||
cacheKey := cache.CreateCacheKey(
|
||||
"SearchShows",
|
||||
o.Query,
|
||||
o.Page,
|
||||
o.Adult,
|
||||
o.Year,
|
||||
o.PrimaryYear)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("SearchShows: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := t.Request(
|
||||
"/search/tv",
|
||||
o.AsParamsMap(),
|
||||
&resp)
|
||||
if err != nil {
|
||||
slog.Error("SearchShows: Request failed!", "error", err)
|
||||
return TMDBSearchShowsResponse{}, errors.New("request failed")
|
||||
}
|
||||
for i := range resp.Results {
|
||||
resp.Results[i].MediaType = "tv"
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (t *TMDB) SearchPeople(
|
||||
o SearchUniversalOptions,
|
||||
) (TMDBSearchPeopleResponse, error) {
|
||||
resp := new(TMDBSearchPeopleResponse)
|
||||
if !o.Valid() {
|
||||
return *resp, errors.New("request is invalid")
|
||||
}
|
||||
cacheKey := cache.CreateCacheKey(
|
||||
"SearchPeople",
|
||||
o.Query,
|
||||
o.Page,
|
||||
o.Adult)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("SearchPeople: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := t.Request(
|
||||
"/search/person",
|
||||
o.AsParamsMap(),
|
||||
&resp)
|
||||
if err != nil {
|
||||
slog.Error("SearchPeople: Request failed!", "error", err)
|
||||
return TMDBSearchPeopleResponse{}, errors.New("request failed")
|
||||
}
|
||||
for i := range resp.Results {
|
||||
resp.Results[i].MediaType = "person"
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
// Search for content by an external id (imdb, etc).
|
||||
// Defaults to imdb if no source if provided (probably most common).
|
||||
func (t *TMDB) SearchByExternalId(
|
||||
id string,
|
||||
source string,
|
||||
) (TMDBSearchMultiResponse, error) {
|
||||
resp := new(TMDBFindByExternalIdResponse)
|
||||
if source == "" {
|
||||
source = "imdb"
|
||||
}
|
||||
cacheKey := cache.CreateCacheKey("SearchByExternalId", id, source)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("SearchByExternalId: Got cache.")
|
||||
} else {
|
||||
// If not found in cache, request data from tmdb.
|
||||
err := t.Request(
|
||||
"/find/"+id,
|
||||
map[string]string{"external_source": source + "_id"},
|
||||
&resp)
|
||||
if err != nil {
|
||||
slog.Error("Failed to complete find/external_id request!",
|
||||
"error", err.Error())
|
||||
return TMDBSearchMultiResponse{},
|
||||
errors.New("failed to complete find/external_id request")
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
}
|
||||
comb := []TMDBSearchMultiResult{}
|
||||
comb = append(comb, resp.MovieResults...)
|
||||
comb = append(comb, resp.TvResults...)
|
||||
comb = append(comb, resp.PersonResults...)
|
||||
comb = append(comb, resp.TvSeasonResults...)
|
||||
comb = append(comb, resp.TvEpisodeResults...)
|
||||
return TMDBSearchMultiResponse{
|
||||
TMDBSearchResponse: TMDBSearchResponse[TMDBSearchMultiResult]{
|
||||
Results: comb,
|
||||
TMDBPageFields: TMDBPageFields{
|
||||
TotalResults: len(comb),
|
||||
// Just providing these so we don't break frontend pagination logic.
|
||||
TotalPages: 1,
|
||||
Page: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
nil
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package tmdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/sbondCo/Watcharr/cache"
|
||||
)
|
||||
|
||||
func (t *TMDB) ShowDetails(
|
||||
id string,
|
||||
country string,
|
||||
rParams map[string]string,
|
||||
) (TMDBShowDetails, error) {
|
||||
cacheKey := cache.CreateCacheKey("ShowDetails", id, country, rParams)
|
||||
resp := new(TMDBShowDetails)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("ShowDetails: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := t.Request("/tv/"+id, rParams, &resp)
|
||||
if err != nil {
|
||||
slog.Error("ShowDetails: Request failed!", "error", err)
|
||||
return TMDBShowDetails{}, errors.New("request failed")
|
||||
}
|
||||
resp.WatchProvidersTransformed = transformProviders(&resp.WatchProviders, country)
|
||||
resp.WatchProviders = nil // We don't want this to linger around (in cache) since we have the transformed version now..
|
||||
go t.contentProvider.CacheContentTv(*resp, true)
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (t *TMDB) ShowCredits(id string) (TMDBContentCredits, error) {
|
||||
resp := new(TMDBContentCredits)
|
||||
err := t.Request("/tv/"+id+"/credits", map[string]string{}, &resp)
|
||||
if err != nil {
|
||||
slog.Error("ShowCredits: Request failed!", "error", err)
|
||||
return TMDBContentCredits{}, errors.New("request failed")
|
||||
}
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (t *TMDB) SeasonDetails(
|
||||
showId string,
|
||||
seasonNumber string,
|
||||
) (TMDBSeasonDetails, error) {
|
||||
cacheKey := cache.CreateCacheKey("SeasonDetails", showId, seasonNumber)
|
||||
resp := new(TMDBSeasonDetails)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("SeasonDetails: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := t.Request(
|
||||
"/tv/"+showId+"/season/"+seasonNumber,
|
||||
map[string]string{},
|
||||
&resp)
|
||||
if err != nil {
|
||||
slog.Error("SeasonDetails: Request failed!", "error", err)
|
||||
return TMDBSeasonDetails{}, errors.New("request failed")
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package tmdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/sbondCo/Watcharr/cache"
|
||||
)
|
||||
|
||||
func (t *TMDB) Trending(
|
||||
ttype TrendingType,
|
||||
pageNum int,
|
||||
region string,
|
||||
) (TMDBTrendingCombined, error) {
|
||||
resp := new(TMDBTrendingCombined)
|
||||
if ttype != TrendingTypeAll &&
|
||||
ttype != TrendingTypeMovie &&
|
||||
ttype != TrendingTypeShow &&
|
||||
ttype != TrendingTypePerson {
|
||||
slog.Error("Trending: Invalid type provided", "provided_t", t)
|
||||
return *resp, errors.New("invalid type")
|
||||
}
|
||||
if pageNum <= 0 {
|
||||
pageNum = 1
|
||||
}
|
||||
cacheKey := cache.CreateCacheKey(
|
||||
"Trending",
|
||||
string(ttype),
|
||||
region,
|
||||
pageNum)
|
||||
if cache.GetCache(ContentStore, cacheKey, &resp) {
|
||||
slog.Debug("Trending: Returning cache.")
|
||||
return *resp, nil
|
||||
}
|
||||
err := t.Request(
|
||||
"/trending/"+string(ttype)+"/day",
|
||||
map[string]string{
|
||||
"page": strconv.Itoa(pageNum),
|
||||
"region": region,
|
||||
},
|
||||
&resp)
|
||||
if err != nil {
|
||||
slog.Error("Trending: Request failed!", "error", err)
|
||||
return *resp, errors.New("request failed")
|
||||
}
|
||||
ContentStore.Set(cacheKey, resp, time.Hour*24)
|
||||
return *resp, nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package tmdb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
func (t *TMDB) Regions() (TMDBRegions, error) {
|
||||
resp := new(TMDBRegions)
|
||||
err := t.Request(
|
||||
"/watch/providers/regions",
|
||||
map[string]string{},
|
||||
&resp)
|
||||
if err != nil {
|
||||
slog.Error("Regions: Request failed", "error", err)
|
||||
return TMDBRegions{}, errors.New("request failed")
|
||||
}
|
||||
return *resp, nil
|
||||
}
|
||||
+9
-8
@@ -196,12 +196,13 @@ func main() {
|
||||
api := gine.Group("/api")
|
||||
br := router.NewBaseRouter(db, api, cfg)
|
||||
|
||||
t := tmdb.NewTMDB(cfg.TMDB_KEY)
|
||||
tmdbService := tmdb.NewTMDB(cfg.TMDB_KEY)
|
||||
|
||||
contentService := content.NewService(db, tmdbService)
|
||||
tmdbService.AddContentProvider(contentService)
|
||||
plexService := plex.NewService(cfg)
|
||||
authService := auth.NewService(db, cfg, plexService)
|
||||
authTrustedHeaderService := auth.NewTrustedHeaderService(db, cfg, authService)
|
||||
contentService := content.NewService(db, t)
|
||||
activityService := activity.NewService(db)
|
||||
userService := user.NewService(db)
|
||||
userManageService := user.NewManageService(db)
|
||||
@@ -217,7 +218,7 @@ func main() {
|
||||
db,
|
||||
watchedService,
|
||||
watchedSeasonService,
|
||||
contentService,
|
||||
tmdbService,
|
||||
activityService,
|
||||
userService)
|
||||
jellyfinService := jellyfin.NewService(cfg)
|
||||
@@ -238,22 +239,22 @@ func main() {
|
||||
profileService := profile.NewService(db)
|
||||
followService := follow.NewService(db)
|
||||
tagService := tag.NewService(db, watchedService)
|
||||
searchService := search.NewService(db, br.Cfg, contentService, watchedService)
|
||||
discoverService := discover.NewService(db, br.Cfg, contentService)
|
||||
searchService := search.NewService(db, br.Cfg, tmdbService, watchedService)
|
||||
discoverService := discover.NewService(db, br.Cfg, tmdbService)
|
||||
importService := imprt.NewService(
|
||||
db,
|
||||
watchedService,
|
||||
watchedSeasonService,
|
||||
watchedEpisodeService,
|
||||
contentService,
|
||||
tmdbService,
|
||||
activityService,
|
||||
tagService,
|
||||
searchService)
|
||||
importTraktService := imprt.NewTraktService(importService)
|
||||
|
||||
auth.NewRouter(br, authService, authTrustedHeaderService).AddRoutes()
|
||||
content.NewRouter(br, contentService, watchedService).AddRoutes()
|
||||
watched.NewRouter(br, t, watchedService).AddRoutes()
|
||||
content.NewRouter(br, contentService, watchedService, tmdbService).AddRoutes()
|
||||
watched.NewRouter(br, watchedService).AddRoutes()
|
||||
season.NewRouter(br, watchedSeasonService).AddRoutes()
|
||||
episode.NewRouter(br, watchedEpisodeService).AddRoutes()
|
||||
activity.NewRouter(br, activityService).AddRoutes()
|
||||
|
||||
Reference in New Issue
Block a user