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