Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:58:28 +08:00
commit e60768ac8e
10 changed files with 1020 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
{
"name": "goodreads",
"description": "TODO: Add description",
"version": "0.0.1",
"author": {
"name": "TODO: Add author"
},
"skills": [
"./skills"
],
"commands": [
"./commands"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# goodreads
TODO: Add description

110
commands/next.md Normal file
View File

@@ -0,0 +1,110 @@
---
description: Analyze my reading patterns and suggest what to read next from my TBR
---
You are helping the user decide what to read next from their Goodreads TBR list.
## Analysis Steps
Use the `analyze-goodreads-export` skill to perform the following analysis:
### 1. Analyze Recent Reading Patterns
Query the last 15 books read (sorted by date_read DESC):
- Calculate average page count of recent reads
- Identify if the user has been reading mostly long books (>600 pages)
- Look for series patterns in recent reads
- Use the `date_read` field to determine actual reading order
- Look at `my_rating` field to see what books the user liked
### 2. Check for Series Continuity
For each series found in recent reads:
- Check if there are unread books in that series on the TBR
- Prioritize the next book in sequence (series_index), especially if the previous book had a high rating
- This is important for maintaining reading momentum!
### 3. Consider Reading Fatigue
Based on recent page counts:
- If average recent reads > 600 pages: Suggest shorter books (< 300 pages)
- If average recent reads < 400 pages: User might be ready for something longer
- Look for highly-rated short books as "palate cleansers"
### 4. Check Book Age in Library
Query books by date_added:
- Find recently added books (last 30 days) that are on TBR
- Find old books (added >1 year ago) that may have been forgotten
- Use `date_added` field to determine when book was added
### 5. Filter by Quality
Prioritize books with:
- Goodreads rating >= 3.75 (if available)
- Consider page count relative to recent reading patterns
- Balance between series continuity and variety
## Output Format
Structure your response as a structured report with these categories:
```
# READING PATTERN SUMMARY
- Books read in last 30 days: X
- Average page count: Y pages
- Notable patterns: [e.g., "Completed The Carls series"]
# RECOMMENDATIONS BY CATEGORY
## 📚 SERIES CONTINUITY
Books that continue series you're currently reading:
- **Book Title** by Author
Series: Series Name #X | Pages: XXX | Rating: X.X/5 | Added: [date/age]
## 🆕 RECENTLY ADDED
Books added to your TBR in the last 30 days:
- **Book Title** by Author
Pages: XXX | Rating: X.X/5 | Added: [date]
## 💎 FORGOTTEN GEMS
Books on your TBR added over a year ago:
- **Book Title** by Author
Pages: XXX | Rating: X.X/5 | Added: [date/years ago]
## ⚡ QUICK READS
Shorter books (< 300 pages) for reading fatigue:
- **Book Title** by Author
Pages: XXX | Rating: X.X/5 | Added: [age]
## 🌟 HIGHLY RATED
Top-rated unread books from your TBR:
- **Book Title** by Author
Pages: XXX | Rating: X.X/5 | Added: [age]
```
## Important Notes
- Use `date_added` to determine when books were added to the library
- Calculate age from date_added (e.g., "2 days ago", "3 months ago", "2 years ago")
- Include 1-3 books per category (skip categories if no matches)
- ALWAYS check for incomplete series from recent reads first
- Balance series continuity with reading fatigue and variety
- Present data in a clean, scannable format
- Each category should help answer a different need: momentum, novelty, rediscovery, fatigue, or quality
- Only include books from the TBR list (where exclusive_shelf contains "to-read")
## Implementation
Write a Python script using the goodreads_lib to:
1. Get the last 15 read books
2. Analyze patterns (page count, series, ratings)
3. Query TBR for recommendations in each category
4. Format and display results
Use the Bash tool to run your Python script.

85
commands/random.md Normal file
View File

@@ -0,0 +1,85 @@
---
description: Pick a random book from TBR or library
---
You are helping the user pick a random book from their Goodreads library.
## Task
Use the `analyze-goodreads-skill` skill to pick a random book from their TBR list.
## Implementation
Write a Python script using goodreads_lib:
```python
#!/usr/bin/env python3
import sys
import random
sys.path.insert(0, '__SKILL_DIR__/scripts')
from goodreads_lib import GoodreadsLibrary
lib = GoodreadsLibrary()
# Get TBR books
tbr = lib.get_tbr_books()
if not tbr:
print("No books in your TBR!")
sys.exit(0)
# Pick random book
book = random.choice(tbr)
# Display
print("\n# 🎲 RANDOM TBR PICK\n")
print(f"**{book.title}**")
print(f"by {book.author}\n")
if book.series and book.series_index:
print(f"📚 Series: {book.series} #{book.series_index}")
if book.num_pages:
print(f"📖 Pages: {book.num_pages}")
if book.average_rating:
print(f"⭐ Goodreads Rating: {book.average_rating:.2f}/5")
if book.date_added:
from datetime import datetime
days_ago = (datetime.now() - book.date_added).days
if days_ago < 30:
print(f"🆕 Added: {days_ago} days ago")
elif days_ago < 365:
months = days_ago // 30
print(f"📅 Added: {months} months ago")
else:
years = days_ago // 365
print(f"📅 Added: {years} years ago")
print(f"\nTotal TBR books: {len(tbr)}")
```
## Output Format
```
# 🎲 RANDOM TBR PICK
**Book Title**
by Author Name
📚 Series: Series Name #X
📖 Pages: XXX
⭐ Goodreads Rating: X.XX/5
📅 Added: X months/years ago
Total TBR books: XX
```
## Important Notes
- Pick from books where `is_tbr` is True
- Display relevant metadata (series, pages, rating, date added)
- Handle missing data gracefully
- Use the Bash tool to run your Python script
- Replace `__SKILL_DIR__` with the actual skill directory path

80
commands/series.md Normal file
View File

@@ -0,0 +1,80 @@
---
description: List unfinished series and the next book to read in each
---
You are helping the user find incomplete series in their Goodreads library.
## Task
Use the `analyze-goodreads-export` skill to find series where:
- The user has read at least one book in the series
- There are still unread books in the series
- Display the next book to read in each series
## Implementation
Write a Python script using goodreads_lib:
```python
#!/usr/bin/env python3
import sys
sys.path.insert(0, '__SKILL_DIR__/scripts')
from goodreads_lib import GoodreadsLibrary
lib = GoodreadsLibrary()
# Get incomplete series
incomplete = lib.get_incomplete_series()
# Sort by series name
incomplete_sorted = sorted(incomplete.items(), key=lambda x: x[0])
# Display results
print("\n# UNFINISHED SERIES\n")
print(f"You have {len(incomplete_sorted)} incomplete series:\n")
for i, (series_name, info) in enumerate(incomplete_sorted, 1):
read = info['read_count']
total = info['total_count']
next_book = info['next_book']
print(f"{i}. **{series_name}** ({read}/{total} books read)")
if next_book:
title = next_book.title
author = next_book.author
index = next_book.series_index or '?'
pages = next_book.num_pages or '?'
rating = f"{next_book.average_rating:.2f}" if next_book.average_rating else "N/A"
print(f" Next: **{title}** by {author}")
print(f" Book #{index} | {pages} pages | Goodreads: {rating}/5\n")
else:
print(f" Next: Unable to determine\n")
```
## Output Format
```
# UNFINISHED SERIES
You have X incomplete series:
1. **Series Name** (X/Y books read)
Next: **Book Title** by Author
Book #X | XXX pages | Goodreads: X.XX/5
2. **Another Series** (X/Y books read)
Next: **Book Title** by Author
Book #X | XXX pages | Goodreads: X.XX/5
```
## Important Notes
- Series information is parsed from book titles (e.g., "Title (Series, #1)")
- Books are sorted by series_index within each series
- Only show series where at least one book has been read
- The "next" book is the first unread book in series order
- Handle missing data gracefully (pages, ratings may be missing)
- Use the Bash tool to run your Python script
- Replace `__SKILL_DIR__` with the actual skill directory path

141
commands/stats.md Normal file
View File

@@ -0,0 +1,141 @@
---
description: Show reading statistics (books per year/month, pages read, average rating, genre breakdown)
---
You are helping the user analyze their reading statistics from their Goodreads library.
## Analysis to Perform
Use the `analyze-goodreads-export` skill to gather and analyze the following statistics:
### 1. Reading Velocity
Query books read in different time periods:
- Books read this year (use `date_read.year == current_year`)
- Books read last 30 days
- Books read last 90 days
- Break down by month for current year
Calculate:
- Books per month average (current year)
- Pages per month average
- Current reading pace vs yearly average
### 2. Page Statistics
Query all read books with page counts:
- Total pages read this year
- Total pages read all time
- Average pages per book
- Longest book read
- Shortest book read
### 3. Rating Analysis
Query all read books with ratings:
- Average rating given (your `my_rating` field)
- Average Goodreads rating of books read (`average_rating` field)
- Most common rating you give
- Distribution of ratings (how many 5-star, 4-star, etc.)
### 4. Author Statistics
Query all read books:
- Most read authors (count by author name)
- Total unique authors read
### 5. Series Statistics
Query all read books with series information:
- Books read that are part of series vs standalone
- Most read series
- Number of complete series finished
### 6. To-Be-Read Statistics
Query TBR list (where `is_tbr` is True):
- Total books in TBR
- Total pages in TBR
- Average Goodreads rating of TBR
- Oldest book in TBR (by date_added)
- Books added to TBR in last 30 days
## Output Format
Present statistics in a clean, organized report:
```
# READING STATISTICS
## 📊 Reading Velocity
- **This Year**: X books (Y pages)
- **Last 30 Days**: X books (Y pages)
- **Average Pace**: X books/month, Y pages/month
### Monthly Breakdown (YYYY)
Jan: X books | Feb: X books | Mar: X books | etc.
## 📖 Page Statistics
- **Total Pages Read (All Time)**: X,XXX pages
- **Total Pages Read (This Year)**: X,XXX pages
- **Average Book Length**: XXX pages
- **Longest Book**: [Title] by [Author] (XXX pages)
- **Shortest Book**: [Title] by [Author] (XXX pages)
## ⭐ Rating Analysis
- **Your Average Rating**: X.X / 5
- **Goodreads Average of Books Read**: X.X / 5
- **Most Common Rating**: X stars
### Rating Distribution
★★★★★: XX books (XX%)
★★★★☆: XX books (XX%)
★★★☆☆: XX books (XX%)
★★☆☆☆: XX books (XX%)
★☆☆☆☆: XX books (XX%)
## ✍️ Author Statistics
- **Total Authors Read**: XX unique authors
- **Most Read Authors**:
1. [Author Name]: X books
2. [Author Name]: X books
3. [Author Name]: X books
## 📚 Series Statistics
- **Books in Series**: XX books (XX% of total)
- **Standalone Books**: XX books (XX% of total)
- **Most Read Series**:
1. [Series Name]: X books
2. [Series Name]: X books
## 📋 To-Be-Read Statistics
- **Total TBR Books**: XXX books (X,XXX pages)
- **Average TBR Rating**: X.X / 5
- **Added Recently**: XX books in last 30 days
- **Oldest Unread**: [Title] (added X years/months ago)
## 🎯 Reading Insights
[Provide 2-3 interesting insights, such as:]
- You're on track to read XX books this year
- Your reading pace has [increased/decreased] compared to last year
- You tend to rate books higher/lower than Goodreads average
- You're reading more/fewer series books than standalone
```
## Implementation
Write a Python script using goodreads_lib to:
1. Query all read books
2. Calculate statistics for each category
3. Format and display results with proper formatting
Use the Bash tool to run your Python script.
## Important Notes
- Use `date_read` field to determine if/when book was read
- Calculate percentages and averages from the data
- Present large numbers with thousand separators for readability
- Compare current year to all-time averages where interesting
- Handle missing data gracefully (some books may not have all fields)
- Round floating point values to 2 decimal places for readability

125
commands/vibes.md Normal file
View File

@@ -0,0 +1,125 @@
---
description: Find similar books in your library based on genre, author, or themes
---
You are helping the user find books similar to one they specify.
## Task
When the user asks to find books with similar "vibes" or similar to a specific book:
1. Ask them what book they want to find similar books to (if not already specified)
2. Use the `analyze-goodreads-export` skill to search for that book in their library
3. Find similar books based on:
- Same author
- Books on the same custom shelves (genre indicators)
- Similar series (if applicable)
- Similar page count
- Similar ratings
## Implementation
Write a Python script using goodreads_lib:
```python
#!/usr/bin/env python3
import sys
sys.path.insert(0, '__SKILL_DIR__/scripts')
from goodreads_lib import GoodreadsLibrary
lib = GoodreadsLibrary()
# Get the reference book (user should specify)
search_term = "BOOK_TITLE" # Replace with user's input
# Find the book
matches = [b for b in lib.books if search_term.lower() in b.title.lower()]
if not matches:
print(f"Could not find '{search_term}' in your library")
sys.exit(1)
ref_book = matches[0]
print(f"\n# BOOKS SIMILAR TO: {ref_book.title}\n")
# Find similar books
similar = []
# 1. Same author
if ref_book.author:
same_author = [b for b in lib.books
if b.author == ref_book.author
and b.book_id != ref_book.book_id
and not b.is_read]
if same_author:
print(f"## 📚 More by {ref_book.author}\n")
for book in same_author[:3]:
pages = f"{book.num_pages} pages" if book.num_pages else "? pages"
rating = f"{book.average_rating:.2f}/5" if book.average_rating else "N/A"
print(f"- **{book.title}** | {pages} | ⭐ {rating}")
print()
# 2. Same shelves (genre indicators)
if ref_book.bookshelves:
shelves = ref_book.bookshelves.split(',')
for shelf in shelves[:2]: # Check first 2 shelves
shelf = shelf.strip()
if shelf:
same_shelf = [b for b in lib.books
if shelf in b.bookshelves
and b.book_id != ref_book.book_id
and not b.is_read]
if same_shelf:
print(f"## 🏷️ Similar (from '{shelf}' shelf)\n")
for book in same_shelf[:3]:
pages = f"{book.num_pages} pages" if book.num_pages else "? pages"
rating = f"{book.average_rating:.2f}/5" if book.average_rating else "N/A"
print(f"- **{book.title}** by {book.author} | {pages} | ⭐ {rating}")
print()
# 3. Similar series (if in a series)
if ref_book.series:
series_books = lib.get_series_books(ref_book.series)
unread_in_series = [b for b in series_books if not b.is_read]
if unread_in_series:
print(f"## 📖 More in {ref_book.series}\n")
for book in unread_in_series[:3]:
idx = f"#{book.series_index}" if book.series_index else ""
pages = f"{book.num_pages} pages" if book.num_pages else "? pages"
rating = f"{book.average_rating:.2f}/5" if book.average_rating else "N/A"
print(f"- **{book.title}** {idx} | {pages} | ⭐ {rating}")
print()
```
## Output Format
```
# BOOKS SIMILAR TO: Reference Book Title
## 📚 More by Author Name
- **Book Title** | XXX pages | ⭐ X.XX/5
- **Book Title** | XXX pages | ⭐ X.XX/5
## 🏷️ Similar (from 'genre' shelf)
- **Book Title** by Author | XXX pages | ⭐ X.XX/5
- **Book Title** by Author | XXX pages | ⭐ X.XX/5
## 📖 More in Series Name
- **Book Title** #X | XXX pages | ⭐ X.XX/5
```
## Important Notes
- Ask the user which book they want to find similar books to
- Search is case-insensitive for the title
- Prioritize unread books in results
- Custom shelves can indicate genres (e.g., "mental-health", "favorites")
- Limit results to 3 per category for readability
- Handle missing data gracefully
- Use the Bash tool to run your Python script
- Replace `__SKILL_DIR__` with the actual skill directory path
- Replace `BOOK_TITLE` with the user's search term

69
plugin.lock.json Normal file
View File

@@ -0,0 +1,69 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:stbenjam/claude-nine:plugins/goodreads",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "93ad1989021eb8b2e8e61ea49cc22ff50b972d51",
"treeHash": "2b47db04774da8cda8df36651d106ddcf03b5eaeb1f43ca04afe36ba92437461",
"generatedAt": "2025-11-28T10:28:27.528112Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "goodreads",
"description": "TODO: Add description",
"version": "0.0.1"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "5c8002b7d9cc7a2b1144edf830a6615f95c94997835eb0d695a9e4d4d7904161"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "3636a3d839a22ce73a12f664f2b203b061dac49f52735a4c6f7c859a61a9d2e1"
},
{
"path": "commands/vibes.md",
"sha256": "d5c8df84aba091f25324f475ef597e3a337414e1f22909cab2dc30fc0d43af78"
},
{
"path": "commands/stats.md",
"sha256": "583390927b3b48dbf8d89d41a21f2ade6836d97d41d9dd7b3e2ece5600020329"
},
{
"path": "commands/next.md",
"sha256": "4888e6ebda6326d23c28af706c6b317d3c4ca3774ee759a3771726f86a8d3dbb"
},
{
"path": "commands/series.md",
"sha256": "78f928101a3df54eb1225a761b33bea2c1df40b2b8e50524cceec0f04f54b691"
},
{
"path": "commands/random.md",
"sha256": "0b2fb6d2478921c0b77e030aeeec0bafbc5489e366371435dd822e25189c773c"
},
{
"path": "skills/analyze-goodreads-export/SKILL.md",
"sha256": "4cbccda7dd17d74fdc89250619fcd09ce94f6e92e282f4f0478e7b0466ce4052"
},
{
"path": "skills/analyze-goodreads-export/scripts/goodreads_lib.py",
"sha256": "420a562570a723724b9d4cd7d32ec0c24f4f4c053d83033cec2c67910e85b8ca"
}
],
"dirSha256": "2b47db04774da8cda8df36651d106ddcf03b5eaeb1f43ca04afe36ba92437461"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,150 @@
---
name: use-goodreads-export
description: Search and query Goodreads library from CSV export. Use when the user asks about books, TBR (to-be-read), reading lists, book searches, or mentions Goodreads. Also use for queries about book ratings, authors, reading status, or library statistics.
---
You are helping the user query their Goodreads library from a CSV
export. Use the script here! DO NOT write your own script
## CSV Location
The Goodreads export CSV is a file typically called:
```
goodreads_library_export.csv
```
You can prompt the user for its location if you can't find it.
## Python Library
A Python library is available at `__SKILL_DIR__/scripts/goodreads_lib.py` that provides:
### Classes
**GoodreadsBook** - Represents a single book with properties:
- `title`, `author`, `series`, `series_index`
- `my_rating` (1-5), `average_rating` (Goodreads rating)
- `num_pages`, `date_read`, `date_added`
- `exclusive_shelf` (e.g., "to-read", "currently-reading")
- `bookshelves` (custom shelves)
- `is_read`, `is_tbr`, `is_currently_reading` (properties)
- `has_shelf(shelf_name)` - Check if on specific shelf
**GoodreadsLibrary** - Main query interface:
```python
from goodreads_lib import GoodreadsLibrary
lib = GoodreadsLibrary() # Loads from default CSV path
# Query methods:
lib.get_read_books(limit=15, sort_by_date=True) # Get read books
lib.get_tbr_books() # Get to-be-read list
lib.get_books_by_shelf('mental-health') # Get books on shelf
lib.get_books_read_in_period(30) # Books read in last 30 days
lib.get_books_read_in_year(2024) # Books read in year
lib.get_books_added_in_period(30) # Recently added books
lib.get_series_books('The Carls') # Books in series
lib.get_all_series() # All series with books
lib.get_incomplete_series() # Series partially read
lib.get_author_stats() # Author statistics
lib.get_rating_distribution() # Rating distribution
lib.query(lambda book: book.num_pages < 300) # Custom queries
```
## Usage Instructions
When the user asks about their Goodreads library:
1. **Determine the query type**: TBR list, read books, statistics, series info, etc.
2. **Write a Python script** using the library:
```python
#!/usr/bin/env python3
import sys
sys.path.insert(0, '__SKILL_DIR__/scripts')
from goodreads_lib import GoodreadsLibrary
lib = GoodreadsLibrary()
# Your query logic here
```
3. **Use the Bash tool** to run your script
4. **Format results** nicely for the user
## Common Query Patterns
### TBR List
```python
tbr = lib.get_tbr_books()
for book in tbr[:10]:
print(f"- {book.title} by {book.author}")
```
### Recent Reads
```python
recent = lib.get_read_books(limit=15)
for book in recent:
print(f"- {book.title} by {book.author} ({book.date_read.strftime('%Y-%m-%d')})")
```
### Books on Specific Shelf
```python
books = lib.get_books_by_shelf('favorites')
for book in books:
print(f"- {book.title} by {book.author} (⭐ {book.my_rating}/5)")
```
### Series Analysis
```python
incomplete = lib.get_incomplete_series()
for series_name, info in incomplete.items():
print(f"{series_name}: {info['read_count']}/{info['total_count']} read")
if info['next_book']:
print(f" Next: {info['next_book'].title}")
```
### Reading Statistics
```python
books_2024 = lib.get_books_read_in_year(2024)
pages_2024 = sum(b.num_pages or 0 for b in books_2024)
print(f"Books read in 2024: {len(books_2024)} ({pages_2024:,} pages)")
```
### Highly Rated Unread Books
```python
tbr = lib.get_tbr_books()
highly_rated = sorted(
[b for b in tbr if b.average_rating and b.average_rating >= 4.0],
key=lambda b: b.average_rating,
reverse=True
)
for book in highly_rated[:10]:
print(f"- {book.title} by {book.author} ({book.average_rating:.2f}⭐)")
```
## Important Notes
- The CSV is read-only - no modifications to the Goodreads library
- Series information is parsed from book titles (e.g., "Title (Series, #1)")
- Date Read determines if a book has been read
- Exclusive Shelf contains values like "to-read", "currently-reading", "mental-health", "favorites"
- Users may have custom shelves in the Bookshelves field
- Handle missing data gracefully (not all books have all fields)
- Always use proper Python error handling when accessing optional fields
## Troubleshooting
If you get import errors, ensure the script includes:
```python
import sys
sys.path.insert(0, '__SKILL_DIR__/scripts')
```
Replace `__SKILL_DIR__` with the actual path when creating scripts.
Important! You have a very serious bug, where you don't know how to find
the python scripts added by a skill. You must look in the "scripts"
folder of where this SKILL.md is located!!

View File

@@ -0,0 +1,243 @@
#!/usr/bin/env python3
"""Library for parsing and querying Goodreads CSV exports."""
import csv
import os
from datetime import datetime, timedelta
from pathlib import Path
from typing import List, Dict, Optional, Callable
import re
class GoodreadsBook:
"""Represents a book from Goodreads CSV export."""
def __init__(self, row: Dict[str, str]):
self.book_id = row.get('Book Id', '')
self.title = row.get('Title', '')
self.author = row.get('Author', '')
self.author_lf = row.get('Author l-f', '')
self.additional_authors = row.get('Additional Authors', '')
self.isbn = self._clean_isbn(row.get('ISBN', ''))
self.isbn13 = self._clean_isbn(row.get('ISBN13', ''))
self.my_rating = self._parse_int(row.get('My Rating', ''))
self.average_rating = self._parse_float(row.get('Average Rating', ''))
self.publisher = row.get('Publisher', '')
self.binding = row.get('Binding', '')
self.num_pages = self._parse_int(row.get('Number of Pages', ''))
self.year_published = self._parse_int(row.get('Year Published', ''))
self.original_publication_year = self._parse_int(row.get('Original Publication Year', ''))
self.date_read = self._parse_date(row.get('Date Read', ''))
self.date_added = self._parse_date(row.get('Date Added', ''))
self.bookshelves = row.get('Bookshelves', '')
self.bookshelves_with_positions = row.get('Bookshelves with positions', '')
self.exclusive_shelf = row.get('Exclusive Shelf', '')
self.my_review = row.get('My Review', '')
self.spoiler = row.get('Spoiler', '')
self.private_notes = row.get('Private Notes', '')
self.read_count = self._parse_int(row.get('Read Count', ''))
self.owned_copies = self._parse_int(row.get('Owned Copies', ''))
# Parse series information from title
self.series, self.series_index = self._parse_series()
def _clean_isbn(self, isbn: str) -> str:
"""Remove Excel formatting from ISBN."""
if isbn.startswith('="') and isbn.endswith('"'):
return isbn[2:-1]
return isbn
def _parse_int(self, value: str) -> Optional[int]:
"""Parse integer value, return None if empty or invalid."""
if not value or value == '':
return None
try:
return int(value)
except ValueError:
return None
def _parse_float(self, value: str) -> Optional[float]:
"""Parse float value, return None if empty or invalid."""
if not value or value == '':
return None
try:
return float(value)
except ValueError:
return None
def _parse_date(self, value: str) -> Optional[datetime]:
"""Parse date in YYYY/MM/DD format."""
if not value or value == '':
return None
try:
return datetime.strptime(value, '%Y/%m/%d')
except ValueError:
return None
def _parse_series(self) -> tuple[Optional[str], Optional[float]]:
"""Extract series name and number from title.
Examples:
- "An Absolutely Remarkable Thing (The Carls, #1)" -> ("The Carls", 1.0)
- "The Three-Body Problem (Remembrance of Earth's Past, #1)" -> ("Remembrance of Earth's Past", 1.0)
"""
# Match pattern: (Series Name, #Number)
match = re.search(r'\(([^,]+),\s*#([\d.]+)\)$', self.title)
if match:
series_name = match.group(1).strip()
try:
series_index = float(match.group(2))
return series_name, series_index
except ValueError:
return series_name, None
return None, None
@property
def is_read(self) -> bool:
"""Check if book has been read."""
return self.date_read is not None
@property
def is_tbr(self) -> bool:
"""Check if book is in to-be-read list."""
return 'to-read' in self.exclusive_shelf
@property
def is_currently_reading(self) -> bool:
"""Check if currently reading."""
return 'currently-reading' in self.exclusive_shelf
def has_shelf(self, shelf_name: str) -> bool:
"""Check if book is on a specific shelf."""
return shelf_name in self.bookshelves or shelf_name in self.exclusive_shelf
def __repr__(self):
return f"<GoodreadsBook: {self.title} by {self.author}>"
class GoodreadsLibrary:
"""Main class for querying Goodreads library from CSV."""
def __init__(self, csv_path: Optional[str] = None):
"""Initialize library from CSV file.
Args:
csv_path: Path to goodreads_library_export.csv
Defaults to ~/Drive/Claude/books/goodreads_library_export.csv
"""
if csv_path is None:
csv_path = os.path.expanduser('~/Drive/Claude/books/goodreads_library_export.csv')
self.csv_path = csv_path
self.books: List[GoodreadsBook] = []
self._load_books()
def _load_books(self):
"""Load books from CSV file."""
with open(self.csv_path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
self.books.append(GoodreadsBook(row))
def query(self, filter_func: Callable[[GoodreadsBook], bool]) -> List[GoodreadsBook]:
"""Query books with a custom filter function."""
return [book for book in self.books if filter_func(book)]
def get_read_books(self, limit: Optional[int] = None,
sort_by_date: bool = True) -> List[GoodreadsBook]:
"""Get all read books, optionally sorted by date read."""
books = [book for book in self.books if book.is_read]
if sort_by_date:
books.sort(key=lambda b: b.date_read or datetime.min, reverse=True)
if limit:
books = books[:limit]
return books
def get_tbr_books(self) -> List[GoodreadsBook]:
"""Get all to-be-read books."""
return [book for book in self.books if book.is_tbr]
def get_books_by_shelf(self, shelf_name: str) -> List[GoodreadsBook]:
"""Get all books on a specific shelf."""
return [book for book in self.books if book.has_shelf(shelf_name)]
def get_books_read_in_period(self, days: int) -> List[GoodreadsBook]:
"""Get books read in the last N days."""
cutoff = datetime.now() - timedelta(days=days)
return [book for book in self.books
if book.date_read and book.date_read >= cutoff]
def get_books_read_in_year(self, year: int) -> List[GoodreadsBook]:
"""Get books read in a specific year."""
return [book for book in self.books
if book.date_read and book.date_read.year == year]
def get_books_added_in_period(self, days: int) -> List[GoodreadsBook]:
"""Get books added to library in the last N days."""
cutoff = datetime.now() - timedelta(days=days)
return [book for book in self.books
if book.date_added and book.date_added >= cutoff]
def get_series_books(self, series_name: str) -> List[GoodreadsBook]:
"""Get all books in a series, sorted by series index."""
books = [book for book in self.books if book.series == series_name]
books.sort(key=lambda b: b.series_index or 0)
return books
def get_all_series(self) -> Dict[str, List[GoodreadsBook]]:
"""Get all series with their books."""
series_dict = {}
for book in self.books:
if book.series:
if book.series not in series_dict:
series_dict[book.series] = []
series_dict[book.series].append(book)
# Sort books within each series
for series in series_dict:
series_dict[series].sort(key=lambda b: b.series_index or 0)
return series_dict
def get_incomplete_series(self) -> Dict[str, Dict]:
"""Get series where at least one book is read but not all."""
all_series = self.get_all_series()
incomplete = {}
for series_name, books in all_series.items():
read_count = sum(1 for b in books if b.is_read)
total_count = len(books)
if read_count > 0 and read_count < total_count:
# Find next unread book
next_unread = None
for book in books:
if not book.is_read:
next_unread = book
break
incomplete[series_name] = {
'books': books,
'read_count': read_count,
'total_count': total_count,
'next_book': next_unread
}
return incomplete
def get_author_stats(self) -> List[tuple[str, int]]:
"""Get author statistics (author, book count) sorted by count."""
author_counts = {}
for book in self.books:
if book.is_read:
author_counts[book.author] = author_counts.get(book.author, 0) + 1
return sorted(author_counts.items(), key=lambda x: x[1], reverse=True)
def get_rating_distribution(self) -> Dict[int, int]:
"""Get distribution of user ratings."""
dist = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0}
for book in self.books:
if book.is_read and book.my_rating:
dist[book.my_rating] = dist.get(book.my_rating, 0) + 1
return dist