├── README.org ├── go.mod └── main.go /README.org: -------------------------------------------------------------------------------- 1 | #+TITLE: Goodreads CSV to Org Mode file 2 | 3 | * Usage 4 | 5 | Steps: 6 | 1. Exporter you library 7 | 2. Run this tool 8 | 3. Open file in emacs 9 | 10 | 11 | It can be run like this: 12 | 13 | #+BEGIN_SRC bash 14 | go run main.go goodreads_library_export.csv > books.org 15 | #+END_SRC 16 | 17 | This will produced output like this: 18 | 19 | #+BEGIN_EXAMPLE 20 | 2020/12/15 11:21:29 Starting importer 21 | 2020/12/15 11:21:29 Parsing complete - 315 books on 3 shelves 22 | #+END_EXAMPLE 23 | 24 | The output looks like this: 25 | 26 | #+BEGIN_EXAMPLE 27 | #+TITLE: Books from Goodreads 28 | #+COMMENT: Imported by goodreads-to-org 29 | 30 | * CURRENTLY-READING 31 | ** INPROGRESS Extreme Ownership: How U.S. Navy SEALs Lead and Win by Jocko Willink 32 | :PROPERTIES: 33 | :Title: Extreme Ownership: How U.S. Navy SEALs Lead and Win 34 | :Author: Jocko Willink 35 | :ISBN13: 9781250067050 36 | :ISBN: 1250067057 37 | :Publisher: St. Martin's Press 38 | :Pages: 320 39 | :FirstPublish: 2015 40 | :Published: 2015 41 | :Added: 2020/06/11 42 | :END: 43 | ** INPROGRESS Dune Messiah by Frank Herbert 44 | :PROPERTIES: 45 | :Title: Dune Messiah 46 | :Author: Frank Herbert 47 | :Series: Dune 48 | :Series#: 2 49 | :Publisher: Ace 50 | :Pages: 350 51 | :FirstPublish: 1969 52 | :Published: 2008 53 | :Added: 2015/07/24 54 | :END: 55 | * TO-READ 56 | ** TODO War Dogs by Greg Bear 57 | :PROPERTIES: 58 | :Title: War Dogs 59 | :Author: Greg Bear 60 | :Series: War Dogs 61 | :Series#: 1 62 | :Publisher: Gollancz 63 | :Pages: 297 64 | :FirstPublish: 2014 65 | :Published: 2014 66 | :Added: 2020/06/11 67 | :END: 68 | * READ 69 | ** DONE The Power of Habit: Why We Do What We Do and How to Change by Charles Duhigg 70 | :PROPERTIES: 71 | :Title: The Power of Habit: Why We Do What We Do and How to Change 72 | :Author: Charles Duhigg 73 | :Publisher: Cornerstone Digital 74 | :Pages: 402 75 | :FirstPublish: 2012 76 | :Published: 2012 77 | :Added: 2015/12/27 78 | :END: 79 | 80 | #+END_EXAMPLE 81 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lchausmann/goodreads-to-org 2 | 3 | go 1.15 4 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/csv" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "strings" 12 | ) 13 | 14 | type Book struct { 15 | Id string 16 | Series string 17 | SeriesNo string 18 | Title string 19 | Author string 20 | AuthorLf string 21 | AdditionalAuthorrs string 22 | Isbn string 23 | Isbn13 string 24 | MyRating string 25 | AvgRating string 26 | Publisher string 27 | Binding string 28 | Pages string 29 | PublishedYear string 30 | OriginalPublicationYear string 31 | ReadDate string 32 | AddDate string 33 | Bookshelves string 34 | State string 35 | } 36 | 37 | func parseBookLine(record []string) Book { 38 | book := Book{} 39 | book.Id = record[0] 40 | title := record[1] 41 | if strings.Contains(title, "#") { 42 | // Handle series 43 | splits := strings.Split(title, "(") 44 | book.Title = strings.TrimSpace(splits[0]) 45 | 46 | if len(splits) > 1 { 47 | seriesSplit := strings.Split(splits[1], "#") 48 | series := strings.TrimRight(strings.TrimSpace(seriesSplit[0]), ",") 49 | book.Series = series 50 | if len(seriesSplit) > 1 { 51 | book.SeriesNo = strings.TrimRight(strings.TrimSpace(seriesSplit[1]), ")") 52 | } 53 | } 54 | 55 | } else { 56 | book.Title = record[1] 57 | } 58 | 59 | book.Author = record[2] 60 | book.Isbn = strings.TrimLeft(strings.ReplaceAll(record[5], "\"", ""), "=") 61 | book.Isbn13 = strings.TrimLeft(strings.ReplaceAll(record[6], "\"", ""), "=") 62 | book.MyRating = record[7] 63 | book.Publisher = record[9] 64 | book.Binding = record[10] 65 | book.Pages = record[11] 66 | book.PublishedYear = record[12] 67 | book.OriginalPublicationYear = record[13] 68 | book.ReadDate = record[14] 69 | book.AddDate = record[15] 70 | book.Bookshelves = record[18] 71 | 72 | switch record[18] { 73 | case "read": 74 | book.State = "DONE" 75 | case "currently-reading": 76 | book.State = "INPROGRESS" 77 | case "to-read": 78 | book.State = "TODO" 79 | default: 80 | book.State = "TODO" 81 | 82 | } 83 | 84 | if record[18] == "read" { 85 | 86 | } else if record[18] == "to-read" { 87 | book.State = "TODO" 88 | } 89 | 90 | return book 91 | } 92 | 93 | func (v Book) String() string { 94 | if len(v.Series) > 0 { 95 | return fmt.Sprintf("Title: %s (Serie: %s/%s) by %s on shelve: %s\n", v.Title, v.Series, v.SeriesNo, v.Author, v.Bookshelves) 96 | } else { 97 | return fmt.Sprintf("Title: %s by %s on shelve: %s\n", v.Title, v.Author, v.Bookshelves) 98 | } 99 | } 100 | 101 | func writeString(input string, key string) string { 102 | out := "" 103 | if len(input) > 0 { 104 | out = fmt.Sprintf(":%s: %s\n", key, input) 105 | } 106 | 107 | return out 108 | } 109 | 110 | func (b Book) ToOrgMode() string { 111 | var buffer strings.Builder 112 | buffer.WriteString(fmt.Sprintf("** %s %s by %s\n", b.State, b.Title, b.Author)) 113 | buffer.WriteString(":PROPERTIES:\n") 114 | buffer.WriteString(writeString(b.Title, "Title")) 115 | buffer.WriteString(writeString(b.Author, "Author")) 116 | buffer.WriteString(writeString(b.Series, "Series")) 117 | buffer.WriteString(writeString(b.SeriesNo, "Series#")) 118 | buffer.WriteString(writeString(b.Isbn13, "ISBN13")) 119 | buffer.WriteString(writeString(b.Isbn, "ISBN")) 120 | buffer.WriteString(writeString(b.Publisher, "Publisher")) 121 | buffer.WriteString(writeString(b.Pages, "Pages")) 122 | buffer.WriteString(writeString(b.OriginalPublicationYear, "FirstPublish")) 123 | buffer.WriteString(writeString(b.PublishedYear, "Published")) 124 | buffer.WriteString(writeString(b.AddDate, "Added")) 125 | buffer.WriteString(writeString(b.ReadDate, "Read")) 126 | 127 | buffer.WriteString(":END:") 128 | return buffer.String() 129 | } 130 | 131 | func main() { 132 | 133 | log.Println("Starting importer") 134 | if len(os.Args) < 2 { 135 | fmt.Println("Please specify file to read") 136 | os.Exit(1) 137 | } 138 | 139 | file := os.Args[1] 140 | data, err := ioutil.ReadFile(file) 141 | if err != nil { 142 | fmt.Println("File reading error", err) 143 | os.Exit(1) 144 | } 145 | 146 | bookshelves := map[string][]Book{} 147 | 148 | //books := []Book{} 149 | r := csv.NewReader(bytes.NewReader(data)) 150 | for { 151 | record, err := r.Read() 152 | if err == io.EOF { 153 | break 154 | } 155 | if err != nil { 156 | log.Fatal(err) 157 | } 158 | book := parseBookLine(record) 159 | 160 | // Skip first line 161 | if book.Title != "Title" && book.Author != "Author" { 162 | bookshelves[book.Bookshelves] = append(bookshelves[book.Bookshelves], book) 163 | 164 | } 165 | 166 | } 167 | 168 | fmt.Println("#+TITLE: Books from Goodreads") 169 | fmt.Println("#+COMMENT: Imported by goodreads-to-org") 170 | fmt.Println("") 171 | 172 | shelves := 0 173 | noOfBooks := 0 174 | 175 | for shelf, books := range bookshelves { 176 | fmt.Println("* ", strings.ToUpper(shelf)) 177 | shelves++ 178 | noOfBooks += len(books) 179 | for _, book := range books { 180 | fmt.Println(book.ToOrgMode()) 181 | } 182 | } 183 | log.Println("Parsing complete -", noOfBooks, "books on", shelves, "shelves") 184 | } 185 | --------------------------------------------------------------------------------