diff --git a/internal/books/api_googlebooks.go b/internal/books/api_googlebooks.go new file mode 100644 index 0000000..f47ff85 --- /dev/null +++ b/internal/books/api_googlebooks.go @@ -0,0 +1,188 @@ +package books + +import ( + "encoding/json" + "fmt" + "hugo-medialog/utils" + "io/ioutil" + "net/http" + "net/url" + "os" + "path/filepath" + "strconv" +) + +const googleBooksAPIBaseURL = "https://www.googleapis.com/books/v1/volumes?q=intitle:%s&key=%s" + +// Function to search for a book by title +func SearchBookByTitle(title string, book *Book) error { + apiKey := os.Getenv("GOOGLE_BOOKS_API_KEY") + url := fmt.Sprintf(googleBooksAPIBaseURL, url.QueryEscape(title), apiKey) + + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + var result map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return err + } + + items := result["items"].([]interface{}) + if len(items) == 0 { + return fmt.Errorf("Book %s not found", title) + } + + // Get the ISBN from any of the results + isbn, err := extractISBNFromResults(items) + if err != nil { + fmt.Printf("ISBN not found\n", isbn) + } else { + book.ISBN = isbn + } + + bookInfo := items[0].(map[string]interface{})["volumeInfo"].(map[string]interface{}) + + book.ID = items[0].(map[string]interface{})["id"].(string) + book.Title = bookInfo["title"].(string) + book.Author = bookInfo["authors"].([]interface{})[0].(string) + book.Link = bookInfo["infoLink"].(string) + if pageCount, ok := bookInfo["pageCount"].(float64); ok { + book.Pages = int(pageCount) + } + if publishedDate, ok := bookInfo["publishedDate"].(string); ok { + if len(publishedDate) >= 4 { + book.Year, _ = strconv.Atoi(publishedDate[:4]) + } + } + + book.Image = getBookCoverByTitle(title) + if err != nil { + fmt.Printf("Image not found: %s\n", err) + } + + return nil +} + +func extractISBNFromResults(items []interface{}) (string, error) { + // Loop over all the results and returns first ISBN found (ISBN_13 or + // ISBN_10) + for _, item := range items { + bookInfo := item.(map[string]interface{})["volumeInfo"].(map[string]interface{}) + industryIdentifiers, ok := bookInfo["industryIdentifiers"].([]interface{}) + if !ok { + continue + } + + var isbn string + for _, identifier := range industryIdentifiers { + id := identifier.(map[string]interface{}) + // Primero intentamos con ISBN_13 + if id["type"] == "ISBN_13" { + isbn = id["identifier"].(string) + return isbn, nil + } else if id["type"] == "ISBN_10" && isbn == "" { + // Si no hay ISBN_13, intentamos con ISBN_10 + isbn = id["identifier"].(string) + return isbn, nil + } + } + } + + return "", fmt.Errorf("no ISBN found in any result") +} + +func getBookCoverByISBN(isbn string) string { + openLibraryURL := fmt.Sprintf("https://covers.openlibrary.org/b/isbn/%s-L.jpg", isbn) + if checkImageExists(openLibraryURL) { + return openLibraryURL + } + + amazonURL := fmt.Sprintf("https://images-na.ssl-images-amazon.com/images/P/%s.jpg", isbn) + if checkImageExists(amazonURL) { + return amazonURL + } + + return "" +} + +func getBookCoverByTitle(title string) string { + apiKey := os.Getenv("GOOGLE_CUSTOM_SEARCH_ENGINE_API_KEY") + cx := os.Getenv("GOOGLE_CX") + + searchURL := fmt.Sprintf("https://www.googleapis.com/customsearch/v1?q=%s+book+cover&key=%s&searchType=image&num=1&cx=%s", url.QueryEscape(title), apiKey, cx) + + resp, err := http.Get(searchURL) + if err != nil { + fmt.Printf("Error fetching cover from Google Images API: %s\n", err) + return "default-cover.jpg" + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + fmt.Printf("Error fetching cover, status code: %d\n", resp.StatusCode) + return "default-cover.jpg" + } + + var result map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + fmt.Printf("Error decoding JSON response: %s\n", err) + return "default-cover.jpg" + } + + if items, ok := result["items"].([]interface{}); ok && len(items) > 0 { + if link, ok := items[0].(map[string]interface{})["link"].(string); ok { + return link + } + } + + return "default-cover.jpg" +} + +func checkImageExists(imageURL string) bool { + resp, err := http.Head(imageURL) + if err != nil || resp.StatusCode != http.StatusOK { + return false + } + return true +} + +func DownloadImage(url, slug string) error { + imageDir := filepath.Join(os.Getenv("MARKDOWN_OUTPUT_BOOKS_DIR"), os.Getenv("IMAGES_OUTPUT_DIR")) + if err := utils.CreateDirIfNotExists(imageDir); err != nil { + return err + } + filename := fmt.Sprintf("%s.jpg", slug) + 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/books/controller.go b/internal/books/controller.go index 8b13789..8c3f109 100644 --- a/internal/books/controller.go +++ b/internal/books/controller.go @@ -1 +1,86 @@ +package books +import ( + "fmt" + "hugo-medialog/utils" + "os" + "path/filepath" + + "gopkg.in/yaml.v2" +) + +func LoadBooks() ([]Book, error) { + booksFile := os.Getenv("OBSIDIAN_BOOKS_FILE") + fileData, err := os.ReadFile(booksFile) + if err != nil { + return nil, err + } + + var books []Book + err = yaml.Unmarshal(fileData, &books) + if err != nil { + return nil, err + } + + return books, nil +} + +func ProcessBooks(books []Book) error { + utils.Sep() + for _, book := range books { + fmt.Printf("Title: %s, Author: %s, ID: %s\n", book.Title, book.Author, book.ID) + + // If we dont have ID, search movie by Title and get the ID + if book.ID == "" { + err := SearchBookByTitle(book.Title, &book) + if err != nil { + fmt.Printf("Error searching book by title %s: %s\n", book.Title, err) + continue + } + } + book.Slug = utils.Sluggify(book.Title) + + // Now we need to get the image + imageURL := getBookCoverByTitle(book.Title) + if imageURL != "" { + err := DownloadImage(imageURL, book.Slug) + if err != nil { + fmt.Printf("Error downloading %s: %s\n", imageURL, err) + } + } + book.Image = fmt.Sprintf("%s.jpg", book.Slug) + + err := generateBookMarkdown(book) + if err != nil { + return err + } + + utils.Sep() + } + return nil +} + +func generateBookMarkdown(book Book) error { + templatePath := filepath.Join(os.Getenv("TEMPLATES_DIR"), "book.md.tpl") + outputDir := os.Getenv("MARKDOWN_OUTPUT_BOOKS_DIR") + if err := utils.CreateDirIfNotExists(outputDir); err != nil { + return err + } + outputPath := filepath.Join(outputDir, fmt.Sprintf("%s.md", book.Slug)) + + data := map[string]interface{}{ + "Title": book.Title, + "Author": book.Author, + "Link": book.Link, + "Subtitle": book.Year, + "Year": book.Year, + "Rate": book.Rate, + "Pages": book.Pages, + "Progress": book.Progress, + "Image": book.Image, + "Date": book.Date, + "Tags": "reading", + } + + return utils.GenerateMarkdown(templatePath, outputPath, data) +} diff --git a/internal/books/model.go b/internal/books/model.go index 7e50d4a..c9433d9 100644 --- a/internal/books/model.go +++ b/internal/books/model.go @@ -1,17 +1,19 @@ package books type Book struct { - Title string `yaml:"title"` - Author string `yaml:"author"` - Link string `yaml:"link"` - Year int `yaml:"year"` - Rate float64 `yaml:"rate"` - Progress string `yaml:"progress"` - Image string `yaml:"image"` - Poster string `yaml:"poster-image"` - Background string `yaml:"background-image"` - Date string `yaml:"date"` - Tags []string + Title string `yaml:"title"` + Slug string `yaml:"slug"` + Author string `yaml:"author"` + Link string `yaml:"link"` + Year int `yaml:"year"` + ID string `yaml:"id"` + ISBN string `yaml:"isbn"` + Rate float64 `yaml:"rate"` + Pages int `yaml:"pages"` + Progress string `yaml:"progress"` + Image string `yaml:"image"` + Date string `yaml:"date"` + Tags []string } type Books []Book diff --git a/main.go b/main.go index d3a9eed..813ce86 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "hugo-medialog/internal/books" "hugo-medialog/internal/movies" "hugo-medialog/internal/series" "hugo-medialog/utils" @@ -33,4 +34,15 @@ func main() { fmt.Printf("Error processing series: %v\n", err) } + // Books + booksList, err := books.LoadBooks() + if err != nil { + fmt.Printf("Error reading books file: %v\n", err) + return + } + err = books.ProcessBooks(booksList) + if err != nil { + fmt.Printf("Error processing books: %v\n", err) + } + } diff --git a/templates/book.md.tpl b/templates/book.md.tpl index 50a5c55..90de81a 100644 --- a/templates/book.md.tpl +++ b/templates/book.md.tpl @@ -5,10 +5,9 @@ subtitle: "{{ .Subtitle }}" author: "{{ .Author }}" year: {{ .Year }} rate: {{ .Rate }} +pages: {{ .Pages }} progress: {{ .Progress }} image: {{ .Image }} -poster-image: {{ .PosterImage }} -background-image: {{ .BackgroundImage }} date: {{ .Date }} draft: false tags: {{ .Tags }}