diff --git a/internal/movies/api_fanart.go b/internal/movies/api_fanart.go new file mode 100644 index 0000000..a8397ac --- /dev/null +++ b/internal/movies/api_fanart.go @@ -0,0 +1,107 @@ +// internal/movies/api_trakt.go +package movies + +import ( + "encoding/json" + "fmt" + "hugo-medialog/utils" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strconv" +) + +// FanartResponse Struct +type FanartResponse struct { + MoviePosters []FanartImage `json:"movieposter"` + MovieBackgrounds []FanartImage `json:"moviebackground"` + MovieLogos []FanartImage `json:"movielogo"` +} + +// FanartImage struct +type FanartImage struct { + URL string `json:"url"` + Likes string `json:"likes"` +} + +func FetchImagesFromFanart(ID int) (posterURL, backgroundURL string, logoURL string, err error) { + fanartAPIKey := os.Getenv("FANART_API_KEY") + url := fmt.Sprintf("https://webservice.fanart.tv/v3/movies/%s?api_key=%s", strconv.Itoa(ID), fanartAPIKey) + + resp, err := http.Get(url) + if err != nil { + return "", "", "", fmt.Errorf("error fetching data from fanart.tv: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", "", "", fmt.Errorf("fanart.tv returned non-200 status: %d", resp.StatusCode) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", "", "", fmt.Errorf("error reading response body: %w", err) + } + + var fanartResp FanartResponse + err = json.Unmarshal(body, &fanartResp) + if err != nil { + return "", "", "", fmt.Errorf("error unmarshalling response: %w", err) + } + + // Get the most voted poster + if len(fanartResp.MoviePosters) > 0 { + posterURL = fanartResp.MoviePosters[0].URL + } + + // Get the most voted background + if len(fanartResp.MovieBackgrounds) > 0 { + backgroundURL = fanartResp.MovieBackgrounds[0].URL + } + + // Get the most voted logo + if len(fanartResp.MovieLogos) > 0 { + logoURL = fanartResp.MovieLogos[0].URL + } + + return posterURL, backgroundURL, logoURL, nil +} + +func DownloadImage(url, slug, imageType string) error { + imageDir := os.Getenv("IMAGES_OUTPUT_DIR") + if err := utils.CreateDirIfNotExists(imageDir); err != nil { + return err + } + filename := fmt.Sprintf("%s-%s.jpg", slug, imageType) + filePath := filepath.Join(imageDir, filename) + + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("error downloading image: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("non-200 response while downloading image: %d", resp.StatusCode) + } + + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("error creating image file: %w", err) + } + defer file.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error reading image data: %w", err) + } + + _, err = file.Write(body) + if err != nil { + return fmt.Errorf("error writing image data: %w", err) + } + + fmt.Printf(" - Image saved successfully at: %s\n", filePath) + return nil +} diff --git a/internal/movies/api_trakt.go b/internal/movies/api_trakt.go new file mode 100644 index 0000000..3c18fb1 --- /dev/null +++ b/internal/movies/api_trakt.go @@ -0,0 +1,198 @@ +// internal/movies/api_trakt.go +package movies + +import ( + "context" + "encoding/json" + "fmt" + "hugo-medialog/utils" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "strconv" + + "golang.org/x/oauth2" +) + +var ( + traktAPIBaseURL = "https://api.trakt.tv" + traktClientID string + traktClientSecret string + traktRedirectURI string +) + +var traktOAuthConfig = &oauth2.Config{ + ClientID: os.Getenv("TRAKT_CLIENT_ID"), + ClientSecret: os.Getenv("TRAKT_CLIENT_SECRET"), + Endpoint: oauth2.Endpoint{ + AuthURL: "https://api.trakt.tv/oauth/authorize", + TokenURL: "https://api.trakt.tv/oauth/token", + }, + RedirectURL: os.Getenv("TRAKT_REDIRECT_URI"), +} + +func init() { + // Load runtime variables + utils.LoadConfig() + traktClientID = os.Getenv("TRAKT_CLIENT_ID") + traktClientSecret = os.Getenv("TRAKT_CLIENT_SECRET") + traktRedirectURI = os.Getenv("TRAKT_REDIRECT_URI") + + if traktClientID == "" || traktClientSecret == "" || traktRedirectURI == "" { + log.Fatal("Missing Trakt API credentials in environment variables") + } +} + +// Function to fetch a token and save it for reuse +func GetTraktToken() (*oauth2.Token, error) { + // Check if the token already exists (from a previous session) + tokenFile := "trakt_token.json" + if _, err := os.Stat(tokenFile); err == nil { + // If token file exists, read and return it + file, err := os.Open(tokenFile) + if err != nil { + return nil, err + } + defer file.Close() + token := &oauth2.Token{} + if err = json.NewDecoder(file).Decode(token); err != nil { + return nil, err + } + return token, nil + } + + // If no token exists, we need to go through the OAuth flow + authURL := traktOAuthConfig.AuthCodeURL("state-token", oauth2.AccessTypeOffline) + fmt.Printf("Visit the URL to authorize: %v\n", authURL) + + // After user authorization, you will receive an authorization code. + // Once you have the code, get a token by calling the token endpoint. + var authCode string + fmt.Print("Enter authorization code: ") + fmt.Scan(&authCode) + + token, err := traktOAuthConfig.Exchange(context.Background(), authCode) + if err != nil { + return nil, err + } + + // Save the token to file for future use + file, err := os.Create(tokenFile) + if err != nil { + return nil, err + } + defer file.Close() + + if err := json.NewEncoder(file).Encode(token); err != nil { + return nil, err + } + + return token, nil +} + +// Function to get movie details by Trakt ID +func GetMovieByID(traktID int) (*Movie, error) { + token, err := GetTraktToken() + if err != nil { + return nil, err + } + + client := traktOAuthConfig.Client(context.Background(), token) + + // Call the Trakt API to get movie details by ID + apiURL := fmt.Sprintf("%s/movies/%s", traktAPIBaseURL, strconv.Itoa(traktID)) + req, _ := http.NewRequest("GET", apiURL, nil) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token.AccessToken) + req.Header.Set("trakt-api-version", "2") + req.Header.Set("trakt-api-key", traktClientID) + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to fetch movie: %v", resp.Status) + } + + body, _ := ioutil.ReadAll(resp.Body) + var traktMovie Movie + if err := json.Unmarshal(body, &traktMovie); err != nil { + return nil, err + } + + return &traktMovie, nil +} + +// Function to search for a movie by title +func SearchMovieByTitle(title string, movie *Movie) error { + token, err := GetTraktToken() + if err != nil { + return err + } + + client := traktOAuthConfig.Client(context.Background(), token) + + // URL encode the title + query := url.QueryEscape(title) + apiURL := fmt.Sprintf("%s/search/movie?query=%s", traktAPIBaseURL, query) + req, _ := http.NewRequest("GET", apiURL, nil) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token.AccessToken) + req.Header.Set("trakt-api-version", "2") + req.Header.Set("trakt-api-key", traktClientID) + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to search movie: %v", resp.Status) + } + + body, _ := ioutil.ReadAll(resp.Body) + + // utils.DebugBody(body) + + // Parse trakt.tv response to a struct + var traktResponse []struct { + Movie struct { + Title string `json:"title"` + Year int `json:"year"` + IDs struct { + Slug string `json:"slug"` + IMDB string `json:"imdb"` + TMDB int `json:"tmdb"` + Trakt int `json:"trakt"` + } `json:"ids"` + } `json:"movie"` + } + + if err := json.Unmarshal(body, &traktResponse); err != nil { + return err + } + + if len(traktResponse) == 0 { + return fmt.Errorf("no movie found with title %s", title) + } + + // Using 1st result to fill our Movie structure + foundMovie := traktResponse[0].Movie + + movie.Title = foundMovie.Title + movie.IDs.Slug = foundMovie.IDs.Slug + movie.IDs.IMDB = foundMovie.IDs.IMDB + movie.IDs.TMDB = foundMovie.IDs.TMDB + movie.IDs.Trakt = foundMovie.IDs.Trakt + movie.Subtitle = fmt.Sprintf("%d", foundMovie.Year) + movie.Year = foundMovie.Year + movie.Link = fmt.Sprintf("https://trakt.tv/movies/%d", foundMovie.IDs.Trakt) + + return nil +}