A production-ready Go application that imports your reading history from Audible, Kindle, and Storytel into a unified JSON format, then exports it to a Goodreads-compatible CSV for easy import.
- 📚 Multiple Sources: Import from Audible (via OpenAudible), Kindle, and Storytel
- 🔄 Unified Library: Consolidates all your books into a single, deduplicated JSON library
- 📊 Goodreads Export: Generates a CSV file that can be imported directly into Goodreads
- 🎨 Three Interfaces: Web UI, interactive TUI, and classic CLI
- 🔒 Privacy-First: All processing happens locally—no logins, no API calls
- ✨ Smart Deduplication: Merges duplicate entries using ISBN13, ISBN10, ASIN, or title+author matching
- 🕐 Timezone-Aware: Properly handles date normalization and formatting
media2goodreads solves the problem of consolidating your scattered reading history:
- Import from multiple sources (Audible, Kindle, Storytel)
- Merge into a unified library with smart deduplication
- Export to Goodreads-compatible CSV format
- Track your complete reading history in one place
- Go 1.22 or higher
- Node.js 18 or higher (for the web UI)
- (Optional) golangci-lint for development
git clone https://github.com/yourusername/media2goodreads.git
cd media2goodreads
# Install dependencies
make deps
# Build the application
make build
# (Optional) Install to your PATH
make installCopy the example configuration file and customize it:
cp configs/env.example .envEdit .env with your preferred settings:
# Output directory for generated files
MEDIA2GR_OUT_DIR=./out
# Timezone for date normalization
MEDIA2GR_TZ=America/Sao_Paulo
# Default shelf for Goodreads export (read|to-read|currently-reading)
MEDIA2GR_DEFAULT_SHELF=read
# Optional date added to Goodreads (YYYY-MM-DD); defaults to today if empty
MEDIA2GR_DATE_ADDED=
# Audible import settings
MEDIA2GR_AUDIBLE_EXPORT_PATH=/path/to/openaudible/books.json
MEDIA2GR_AUDIBLE_FORMAT=json
MEDIA2GR_AUDIBLE_REGION=br
# Kindle import settings
MEDIA2GR_KINDLE_CLIPPINGS_PATH=/path/to/My Clippings.txt
MEDIA2GR_KINDLE_NOTEBOOK_HTML_DIR=
# Storytel import settings
MEDIA2GR_STORYTEL_EXPORT_PATH=/path/to/storytel_history.csv
MEDIA2GR_STORYTEL_FORMAT=csv- Download and install OpenAudible
- Connect your Audible account and download your library metadata
- Export your library: File → Export → JSON or CSV
- Use the exported
books.jsonorbooks.csvfile
Option 1: My Clippings.txt
- Connect your Kindle device to your computer
- Navigate to the Kindle drive (appears as a USB drive)
- Copy
documents/My Clippings.txtfrom your Kindle
Option 2: Kindle Notebook (for more detail)
- Go to Amazon Kindle Notebook
- For each book, save the notebook page as HTML (Ctrl+S or Cmd+S)
- Save all HTML files to a directory
- Log in to your Storytel account on the web
- Go to your listening history or library
- Export your history (if available) as CSV or JSON
- Note: Storytel's export capabilities may vary by region
After generating your CSV:
- Go to Goodreads Import & Export
- Click "Choose File" and select your
goodreads_import.csv - Click "Import books"
The web UI requires both the Go backend and the React frontend to be running.
Option A — production mode (single command, frontend pre-built):
# Install all dependencies and build everything
make deps
make web
# Opens the server at http://localhost:8080Option B — development mode (hot-reload frontend, separate terminals):
# Terminal 1: build and start the Go API server
make build
./bin/media2goodreads web
# Terminal 2: start the Vite dev server (proxies /api to localhost:8080)
cd web && npm run dev
# Open http://localhost:5173 in your browserImportant: The frontend dev server (
npm run dev) proxies all/apirequests to the Go backend on port 8080. You must have the Go server running first or every API call will fail with "Internal Server Error".
Custom address and static directory:
./bin/media2goodreads web --addr localhost:9000 --static ./web/dist./bin/media2goodreads tuiThe TUI provides an interactive menu to guide you through:
- Selecting your import source
- Entering file paths
- Viewing import results
- Exporting to Goodreads CSV
Keyboard shortcuts:
↑/↓orTab: NavigateEnter: Select/ConfirmEsc: Go backo: Open output folderq: Quit
1. Import from Audible
./bin/media2goodreads audible import \
--from ./exports/books.json \
--format json \
--region br2. Import from Kindle
./bin/media2goodreads kindle import \
--clippings "./My Clippings.txt" \
--notebook-html ./kindle_notebooks3. Import from Storytel
./bin/media2goodreads storytel import \
--from ./storytel_history.csv \
--format csv4. Generate Goodreads CSV
./bin/media2goodreads goodreads-csv \
--in ./out/library.json \
--out ./out/goodreads_import.csv \
--shelf read \
--added 2025-10-05All-in-one example:
# Import from all sources
./bin/media2goodreads audible import --from ./audible_books.json --format json
./bin/media2goodreads kindle import --clippings "./My Clippings.txt"
./bin/media2goodreads storytel import --from ./storytel.csv --format csv
# Export to Goodreads
./bin/media2goodreads goodreads-csv --in ./out/library.json --out ./out/goodreads_import.csvThe unified library uses this data model:
type BookItem struct {
ID string // Auto-generated unique ID
Title string // Normalized title
Authors []string // List of authors (normalized)
ASIN string // Amazon Standard Identification Number
ISBN10 string // 10-digit ISBN
ISBN13 string // 13-digit ISBN
Format string // "audiobook" or "ebook"
Source []string // e.g., ["audible", "kindle"]
StartedAt *time.Time // When you started reading
FinishedAt *time.Time // When you finished
Rating *int // Your rating (0-5)
Review string // Your review text
Publisher string // Publisher name
PageCount *int // Number of pages
YearPublished *int // Year of publication
Notes []string // Additional notes
}Books are deduplicated using this priority order:
- ISBN13 - Highest priority, most reliable
- ISBN10 - Second priority
- ASIN - Third priority (for Audible books)
- Title + First Author Slug - Fallback for books without identifiers
When merging duplicate entries:
- Non-empty values are preferred
- Authors and sources are combined (union)
- Earliest
StartedAtis kept - Latest
FinishedAtis kept - Existing rating is preserved
The CSV export follows Goodreads' exact format requirements:
| Goodreads Field | Source |
|---|---|
| Title | BookItem.Title |
| Author | First author |
| Author l-f | First author in "Last, First" format |
| Additional Authors | Remaining authors (comma-separated) |
| ISBN | BookItem.ISBN10 |
| ISBN13 | BookItem.ISBN13 |
| My Rating | BookItem.Rating (0-5 scale) |
| Publisher | BookItem.Publisher |
| Year Published | BookItem.YearPublished |
| Date Read | BookItem.FinishedAt (MM/DD/YYYY) |
| Date Added | Today or --added flag (MM/DD/YYYY) |
| Bookshelves | Format + sources (e.g., "audiobook, audible") |
| Exclusive Shelf | "read" if FinishedAt exists, else "to-read" |
| My Review | BookItem.Review |
| Private Notes | BookItem.Notes (joined) |
make testmake test-coveragemake lintmake fmtmake checkmedia2goodreads/
├── cmd/media2goodreads/ # Main application entry point
│ └── main.go
├── internal/
│ ├── model/ # Data models
│ │ └── types.go
│ ├── util/ # Utility functions
│ │ ├── normalize.go # Text normalization
│ │ ├── dedupe.go # Deduplication logic
│ │ ├── dates.go # Date parsing/formatting
│ │ ├── files.go # File operations
│ │ └── logger.go # Structured logging
│ ├── ingest/ # Import parsers
│ │ ├── audible.go
│ │ ├── kindle.go
│ │ ├── storytel.go
│ │ └── merge.go
│ ├── export/ # Export logic
│ │ └── goodreads_csv.go
│ └── tui/ # Text-based UI
│ ├── model.go
│ ├── update.go
│ ├── view.go
│ └── components.go
├── configs/
│ └── env.example # Example configuration
├── testdata/ # Test fixtures
├── out/ # Output directory (generated)
├── Makefile # Build automation
├── .golangci.yml # Linter configuration
└── README.md
- Issue: Goodreads rejects the CSV file
- Solution:
- Ensure the CSV has the exact header format (run
make testto verify) - Check that dates are in MM/DD/YYYY format
- Verify ISBN format (no hyphens, correct length)
- Ensure the CSV has the exact header format (run
- Issue: Some books don't have read dates
- Solution:
- For Kindle: Only books with highlights/notes will have dates (latest highlight timestamp)
- For Audible: Uses purchase date as finished date
- Manually edit
out/library.jsonif needed before exporting
- Issue: Books appear multiple times in the library
- Solution:
- Check if books have proper ISBNs
- Different editions may have different ISBNs (working as intended)
- Manually merge in
out/library.jsonif needed
- Issue: Dates are off by a day or more
- Solution:
- Set
MEDIA2GR_TZin your.envfile to your timezone - Common values:
America/New_York,Europe/London,Asia/Tokyo - List of timezones: Wikipedia TZ Database
- Set
- Issue: No books detected from My Clippings.txt
- Solution:
- Ensure the file is in UTF-8 encoding
- Check that highlights are separated by
========== - Verify the file isn't corrupted
✅ All processing is local - No data leaves your computer
✅ No logins required - Works entirely with exported files
✅ No API calls - Doesn't connect to any external services
✅ Open source - Inspect the code yourself
Your reading data is personal. This tool respects your privacy by keeping everything on your machine.
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT License - see LICENSE file for details
- OpenAudible - For making Audible exports possible
- Bubble Tea - For the excellent TUI framework
- Cobra - For CLI scaffolding
- The Goodreads community for maintaining import compatibility
Made with ❤️ for book lovers who want their reading history in one place