mirror of
https://github.com/sbondCo/Watcharr.git
synced 2026-08-07 07:14:44 +00:00
705 lines
22 KiB
Go
705 lines
22 KiB
Go
package content
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"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"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
// inmemory content cache
|
|
var ContentStore = gocache.New(time.Hour*24, time.Minute)
|
|
|
|
// Download file over http (used for downloading poster images)
|
|
// url - The remote file url.
|
|
// outf - Where should we store the downloaded file.
|
|
// force - Should we overwrite an existing file? If false, existing files will be skipped.
|
|
func download(url string, outf string, force bool) (err error) {
|
|
slog.Debug("download: Attempting to download file", "url", url, "outf", outf, "force", force)
|
|
// If not forced, skip call if file already exists to save unnecessary requests.
|
|
if !force {
|
|
if _, err := os.Stat(outf); !errors.Is(err, os.ErrNotExist) {
|
|
slog.Debug("download: Skipping file, it already exists locally.", "outf", outf, "error", err)
|
|
return nil
|
|
} else {
|
|
slog.Debug("download: Continuing to download file, it does not already exist.", "outf", outf, "error", err)
|
|
}
|
|
}
|
|
|
|
// Get the data
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
slog.Error("download: Failed to make request.", "outf", outf, "error", err)
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Check server response
|
|
if resp.StatusCode != http.StatusOK {
|
|
slog.Error("download: Request failed. Non OK response.", "outf", outf, "status", resp.Status, "error", err)
|
|
return fmt.Errorf("bad status: %s", resp.Status)
|
|
}
|
|
|
|
// Create the file
|
|
out, err := os.Create(outf)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
slog.Warn("download: Failed to create out file, trying to recover by ensuring directories exist.", "outf", outf)
|
|
err = os.MkdirAll(path.Dir(outf), 0764)
|
|
if err != nil {
|
|
slog.Error("download: Failed to create dir(s) in recovery attempt.", "outf", outf, "error", err)
|
|
return err
|
|
}
|
|
// If dirs made, try making file again
|
|
out, err = os.Create(outf)
|
|
if err != nil {
|
|
slog.Error("download: Failed to create out file again in recovery attempt.", "outf", outf, "error", err)
|
|
return err
|
|
}
|
|
slog.Info("download: recovered by creating dir(s).", "outf", outf)
|
|
} else {
|
|
slog.Error("download: Failed to create out file. No known recovery path possible.", "outf", outf, "error", err)
|
|
return err
|
|
}
|
|
}
|
|
defer out.Close()
|
|
|
|
// Write the body to file
|
|
_, err = io.Copy(out, resp.Body)
|
|
if err != nil {
|
|
slog.Error("download: Failed to write file to our file.", "outf", outf, "error", err)
|
|
return err
|
|
}
|
|
|
|
slog.Debug("download: Successfully downloaded file", "outf", outf)
|
|
return nil
|
|
}
|
|
|
|
type Service struct {
|
|
db *gorm.DB
|
|
tmdb *tmdb.TMDB
|
|
}
|
|
|
|
func NewService(db *gorm.DB, tmdb *tmdb.TMDB) *Service {
|
|
return &Service{
|
|
db,
|
|
tmdb,
|
|
}
|
|
}
|
|
|
|
// onlyUpdate - If we should only update existing row if exists, or false to create/update if not exist.
|
|
func (s *Service) saveContent(c *entity.Content, onlyUpdate bool) error {
|
|
slog.Info("Saving content to db", "id", c.TmdbID, "title", c.Title)
|
|
if c.TmdbID == 0 || c.Title == "" || c.Type == "" {
|
|
slog.Error("saveContent: content missing id, title or type!", "id", c.TmdbID, "title", c.Title, "type", c.Type)
|
|
return errors.New("content missing id or title")
|
|
}
|
|
var res *gorm.DB
|
|
if onlyUpdate {
|
|
// We only want to update an existing row, if it exists.
|
|
res = s.db.Model(&entity.Content{}).Where("type = ? AND tmdb_id = ?", c.Type, c.TmdbID).Updates(c)
|
|
if res.Error != nil {
|
|
slog.Error("saveContent: Error updating content in database", "error", res.Error.Error())
|
|
return errors.New("failed to update cached content in database")
|
|
}
|
|
} else {
|
|
// On conflict, update existing row with details incase any were updated/missing.
|
|
res = s.db.Clauses(clause.OnConflict{
|
|
Columns: []clause.Column{{Name: "tmdb_id"}, {Name: "type"}},
|
|
DoUpdates: clause.AssignmentColumns([]string{
|
|
"title",
|
|
"poster_path",
|
|
"overview",
|
|
"release_date",
|
|
"popularity",
|
|
"vote_average",
|
|
"vote_count",
|
|
"imdb_id",
|
|
"status",
|
|
"budget",
|
|
"revenue",
|
|
"runtime",
|
|
"number_of_episodes",
|
|
"number_of_seasons",
|
|
}),
|
|
}).Create(&c)
|
|
if res.Error != nil {
|
|
// Error if anything but unique contraint error
|
|
if res.Error != gorm.ErrDuplicatedKey {
|
|
slog.Error("saveContent: Error creating content in database", "error", res.Error.Error())
|
|
return errors.New("failed to cache content in database")
|
|
}
|
|
}
|
|
}
|
|
// If row created, download the image
|
|
if res.RowsAffected > 0 {
|
|
slog.Debug("saveContent: Downloading poster.")
|
|
err := download(
|
|
"https://image.tmdb.org/t/p/w500"+c.PosterPath,
|
|
path.Join(config.DataPath, "img", c.PosterPath),
|
|
false,
|
|
)
|
|
if err != nil {
|
|
slog.Error("saveContent: Failed to download content image!", "error", err.Error())
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) cacheContentTv(content tmdb.TMDBShowDetails, onlyUpdate bool) (entity.Content, error) {
|
|
slog.Debug("cacheContentTv", "content", content)
|
|
var (
|
|
releaseDate time.Time
|
|
runtime uint32
|
|
)
|
|
var dateFormat = "2006-01-02"
|
|
releaseDate, err := time.Parse(dateFormat, content.FirstAirDate)
|
|
if err != nil {
|
|
slog.Error("Failed to parse tv release date", "error", err)
|
|
}
|
|
if len(content.EpisodeRunTime) > 0 {
|
|
runtime = uint32(content.EpisodeRunTime[0])
|
|
}
|
|
|
|
c := entity.Content{
|
|
TmdbID: content.ID,
|
|
Title: content.Name,
|
|
Overview: content.Overview,
|
|
PosterPath: content.PosterPath,
|
|
Type: entity.SHOW,
|
|
ReleaseDate: &releaseDate,
|
|
Popularity: content.Popularity,
|
|
VoteAverage: content.VoteAverage,
|
|
VoteCount: content.VoteCount,
|
|
Status: content.Status,
|
|
Runtime: runtime,
|
|
NumberOfEpisodes: content.NumberOfEpisodes,
|
|
NumberOfSeasons: content.NumberOfSeasons,
|
|
}
|
|
|
|
err = s.saveContent(&c, onlyUpdate)
|
|
if err != nil {
|
|
slog.Error("cacheContentTv: Failed to save content!", "error", err)
|
|
return entity.Content{}, errors.New("failed to save content")
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
func (s *Service) cacheContentMovie(content tmdb.TMDBMovieDetails, onlyUpdate bool) (entity.Content, error) {
|
|
var (
|
|
releaseDate time.Time
|
|
)
|
|
var dateFormat = "2006-01-02"
|
|
// Get details from movie/show response and fill out needed vars
|
|
releaseDate, err := time.Parse(dateFormat, content.ReleaseDate)
|
|
if err != nil {
|
|
slog.Error("Failed to parse movie release date", "error", err)
|
|
}
|
|
|
|
c := entity.Content{
|
|
TmdbID: content.ID,
|
|
Title: content.Title,
|
|
Overview: content.Overview,
|
|
PosterPath: content.PosterPath,
|
|
Type: entity.MOVIE,
|
|
ReleaseDate: &releaseDate,
|
|
Popularity: content.Popularity,
|
|
VoteAverage: content.VoteAverage,
|
|
VoteCount: content.VoteCount,
|
|
ImdbID: content.ImdbID,
|
|
Status: content.Status,
|
|
Budget: content.Budget,
|
|
Revenue: content.Revenue,
|
|
Runtime: content.Runtime,
|
|
}
|
|
|
|
err = s.saveContent(&c, onlyUpdate)
|
|
if err != nil {
|
|
slog.Error("cacheContentMovie: Failed to save content!", "error", err)
|
|
return entity.Content{}, errors.New("failed to save content")
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// Get content from our db cache, or cache it if it doesn't exist.
|
|
func (s *Service) GetOrCacheContent(contentType entity.ContentType, tmdbId int) (entity.Content, error) {
|
|
var content entity.Content
|
|
// Look in db for content.
|
|
s.db.Where("type = ? AND tmdb_id = ?", contentType, tmdbId).Find(&content)
|
|
// Create content if not found from our db.
|
|
if content == (entity.Content{}) {
|
|
slog.Debug("Content not in db, fetching...", "type", contentType, "tmdbId", tmdbId)
|
|
|
|
resp, err := s.tmdb.APIRequest("/"+string(contentType)+"/"+strconv.Itoa(tmdbId), map[string]string{})
|
|
if err != nil {
|
|
slog.Error("GetOrCacheContent: content tmdb api request failed", "error", err)
|
|
return entity.Content{}, errors.New("failed to find requested media")
|
|
}
|
|
|
|
if contentType == "movie" {
|
|
c := new(tmdb.TMDBMovieDetails)
|
|
err := json.Unmarshal([]byte(resp), &c)
|
|
if err != nil {
|
|
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)
|
|
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")
|
|
}
|
|
} else {
|
|
c := new(tmdb.TMDBShowDetails)
|
|
err := json.Unmarshal(resp, &c)
|
|
if err != nil {
|
|
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)
|
|
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")
|
|
}
|
|
}
|
|
}
|
|
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
|
|
}
|