Added series
This commit is contained in:
parent
48fd96763d
commit
38a946f946
107
internal/series/api_fanart.go
Normal file
107
internal/series/api_fanart.go
Normal file
@ -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
|
||||||
|
}
|
200
internal/series/api_trakt.go
Normal file
200
internal/series/api_trakt.go
Normal file
@ -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
|
||||||
|
}
|
@ -1,2 +1,118 @@
|
|||||||
// internal/series/controller.go
|
// 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)
|
||||||
|
}
|
||||||
|
@ -2,9 +2,16 @@
|
|||||||
package series
|
package series
|
||||||
|
|
||||||
type Serie struct {
|
type Serie struct {
|
||||||
Title string `yaml:"title"`
|
Title string `yaml:"title"`
|
||||||
Link string `yaml:"link"`
|
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"`
|
Subtitle string `yaml:"subtitle"`
|
||||||
|
Link string `yaml:"link"`
|
||||||
Year int `yaml:"year"`
|
Year int `yaml:"year"`
|
||||||
Rate float64 `yaml:"rate"`
|
Rate float64 `yaml:"rate"`
|
||||||
Progress string `yaml:"progress"`
|
Progress string `yaml:"progress"`
|
||||||
|
12
main.go
12
main.go
@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"hugo-medialog/internal/movies"
|
"hugo-medialog/internal/movies"
|
||||||
|
"hugo-medialog/internal/series"
|
||||||
"hugo-medialog/utils"
|
"hugo-medialog/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -21,4 +22,15 @@ func main() {
|
|||||||
fmt.Printf("Error processing movies: %v\n", err)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,8 @@ year: {{ .Year }}
|
|||||||
rate: {{ .Rate }}
|
rate: {{ .Rate }}
|
||||||
progress: {{ .Progress }}
|
progress: {{ .Progress }}
|
||||||
image: {{ .Image }}
|
image: {{ .Image }}
|
||||||
poster-image: {{ .PosterImage }}
|
poster-image: {{ .Poster }}
|
||||||
background-image: {{ .BackgroundImage }}
|
background-image: {{ .Background }}
|
||||||
date: {{ .Date }}
|
date: {{ .Date }}
|
||||||
draft: false
|
draft: false
|
||||||
tags: {{ .Tags }}
|
tags: {{ .Tags }}
|
||||||
|
Loading…
Reference in New Issue
Block a user