Initial commit
This commit is contained in:
14
.claude-plugin/plugin.json
Normal file
14
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "goodreads",
|
||||
"description": "TODO: Add description",
|
||||
"version": "0.0.1",
|
||||
"author": {
|
||||
"name": "TODO: Add author"
|
||||
},
|
||||
"skills": [
|
||||
"./skills"
|
||||
],
|
||||
"commands": [
|
||||
"./commands"
|
||||
]
|
||||
}
|
||||
110
commands/next.md
Normal file
110
commands/next.md
Normal 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
85
commands/random.md
Normal 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
80
commands/series.md
Normal 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
141
commands/stats.md
Normal 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
125
commands/vibes.md
Normal 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
69
plugin.lock.json
Normal 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": []
|
||||
}
|
||||
}
|
||||
150
skills/analyze-goodreads-export/SKILL.md
Normal file
150
skills/analyze-goodreads-export/SKILL.md
Normal 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!!
|
||||
243
skills/analyze-goodreads-export/scripts/goodreads_lib.py
Normal file
243
skills/analyze-goodreads-export/scripts/goodreads_lib.py
Normal 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
|
||||
Reference in New Issue
Block a user