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:
@@ -60,6 +60,7 @@ I have been slacking in this department, so there has been a lot of house cleani
|
||||
- ESLint: Migrate to flat config: I ran `npx sv add eslint` and modified the eslint.config.js it generated to line up better with our old one and work a bit better (i think) with our normal ts files. Also set `ecmaVersion` to `latest`, previously was `2020`.
|
||||
- Moved `env.d.ts` to `src` directory (which is covered by svelte-kits include) so that we can drop the custom `include` in our `tsconfig.json` (which when out of sync with the generated tsconfig by svelte-kit can lead to headaches because it's not immediately obvious, so this change will eliminate that dev time footgun).
|
||||
- Fixed lots of new (and probably old which may not have been applying with the old bad config) {ts,svelte} eslint rules, improving code quality.
|
||||
- Finally made tmdb media package function like the igdb media package, so that our direct tmdb interfacing code is neatly tucked away and isn't infesting the Content package (probs some more improvements I could make in regards to db caching, but that is for the future).
|
||||
|
||||
# [4.1.1] - 2026-07-26T02:00:00Z
|
||||
|
||||
|
||||
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