diff --git a/internal/series/api_fanart.go b/internal/series/api_fanart.go new file mode 100644 index 0000000..1dc403a --- /dev/null +++ b/internal/series/api_fanart.go @@ -0,0 +1,107 @@ +// internal/series/api_trakt.go +package series + +import ( + "encoding/json" + "fmt" + "hugo-medialog/utils" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strconv" +) + +// FanartResponse Struct +type FanartResponse struct { + SeriePosters []FanartImage `json:"tvposter"` + SerieBackgrounds []FanartImage `json:"showbackground"` + SerieLogos []FanartImage `json:"hdtvlogo"` +} + +// 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/tv/%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.SeriePosters) > 0 { + posterURL = fanartResp.SeriePosters[0].URL + } + + // Get the most voted background + if len(fanartResp.SerieBackgrounds) > 0 { + backgroundURL = fanartResp.SerieBackgrounds[0].URL + } + + // Get the most voted logo + if len(fanartResp.SerieLogos) > 0 { + logoURL = fanartResp.SerieLogos[0].URL + } + + return posterURL, backgroundURL, logoURL, nil +} + +func DownloadImage(url, slug, imageType string) error { + imageDir := filepath.Join(os.Getenv("MARKDOWN_OUTPUT_SERIES_DIR"), 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/series/api_trakt.go b/internal/series/api_trakt.go new file mode 100644 index 0000000..f4aca2d --- /dev/null +++ b/internal/series/api_trakt.go @@ -0,0 +1,200 @@ +// internal/series/api_trakt.go +package series + +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 serie details by Trakt ID +func GetSerieByID(traktID int) (*Serie, error) { + token, err := GetTraktToken() + if err != nil { + return nil, err + } + + client := traktOAuthConfig.Client(context.Background(), token) + + // Call the Trakt API to get serie details by ID + apiURL := fmt.Sprintf("%s/shows/%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 serie: %v", resp.Status) + } + + body, _ := ioutil.ReadAll(resp.Body) + var traktSerie Serie + if err := json.Unmarshal(body, &traktSerie); err != nil { + return nil, err + } + + return &traktSerie, nil +} + +// Function to search for a serie by title +func SearchSerieByTitle(title string, serie *Serie) 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/show?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 serie: %v", resp.Status) + } + + body, _ := ioutil.ReadAll(resp.Body) + + // utils.DebugBody(body) + + // Parse trakt.tv response to a struct + var traktResponse []struct { + Serie 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"` + TVDB int `json:"tvdb"` + } `json:"ids"` + } `json:"show"` + } + + if err := json.Unmarshal(body, &traktResponse); err != nil { + return err + } + + if len(traktResponse) == 0 { + return fmt.Errorf("no serie found with title %s", title) + } + + // Using 1st result to fill our Serie structure + foundSerie := traktResponse[0].Serie + + serie.Title = foundSerie.Title + serie.IDs.Slug = foundSerie.IDs.Slug + serie.IDs.IMDB = foundSerie.IDs.IMDB + serie.IDs.TMDB = foundSerie.IDs.TMDB + serie.IDs.Trakt = foundSerie.IDs.Trakt + serie.IDs.TVDB = foundSerie.IDs.TVDB + serie.Subtitle = fmt.Sprintf("%d", foundSerie.Year) + serie.Year = foundSerie.Year + serie.Link = fmt.Sprintf("https://trakt.tv/shows/%d", foundSerie.IDs.Trakt) + + return nil +} diff --git a/internal/series/controller.go b/internal/series/controller.go index f4e1967..185d854 100644 --- a/internal/series/controller.go +++ b/internal/series/controller.go @@ -1,2 +1,118 @@ // internal/series/controller.go +package series +import ( + "fmt" + "hugo-medialog/utils" + "os" + "path/filepath" + + "gopkg.in/yaml.v3" +) + +func LoadSeries() ([]Serie, error) { + seriesFile := os.Getenv("OBSIDIAN_SERIES_FILE") + fileData, err := os.ReadFile(seriesFile) + if err != nil { + return nil, err + } + + var series []Serie + err = yaml.Unmarshal(fileData, &series) + if err != nil { + return nil, err + } + + return series, nil +} + +func ProcessSeries(series []Serie) error { + utils.Sep() + for _, serie := range series { + // Debug print + fmt.Printf("Título: %s, Puntuación: %.1f, Fecha: %s\n", + serie.Title, serie.Rate, serie.Date) + + // If we dont have IDs, search serie by Title and get the IDs + if serie.IDs.Trakt == 0 { + err := SearchSerieByTitle(serie.Title, &serie) + if err != nil { + fmt.Printf("Error searching serie by title %s: %s\n", serie.Title, err) + continue + } + } + + // Now we need to get the images from fanart + posterURL, backgroundURL, logoURL, err := FetchImagesFromFanart(serie.IDs.TVDB) + if err != nil { + fmt.Printf("Error fetching images from Fanart.tv for %s: %s\n", serie.Title, err) + continue + } + + imageTypes := []struct { + URL string + ImageType string + SetField func(imagePath string) + }{ + { + URL: posterURL, + ImageType: "poster", + SetField: func(imagePath string) { serie.Poster = imagePath }, + }, + { + URL: backgroundURL, + ImageType: "background", + SetField: func(imagePath string) { serie.Background = imagePath }, + }, + { + URL: logoURL, + ImageType: "logo", + SetField: func(imagePath string) { serie.Image = imagePath }, + }, + } + + for _, image := range imageTypes { + if image.URL != "" { + err := DownloadImage(image.URL, serie.IDs.Slug, image.ImageType) + if err != nil { + fmt.Printf("Error downloading %s for %s: %s\n", image.ImageType, serie.Title, err) + } else { + image.SetField(fmt.Sprintf("%s-%s.jpg", serie.IDs.Slug, image.ImageType)) + } + } + } + // utils.Debug(serie) + + err = generateSerieMarkdown(serie) + if err != nil { + return err + } + utils.Sep() + } + return nil +} + +func generateSerieMarkdown(serie Serie) error { + templatePath := filepath.Join(os.Getenv("TEMPLATES_DIR"), "serie.md.tpl") + outputDir := os.Getenv("MARKDOWN_OUTPUT_SERIES_DIR") + if err := utils.CreateDirIfNotExists(outputDir); err != nil { + return err + } + outputPath := filepath.Join(outputDir, fmt.Sprintf("%s.md", serie.IDs.Slug)) + + data := map[string]interface{}{ + "Title": serie.Title, + "Link": serie.Link, + "Subtitle": serie.Year, + "Year": serie.Year, + "Rate": serie.Rate, + "Progress": serie.Progress, + "Image": serie.Image, + "Poster": serie.Poster, + "Background": serie.Background, + "Date": serie.Date, + "Tags": "watching", + } + + return utils.GenerateMarkdown(templatePath, outputPath, data) +} diff --git a/internal/series/model.go b/internal/series/model.go index 1036fbd..fd09efb 100644 --- a/internal/series/model.go +++ b/internal/series/model.go @@ -2,9 +2,16 @@ package series type Serie struct { - Title string `yaml:"title"` - Link string `yaml:"link"` + Title string `yaml:"title"` + IDs struct { + Slug string `json:"slug"` + IMDB string `json:"imdb"` + TMDB int `json:"tmdb"` + Trakt int `json:"trakt"` + TVDB int `json:"tvdb"` + } `json:"ids"` Subtitle string `yaml:"subtitle"` + Link string `yaml:"link"` Year int `yaml:"year"` Rate float64 `yaml:"rate"` Progress string `yaml:"progress"` diff --git a/main.go b/main.go index a4e3159..d3a9eed 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "hugo-medialog/internal/movies" + "hugo-medialog/internal/series" "hugo-medialog/utils" ) @@ -21,4 +22,15 @@ func main() { fmt.Printf("Error processing movies: %v\n", err) } + // Series + seriesList, err := series.LoadSeries() + if err != nil { + fmt.Printf("Error reading series file: %v\n", err) + return + } + err = series.ProcessSeries(seriesList) + if err != nil { + fmt.Printf("Error processing series: %v\n", err) + } + } diff --git a/templates/serie.md.tpl b/templates/serie.md.tpl index 18514cd..5ba5d31 100644 --- a/templates/serie.md.tpl +++ b/templates/serie.md.tpl @@ -6,8 +6,8 @@ year: {{ .Year }} rate: {{ .Rate }} progress: {{ .Progress }} image: {{ .Image }} -poster-image: {{ .PosterImage }} -background-image: {{ .BackgroundImage }} +poster-image: {{ .Poster }} +background-image: {{ .Background }} date: {{ .Date }} draft: false tags: {{ .Tags }}