From d58a119c10aca63dec7c7e3c3d83ba9e4a7cdee7 Mon Sep 17 00:00:00 2001
From: AaronBenDaniel <144371000+AaronBenDaniel@users.noreply.github.com>
Date: Fri, 8 Nov 2024 17:43:11 -0500
Subject: [PATCH 1/4] feat(api): Invalid ID error message
---
src/api/src/main.py | 64 +++++++++++++++++++++++++--------------------
1 file changed, 36 insertions(+), 28 deletions(-)
diff --git a/src/api/src/main.py b/src/api/src/main.py
index d9242f5..0c8076a 100644
--- a/src/api/src/main.py
+++ b/src/api/src/main.py
@@ -63,41 +63,49 @@ async def handle_download(
else:
cookies = None
- match mode:
- case DownloadMode.story:
- story_id = download_id
- case DownloadMode.part:
- story_id = await fetch_story_id(download_id, cookies)
+ try:
+ match mode:
+ case DownloadMode.story:
+ story_id = download_id
+ case DownloadMode.part:
+ story_id = await fetch_story_id(download_id, cookies)
- metadata = await retrieve_story(story_id, cookies)
- book = epub.EpubBook()
+ metadata = await retrieve_story(story_id, cookies)
+ book = epub.EpubBook()
- set_metadata(book, metadata)
- await set_cover(book, metadata, cookies=cookies)
+ set_metadata(book, metadata)
+ await set_cover(book, metadata, cookies=cookies)
- async for title in add_chapters(
- book, metadata, download_images=download_images, cookies=cookies
- ):
- ...
+ async for title in add_chapters(
+ book, metadata, download_images=download_images, cookies=cookies
+ ):
+ ...
- # Book is compiled
- temp_file = tempfile.NamedTemporaryFile(
- suffix=".epub", delete=True
- ) # Thanks https://stackoverflow.com/a/75398222
+ # Book is compiled
+ temp_file = tempfile.NamedTemporaryFile(
+ suffix=".epub", delete=True
+ ) # Thanks https://stackoverflow.com/a/75398222
- # create epub file
- epub.write_epub(temp_file, book, {})
+ # create epub file
+ epub.write_epub(temp_file, book, {})
- temp_file.file.seek(0)
- book_data = temp_file.file.read()
+ temp_file.file.seek(0)
+ book_data = temp_file.file.read()
- return StreamingResponse(
- BytesIO(book_data),
- media_type="application/epub+zip",
- headers={
- "Content-Disposition": f'attachment; filename="{slugify(metadata["title"])}_{story_id}_{"images" if download_images else ""}.epub"' # Thanks https://stackoverflow.com/a/72729058
- },
- )
+ return StreamingResponse(
+ BytesIO(book_data),
+ media_type="application/epub+zip",
+ headers={
+ "Content-Disposition": f'attachment; filename="{slugify(metadata["title"])}_{story_id}_{"images" if download_images else ""}.epub"' # Thanks https://stackoverflow.com/a/72729058
+ },
+ )
+
+ except KeyError:
+ # Invalid ID
+ return HTMLResponse(
+ status_code=404,
+ content='The story you tried to download does not exist or has been deleted. Support is available on the Discord',
+ )
app.mount("/", StaticFiles(directory=BUILD_PATH), "static")
From fa1bac30458999e2466b92fc29d546a677e92189 Mon Sep 17 00:00:00 2001
From: AaronBenDaniel <144371000+AaronBenDaniel@users.noreply.github.com>
Date: Sat, 9 Nov 2024 14:39:21 -0500
Subject: [PATCH 2/4] feat(api): Add rate-limiting error message
---
src/api/src/main.py | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/src/api/src/main.py b/src/api/src/main.py
index 0c8076a..cca7941 100644
--- a/src/api/src/main.py
+++ b/src/api/src/main.py
@@ -16,6 +16,7 @@ from create_book import (
import tempfile
from io import BytesIO
from fastapi.staticfiles import StaticFiles
+from aiohttp import ClientResponseError
app = FastAPI()
BUILD_PATH = Path(__file__).parent / "build"
@@ -107,6 +108,13 @@ async def handle_download(
content='The story you tried to download does not exist or has been deleted. Support is available on the Discord',
)
+ except ClientResponseError:
+ # Rate-limit by Wattpad
+ return HTMLResponse(
+ status_code=429,
+ content='Unfortunately, the downloader got rate-limited by Wattpad. Please try again later. Support is available on the Discord',
+ )
+
app.mount("/", StaticFiles(directory=BUILD_PATH), "static")
From 308afde25fd684cc23df549661ff681ae6c9bac7 Mon Sep 17 00:00:00 2001
From: AaronBenDaniel <144371000+AaronBenDaniel@users.noreply.github.com>
Date: Sun, 24 Nov 2024 21:42:07 -0500
Subject: [PATCH 3/4] fix(api): Handle invalid part IDs
---
src/api/src/main.py | 39 ++++++++++++++++++++++++++-------------
1 file changed, 26 insertions(+), 13 deletions(-)
diff --git a/src/api/src/main.py b/src/api/src/main.py
index cca7941..21347d5 100644
--- a/src/api/src/main.py
+++ b/src/api/src/main.py
@@ -74,6 +74,13 @@ async def handle_download(
metadata = await retrieve_story(story_id, cookies)
book = epub.EpubBook()
+ if not metadata:
+ # Invalid ID
+ return HTMLResponse(
+ status_code=404,
+ content='The story you tried to download does not exist or has been deleted. Support is available on the Discord',
+ )
+
set_metadata(book, metadata)
await set_cover(book, metadata, cookies=cookies)
@@ -101,19 +108,25 @@ async def handle_download(
},
)
- except KeyError:
- # Invalid ID
- return HTMLResponse(
- status_code=404,
- content='The story you tried to download does not exist or has been deleted. Support is available on the Discord',
- )
-
- except ClientResponseError:
- # Rate-limit by Wattpad
- return HTMLResponse(
- status_code=429,
- content='Unfortunately, the downloader got rate-limited by Wattpad. Please try again later. Support is available on the Discord',
- )
+ except ClientResponseError as error:
+ if error.status == 429:
+ # Rate-limit by Wattpad
+ return HTMLResponse(
+ status_code=429,
+ content='Unfortunately, the downloader got rate-limited by Wattpad. Please try again later. Support is available on the Discord',
+ )
+ elif error.status == 400:
+ # Invalid ID
+ return HTMLResponse(
+ status_code=404,
+ content='The story you tried to download does not exist or has been deleted. Support is available on the Discord',
+ )
+ else:
+ # Unhandled error
+ return HTMLResponse(
+ status_code=500,
+ content='Something went wrong. Support is available on the Discord',
+ )
app.mount("/", StaticFiles(directory=BUILD_PATH), "static")
From f9e27689e3818e4de354b963a9b69778229ffbc7 Mon Sep 17 00:00:00 2001
From: TheOnlyWayUp
Date: Thu, 28 Nov 2024 18:23:52 +0000
Subject: [PATCH 4/4] feat(api): Use FastAPI Error handler
---
.gitignore | 1 +
src/api/src/create_book.py | 9 ---
src/api/src/main.py | 118 ++++++++++++++++++-------------------
3 files changed, 58 insertions(+), 70 deletions(-)
diff --git a/.gitignore b/.gitignore
index 1c01200..20ecf1f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ venv
data
*ipynb
build
+.vscode
diff --git a/src/api/src/create_book.py b/src/api/src/create_book.py
index 1d8d75d..fb6c6c5 100644
--- a/src/api/src/create_book.py
+++ b/src/api/src/create_book.py
@@ -111,9 +111,6 @@ async def retrieve_story(story_id: int, cookies: Optional[dict] = None) -> dict:
async with session.get(
f"https://www.wattpad.com/api/v3/stories/{story_id}?fields=tags,id,title,createDate,modifyDate,language(name),description,completed,mature,url,isPaywalled,user(username),parts(id,title),cover"
) as response:
- if not response.ok:
- if response.status in [404, 400]:
- return {}
response.raise_for_status()
body = await response.json()
@@ -132,9 +129,6 @@ async def fetch_part_content(part_id: int, cookies: Optional[dict] = None) -> st
async with session.get(
f"https://www.wattpad.com/apiv2/?m=storytext&id={part_id}"
) as response:
- if not response.ok:
- if response.status in [404, 400]:
- return ""
response.raise_for_status()
body = await response.text()
@@ -151,9 +145,6 @@ async def fetch_cover(url: str, cookies: Optional[dict] = None) -> bytes:
else ClientSession(headers=headers, cookies=cookies)
) as session: # Don't cache requests with Cookies.
async with session.get(url) as response:
- if not response.ok:
- if response.status in [404, 400]:
- return bytes()
response.raise_for_status()
body = await response.read()
diff --git a/src/api/src/main.py b/src/api/src/main.py
index 21347d5..f1129d6 100644
--- a/src/api/src/main.py
+++ b/src/api/src/main.py
@@ -1,8 +1,14 @@
+"""WattpadDownloader API Server."""
+
from typing import Optional
+import tempfile
from pathlib import Path
+from io import BytesIO
from enum import Enum
-from fastapi import FastAPI
+from aiohttp import ClientResponseError
+from fastapi import FastAPI, Request
from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
+from fastapi.staticfiles import StaticFiles
from ebooklib import epub
from create_book import (
retrieve_story,
@@ -13,10 +19,6 @@ from create_book import (
wp_get_cookies,
fetch_story_id,
)
-import tempfile
-from io import BytesIO
-from fastapi.staticfiles import StaticFiles
-from aiohttp import ClientResponseError
app = FastAPI()
BUILD_PATH = Path(__file__).parent / "build"
@@ -37,6 +39,28 @@ def home():
return FileResponse(BUILD_PATH / "index.html")
+@app.exception_handler(ClientResponseError)
+def download_error_handler(request: Request, exception: ClientResponseError):
+ match exception.status:
+ case 400 | 404:
+ return HTMLResponse(
+ status_code=404,
+ content='This story does not exist, or has been deleted. Support is available on the Discord',
+ )
+ case 429:
+ # Rate-limit by Wattpad
+ return HTMLResponse(
+ status_code=429,
+ content='The website is overloaded. Please try again in a few minutes. Support is available on the Discord',
+ )
+ case _:
+ # Unhandled error
+ return HTMLResponse(
+ status_code=500,
+ content='Something went wrong. Yell at me on the Discord',
+ )
+
+
@app.get("/download/{download_id}")
async def handle_download(
download_id: int,
@@ -45,7 +69,6 @@ async def handle_download(
username: Optional[str] = None,
password: Optional[str] = None,
):
-
if username and not password or password and not username:
return HTMLResponse(
status_code=422,
@@ -64,69 +87,42 @@ async def handle_download(
else:
cookies = None
- try:
- match mode:
- case DownloadMode.story:
- story_id = download_id
- case DownloadMode.part:
- story_id = await fetch_story_id(download_id, cookies)
+ match mode:
+ case DownloadMode.story:
+ story_id = download_id
+ case DownloadMode.part:
+ story_id = await fetch_story_id(download_id, cookies)
- metadata = await retrieve_story(story_id, cookies)
- book = epub.EpubBook()
+ book = epub.EpubBook()
- if not metadata:
- # Invalid ID
- return HTMLResponse(
- status_code=404,
- content='The story you tried to download does not exist or has been deleted. Support is available on the Discord',
- )
+ metadata = await retrieve_story(story_id, cookies)
+ set_metadata(book, metadata)
- set_metadata(book, metadata)
- await set_cover(book, metadata, cookies=cookies)
+ await set_cover(book, metadata, cookies=cookies)
- async for title in add_chapters(
- book, metadata, download_images=download_images, cookies=cookies
- ):
- ...
+ async for title in add_chapters(
+ book, metadata, download_images=download_images, cookies=cookies
+ ):
+ ...
- # Book is compiled
- temp_file = tempfile.NamedTemporaryFile(
- suffix=".epub", delete=True
- ) # Thanks https://stackoverflow.com/a/75398222
+ # Book is compiled
+ temp_file = tempfile.NamedTemporaryFile(
+ suffix=".epub", delete=True
+ ) # Thanks https://stackoverflow.com/a/75398222
- # create epub file
- epub.write_epub(temp_file, book, {})
+ # create epub file
+ epub.write_epub(temp_file, book, {})
- temp_file.file.seek(0)
- book_data = temp_file.file.read()
+ temp_file.file.seek(0)
+ book_data = temp_file.file.read()
- return StreamingResponse(
- BytesIO(book_data),
- media_type="application/epub+zip",
- headers={
- "Content-Disposition": f'attachment; filename="{slugify(metadata["title"])}_{story_id}_{"images" if download_images else ""}.epub"' # Thanks https://stackoverflow.com/a/72729058
- },
- )
-
- except ClientResponseError as error:
- if error.status == 429:
- # Rate-limit by Wattpad
- return HTMLResponse(
- status_code=429,
- content='Unfortunately, the downloader got rate-limited by Wattpad. Please try again later. Support is available on the Discord',
- )
- elif error.status == 400:
- # Invalid ID
- return HTMLResponse(
- status_code=404,
- content='The story you tried to download does not exist or has been deleted. Support is available on the Discord',
- )
- else:
- # Unhandled error
- return HTMLResponse(
- status_code=500,
- content='Something went wrong. Support is available on the Discord',
- )
+ return StreamingResponse(
+ BytesIO(book_data),
+ media_type="application/epub+zip",
+ headers={
+ "Content-Disposition": f'attachment; filename="{slugify(metadata["title"])}_{story_id}_{"images" if download_images else ""}.epub"' # Thanks https://stackoverflow.com/a/72729058
+ },
+ )
app.mount("/", StaticFiles(directory=BUILD_PATH), "static")