bookends

commit 3da94146cf7e50145c027e8528321d29c9b72b29

Author: Honza Pokorny <honza@redhat.com>

fixup

 cmd/cache.go | 35 ++++++++
 cmd/root.go | 2 
 cmd/yaml2org.go | 19 ++++
 go.mod | 4 
 go.sum | 8 +
 pkg/bookends/bookends.go | 170 +++++++++++++++++++++++++++++++++++++++++
 template.html | 68 ++++++++++++++++


diff --git a/cmd/cache.go b/cmd/cache.go
new file mode 100644
index 0000000000000000000000000000000000000000..437fed7bbfd135f256dd10ebf7112ee2477e1bc5
--- /dev/null
+++ b/cmd/cache.go
@@ -0,0 +1,35 @@
+// bookends
+// Copyright (C) 2021   Honza Pokorny <me@honza.ca>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+package cmd
+
+import (
+	"fmt"
+
+	"git.pokorny.ca/bookends/pkg/bookends"
+	"github.com/spf13/cobra"
+)
+
+var cacheCovers = &cobra.Command{
+	Use:   "cache",
+	Short: "",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := bookends.CacheCovers()
+		if err != nil {
+			fmt.Println("ERROR", err)
+		}
+	},
+}




diff --git a/cmd/root.go b/cmd/root.go
index 681008fc36febe63f6047ab7795ce3527ac523fb..0c75a15deaea89e83a2d0b36fbb1b68a7aabad70 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -44,5 +44,7 @@ 	// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file path (default is config.yaml)")
 	// rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "")
 	// rootCmd.AddCommand(generateDefaultConfigurationCmd)
 	rootCmd.AddCommand(buildCmd)
+	rootCmd.AddCommand(yaml2OrgCmd)
+	rootCmd.AddCommand(cacheCovers)
 	// rootCmd.AddCommand(versionCmd)
 }




diff --git a/cmd/yaml2org.go b/cmd/yaml2org.go
new file mode 100644
index 0000000000000000000000000000000000000000..396bbda60ef64c142a3eca3b963d8d399930f0ca
--- /dev/null
+++ b/cmd/yaml2org.go
@@ -0,0 +1,19 @@
+package cmd
+
+import (
+	"fmt"
+
+	"git.pokorny.ca/bookends/pkg/bookends"
+	"github.com/spf13/cobra"
+)
+
+var yaml2OrgCmd = &cobra.Command{
+	Use:   "yaml2org",
+	Short: "",
+	Run: func(cmd *cobra.Command, args []string) {
+		err := bookends.Yaml2Org()
+		if err != nil {
+			fmt.Println(err)
+		}
+	},
+}




diff --git a/go.mod b/go.mod
index 04c6725ccdf946ea63a979f7578b42b9bf887157..f9195ccb5e52a0729c67e2c9ba64957c02d901eb 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@
 go 1.16
 
 require (
-	github.com/niklasfasching/go-org v1.5.0 // indirect
+	github.com/niklasfasching/go-org v1.5.0
 	github.com/spf13/cobra v1.1.3
-	gopkg.in/yaml.v2 v2.4.0
+	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
 )




diff --git a/go.sum b/go.sum
index 9577210ba58e16ffffda440c67d53d39f58c73ce..a799ca1dc78caf93fc0d337b7e3690ea9a4d00b9 100644
--- a/go.sum
+++ b/go.sum
@@ -95,6 +95,7 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
 github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
 github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
 github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -105,8 +106,10 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
@@ -134,6 +137,7 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@@ -291,6 +295,7 @@ google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
@@ -298,8 +303,9 @@ gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=




diff --git a/pkg/bookends/bookends.go b/pkg/bookends/bookends.go
index 357a2eb437560c33317e8b2971677c1bd9a4146f..51c798a904c905f103a132707bcdb5a8b38cdc01 100644
--- a/pkg/bookends/bookends.go
+++ b/pkg/bookends/bookends.go
@@ -2,9 +2,12 @@ package bookends
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 	"html/template"
 	"io"
+	"io/ioutil"
+	"net/http"
 	"net/url"
 	"os"
 	"path"
@@ -14,10 +17,27 @@ 	"strings"
 	"time"
 
 	"github.com/niklasfasching/go-org/org"
+	"gopkg.in/yaml.v3"
 )
 
 type DateRead struct {
 	time.Time
+}
+
+func (t *DateRead) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	var tm string
+	if err := unmarshal(&tm); err != nil {
+		return err
+	}
+
+	dr, err := time.Parse("2006-01-02", tm)
+	if err != nil {
+		return err
+	}
+
+	*t = DateRead{dr}
+
+	return nil
 }
 
 type Book struct {
@@ -63,6 +83,11 @@ 	return url.Parse(s)
 
 }
 
+func (b Book) CacheCoverURL() (*url.URL, error) {
+	localPath := path.Join("cache", fmt.Sprintf("%s.jpg", b.ISBN))
+	return url.Parse(localPath)
+}
+
 func (b Book) DownloadCover() error {
 	return nil
 }
@@ -110,7 +135,7 @@ 	if err != nil {
 		return err
 	}
 
-	books, err := OrgTest(f)
+	books, err := ParseOrgFile(f)
 
 	if err != nil {
 		return err
@@ -135,16 +160,20 @@
 	return nil
 }
 
-func OrgTest(input io.Reader) ([]Book, error) {
+func ParseOrgFile(input io.Reader) ([]Book, error) {
 	books := []Book{}
 	conf := org.New()
 	d := conf.Parse(input, "")
 
 	for _, child := range d.Outline.Children {
 		for _, sub := range child.Children {
+			tags := []string{}
+			for _, t := range sub.Headline.Tags {
+				tags = append(tags, strings.ReplaceAll(t, "_", "-"))
+			}
 			book := Book{
 				Title: sub.Headline.Title[0].String(),
-				Tags:  sub.Headline.Tags,
+				Tags:  tags,
 			}
 
 			rest := bytes.NewBufferString("")
@@ -248,3 +277,138 @@
 	return books, nil
 
 }
+
+func Yaml2Org() error {
+	contents, err := ioutil.ReadFile("books.yaml")
+	if err != nil {
+		return err
+	}
+
+	var books []Book
+	yaml.Unmarshal(contents, &books)
+	if err != nil {
+		return err
+	}
+
+	fmt.Println(books)
+	sort.Sort(BooksByDateRead(books))
+
+	year := 2021
+	fmt.Println("* 2021")
+
+	for _, book := range books {
+		if book.DateRead.Year() != year {
+			fmt.Println("*", book.DateRead.Year())
+			year = book.DateRead.Year()
+		}
+		fmt.Println("** DONE", book.Title)
+		fmt.Printf("CLOSED: [%s]\n", book.DateRead.Format("2006-01-02 Mon 15:04"))
+		fmt.Println(":PROPERTIES:")
+		fmt.Printf(":author: %s\n", book.Author)
+		fmt.Printf(":rating: %d\n", book.Rating)
+		fmt.Printf(":isbn: %s\n", book.ISBN)
+		fmt.Println(":END:")
+		fmt.Println("")
+	}
+
+	return nil
+}
+
+func copy(src, dst string) error {
+	sourceFileStat, err := os.Stat(src)
+	if err != nil {
+		return err
+	}
+
+	if !sourceFileStat.Mode().IsRegular() {
+		return fmt.Errorf("%s is not a regular file", src)
+	}
+
+	source, err := os.Open(src)
+	if err != nil {
+		return err
+	}
+	defer source.Close()
+
+	destination, err := os.Create(dst)
+	if err != nil {
+		return err
+	}
+	defer destination.Close()
+	_, err = io.Copy(destination, source)
+	return err
+}
+
+func CacheCovers() error {
+	f, err := os.Open("book-log.org")
+
+	if err != nil {
+		return err
+	}
+
+	books, err := ParseOrgFile(f)
+
+	if err != nil {
+		return err
+	}
+
+	cacheDir := "cache"
+	coversDir := "covers"
+
+	fmt.Println(len(books))
+
+	for _, book := range books {
+		fmt.Println("Processing", book.Title)
+		cachePath := path.Join(cacheDir, fmt.Sprintf("%s.jpg", book.ISBN))
+
+		if PathExists(cachePath) {
+			fmt.Println("  Cache exists", cachePath)
+			continue
+		}
+
+		coversPath := path.Join(coversDir, fmt.Sprintf("%s.jpg", book.ISBN))
+
+		if PathExists(coversPath) {
+			fmt.Println("  Cover exists", coversPath)
+			// Copy the file over
+			err := copy(coversPath, cachePath)
+
+			if err != nil {
+				return nil
+			}
+
+			continue
+
+		}
+
+		coverUrl, err := book.CoverURL()
+
+		if err != nil {
+			return err
+		}
+
+		fmt.Println("  GET", coverUrl.String())
+		resp, err := http.Get(coverUrl.String())
+
+		if err != nil {
+			return err
+		}
+
+		if resp.StatusCode > 201 {
+			return errors.New("  non 200 response:" + book.Title)
+		}
+
+		destination, err := os.Create(cachePath)
+		if err != nil {
+			return err
+		}
+		defer destination.Close()
+		_, err = io.Copy(destination, resp.Body)
+
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}




diff --git a/template.html b/template.html
new file mode 100644
index 0000000000000000000000000000000000000000..a5c0d1a3e3fb1c16662097378e8305e75f2999a9
--- /dev/null
+++ b/template.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>Reading List --- Honza Pokorny</title>
+        <meta name="viewport" content="width=device-width, initial-scale=1">
+        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
+
+        <style>
+            .star {
+                width: 13px;
+                height: 12px;
+                display: inline-block;
+                background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTMiIGhlaWdodD0iMTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTYuMzA5IDkuMjJMMi40MDkgMTJsMS40NC00LjU2N0wwIDQuNTgzbDQuNzg4LS4wNDJMNi4zMDggMCA3LjgzIDQuNTRsNC43ODkuMDQ0LTMuODUgMi44NDlMMTAuMjA5IDEyeiIgZmlsbC1ydWxlPSJldmVub2RkIi8+PC9zdmc+);
+            }
+            .star-1 { width: 13px; }
+            .star-2 { width: 26px; }
+            .star-3 { width: 39px; }
+            .star-4 { width: 52px; }
+            .star-5 { width: 65px; }
+        </style>
+    </head>
+    <body>
+        <section class="section">
+            <div class="container">
+                <h1 class="title">Reading List</h1>
+                <p class="subtitle">What is Honza reading these days?</p>
+            </div>
+        </section>
+
+        <section class="section">
+            <div class="container">
+                {{ range . }}
+                    <div class="columns">
+                        <div class="column is-2">
+                            <figure class="image">
+                                <img src="{{ .CacheCoverURL }}" alt="Placeholder image">
+                            </figure>
+                        </div>
+                        <div class="column is-10">
+                            <div class="media">
+                                <div class="media-content">
+                                    <p class="title is-4">{{ .Title }}</p>
+                                    <p class="subtitle is-6">{{ .Author }}</p>
+                                </div>
+                            </div>
+
+                            <div class="content">
+                                <div class="star star-{{ .Rating }}"></div>
+                                <div>
+                                    Finished on <time datetime="2016-1-1">{{ .DateRead.Format "January 2, 2006" }}</time>
+                                </div>
+                                <div>
+                                    {{ range .Tags }}
+                                        <span class="tag">{{ . }}</span>
+                                    {{ end }}
+                                </div>
+                                <div>
+                                    {{ .Review }}
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                {{ end }}
+            </div>
+        </section>
+
+    </body>
+</html>