Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ build/
dist/
*.egg-info/
.eggs/

**/.env
repomix-output.xml
**/repomix-output.xml
2 changes: 2 additions & 0 deletions .vscode/ltex.dictionary.en-US.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
StoryGraph
storygraph-api
193 changes: 99 additions & 94 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,116 +1,121 @@
# Storygraph API
A python package to interact with and fetch data from the [StoryGraph](https://app.thestorygraph.com/) website.
# Unofficial StoryGraph API for Python

## Features
- **Book Details**: Fetch detailed information about a book using its unique ID.
- **Search**: Perform a book search on StoryGraph and retrieve the results.
- **Fetch User lists**:
- currently reading
- planning to read
- books read
An unofficial Python wrapper for The StoryGraph API, forked from [ym496/storygraph-api](https://github.com/ym496/storygraph-api).

This fork has been significantly refactored and enhanced to be more efficient, reliable, and feature-rich.

## Key Enhancements in This Fork

* **No More Selenium**: The original dependency on Selenium and a headless browser has been completely removed. This version uses the `requests` library directly for all API communication, resulting in a much lighter, faster, and more stable experience.
* **Expanded API Coverage**: Many new features have been added, including methods to:
* Fetch your reading progress.
* Get your read dates for a book.
* Retrieve all your journal entries or entries for a specific book.
* Get a book's AI-generated summary.
* Fetch a user's ID.
* **Modernized Codebase**: The code has been updated with type hints and a more robust project structure.
* **Cookie-Based Authentication**: Authentication is now handled by passing your browser's session cookies, which is a more reliable method than the previous implementation.

## Installation
```
pip install storygraph-api

```bash
pip install -r requirements.txt
```

## Getting Started
## Configuration

The API is divided into two components, `Books Client` and `User Client`.
This wrapper requires authentication for most features. You'll need to provide your StoryGraph session cookies and username.

### Book Details:
1. **Create a `.env` file** in the root of the project.
2. **Find your cookies**:
* Open your web browser and log in to [The StoryGraph](https://app.thestorygraph.com/).
* Open your browser's developer tools (usually by pressing F12).
* Go to the "Application" (in Chrome) or "Storage" (in Firefox) tab.
* Under the "Cookies" section for `app.thestorygraph.com`, find the values for `_storygraph_session` and `remember_user_token`.
3. **Add your credentials to the `.env` file**:

```python
# Books Client
# Fetch details of a book using its ID

from storygraph_api import Book
id = "fbdd6b7c-f512-47f2-aa94-d8bf0d5f5175"
book = Book()
result = book.book_info(id)
print(result)
```
#### Result:
```json
{
"title": "Hagakure: The Book of the Samurai",
"authors": [
"Yamamoto Tsunetomo",
"William Scott Wilson"
],
"pages": "179",
"first_pub": "1716",
"tags": [
"nonfiction",
"history",
"philosophy",
"informative",
"reflective",
"slow-paced"
],
"average_rating": "3.65",
"description": "<div><em>Hagakure<\\/em> (\\\"In the Shadow of Leaves\\\") is a manual for the samurai classes consisting of a series of short anecdotes and reflections that give both insight and instruction-in the philosophy and code of behavior that foster the true spirit of Bushido-the Way of the Warrior. It is not a book of philosophy as most would understand the word: it is a collection of thoughts and sayings recorded over a period of seven years, and as such covers a wide variety of subjects, often in no particular sequence. <br><br>The work represents an attitude far removed from our modern pragmatism and materialism, and possesses an intuitive rather than rational appeal in its assertion that Bushido is a Way of Dying, and that only a samurai retainer prepared and willing to die at any moment can be totally true to his lord. While <em>Hagakure<\\/em> was for many years a secret text known only to the warrior vassals of the Hizen fief to which the author belonged, it later came to be recognized as a classic exposition of samurai thought and came to influence many subsequent generations, including Yukio Mishima. <br><br>This translation offers 300 selections that constitute the core texts of the 1,300 present in the original. <br><em>Hagakure<\\/em> was featured prominently in the film <em>Ghost Dog<\\/em>, by Jim Jarmusch.<\\/div>",
"warnings": {
"graphic": [
"Suicide",
"Violence"
],
"moderate": [
"Suicide",
"Suicide attempt",
"War"
],
"minor": [
"Gore"
]
}
}
```
```dotenv
_STORYGRAPH_SESSION=your_session_cookie_value
REMEMBER_USER_TOKEN=your_remember_token_value
STORYGRAPH_USERNAME=your_storygraph_username
```

## Usage

### User List:
Here's a basic example of how to use the `Book` and `User` clients.

```python
# User Client
# works only for public profiles
# fetch user's currently reading list

from storygraph_api import User
import os
import json
from dotenv import load_dotenv
from storygraph_api import Book, User

# Load environment variables from .env file
load_dotenv()
cookie = os.getenv('COOKIE') # retrieve cookie from .env file
uname = 'sampleuname' #some username
user = User()
result = user.currently_reading(uname,cookie=cookie)
print(result)

```
# --- Authentication ---
username = os.getenv("STORYGRAPH_USERNAME")
session_cookie = os.getenv("_STORYGRAPH_SESSION")
remember_token = os.getenv("REMEMBER_USER_TOKEN")

#### Result:

```json
[
{
"title": "The Murder After the Night Before",
"book_id": "38cb5b56-23f1-48fd-b4b3-a80e07a19775"
},
{
"title": "The Graces",
"book_id": "653b54b3-a79d-4c2e-ae40-eae281a91315"
}
]
auth_cookies = {
"_storygraph_session": session_cookie,
"remember_user_token": remember_token
}

# --- Initialize Clients ---
book_client = Book()
user_client = User()

# --- User Client Examples ---

# Get user ID
user_id_json = user_client.get_user_id(username)
user_id = json.loads(user_id_json).get("user_id")
print(f"User ID: {user_id}")

# Get 'Currently Reading' list
currently_reading = user_client.currently_reading(username, auth_cookies)
print(currently_reading)

# Get 'To-Read' list
to_read = user_client.to_read(username, auth_cookies)
print(to_read)

```
# Get 'Read' list
books_read = user_client.books_read(username, auth_cookies)
print(books_read)

## Further Information
* Refer to [books_client.py](https://github.com/ym496/storygraph-api/tree/main/storygraph_api/books_client.py) and [users_client.py](https://github.com/ym496/storygraph-api/tree/main/storygraph_api/users_client.py) files to know more functionalities.
* All the user related tasks require the `remember_user_token` cookie. It can be found in the `Application` section of your browser’s developer tools for the StoryGraph website.
# --- Book Client Examples ---

## Contributing
Contributions are welcome! Fork the repository, make your changes, and submit a pull request.
book_id = "1c023e31-637b-41d9-ba64-260c3c1b0f3d" # Example book ID

For bugs or feature requests, please open an issue on [GitHub](https://github.com/ym496/storygraph-api/issues).
# Search for a book
search_results = book_client.search("Dune Frank Herbert")
print(search_results)

# Get book info
book_info = book_client.book_info(book_id)
print(book_info)

# Get your reading progress for a book
progress = book_client.reading_progress(book_id, auth_cookies)
print(progress)

# Get your read dates for a book
read_dates = book_client.get_read_dates(book_id, auth_cookies)
print(read_dates)

# Get your journal entries for a book
journal_entries = book_client.get_journal_entries(book_id, auth_cookies)
print(journal_entries)

# Get the AI summary for a book
if user_id:
ai_summary = book_client.get_ai_summary(book_id, user_id)
print(ai_summary)
```

## License
## Disclaimer

This project is licensed under the MIT License.
This is an unofficial wrapper. It is not affiliated with or endorsed by The StoryGraph. Use it at your own risk. The StoryGraph's website structure could change at any time, which might break this wrapper.
39 changes: 0 additions & 39 deletions manual_tests.py

This file was deleted.

13 changes: 1 addition & 12 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,20 +1,9 @@
attrs==24.2.0
beautifulsoup4==4.12.3
certifi==2024.7.4
charset-normalizer==3.3.2
exceptiongroup==1.2.2
h11==0.14.0
idna==3.7
outcome==1.3.0.post0
PySocks==1.7.1
python-dotenv==1.1.1
requests==2.32.3
selenium==4.23.1
sniffio==1.3.1
sortedcontainers==2.4.0
soupsieve==2.6
trio==0.26.2
trio-websocket==0.11.1
typing_extensions==4.12.2
urllib3==2.2.2
websocket-client==1.8.0
wsproto==1.2.0
30 changes: 26 additions & 4 deletions storygraph_api/books_client.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,36 @@
from storygraph_api.parse.books_parser import BooksParser
from storygraph_api.exception_handler import handle_exceptions
import json
from typing import Dict

class Book:
@handle_exceptions
def book_info(self,book_id):
def book_info(self, book_id: str) -> str:
data = BooksParser.book_page(book_id)
return json.dumps(data,indent=4)
return json.dumps(data, indent=4)

@handle_exceptions
def search(self,query):
def reading_progress(self, book_id: str, cookies: Dict[str, str]) -> str:
progress = BooksParser.reading_progress(book_id, cookies)
data = {"progress": progress}
return json.dumps(data, indent=4)

@handle_exceptions
def get_read_dates(self, book_id: str, cookies: Dict[str, str]) -> str:
data = BooksParser.get_read_dates(book_id, cookies)
return json.dumps(data, indent=4)

@handle_exceptions
def get_ai_summary(self, book_id: str, user_id: str) -> str:
data = BooksParser.get_ai_summary(book_id, user_id)
return json.dumps(data, indent=4)

@handle_exceptions
def get_journal_entries(self, book_id: str, cookies: Dict[str, str]) -> str:
data = BooksParser.journal_entries(book_id, cookies)
return json.dumps(data, indent=4)

@handle_exceptions
def search(self, query: str) -> str:
data = BooksParser.search(query)
return json.dumps(data,indent=4)
return json.dumps(data, indent=4)
20 changes: 9 additions & 11 deletions storygraph_api/exception_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
import requests
from functools import wraps
from storygraph_api.exceptions import RequestError, ParsingError, UnexpectedError
from selenium.common.exceptions import WebDriverException

def handle_exceptions(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except RequestError as e:
return json.dumps({"error": e.message}, indent=4)
except ParsingError as e:
except (RequestError, ParsingError) as e:
return json.dumps({"error": e.message}, indent=4)
except Exception as e:
raise UnexpectedError(f"Unexpected error: {str(e)}")
unexpected_error = UnexpectedError(f"An unexpected error occurred: {str(e)}")
return json.dumps({"error": unexpected_error.message}, indent=4)
return wrapper

def request_exception(func):
Expand All @@ -22,18 +22,16 @@ def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except requests.RequestException as e:
return json.dumps({"error": f"Scraping Error: {str(e)}"}, indent=4)
except Exception as e:
return json.dumps({"error": f"Scraping Error: {str(e)}"}, indent=4)
raise RequestError(f"A network error occurred: {str(e)}") from e
except WebDriverException as e:
raise RequestError(f"A browser automation error occurred: {str(e)}") from e
return wrapper

def parsing_exception(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except ParsingError as e:
return json.dumps({"error": e.message}, indent=4)
except Exception as e:
return json.dumps({"error": f"Parsing Error: {str(e)}"}, indent=4)
except (AttributeError, IndexError, TypeError, ValueError) as e:
raise ParsingError(f"Failed to parse page content. The website structure may have changed. Details: {str(e)}") from e
return wrapper
4 changes: 0 additions & 4 deletions storygraph_api/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
class StoryGraphAPIError(Exception):
"""Base class for exceptions in StoryGraphAPI"""
pass
class RequestError(StoryGraphAPIError):
"""Exception raised for errors during the request."""

def __init__(self, message="An error occurred during the request."):
self.message = message
super().__init__(self.message)

class ParsingError(StoryGraphAPIError):
"""Exception raised for errors during parsing responses."""

def __init__(self, message="An error occurred while parsing the response."):
self.message = message
super().__init__(self.message)
class UnexpectedError(StoryGraphAPIError):
"""Exception raised for unexpected errors."""

def __init__(self, message="An unexpected error occurred."):
self.message = message
Expand Down
Loading