feat(api): Use FastAPI Error handler

This commit is contained in:
TheOnlyWayUp
2024-11-28 18:23:52 +00:00
parent 308afde25f
commit f9e27689e3
3 changed files with 58 additions and 70 deletions
+1
View File
@@ -4,3 +4,4 @@ venv
data data
*ipynb *ipynb
build build
.vscode
-9
View File
@@ -111,9 +111,6 @@ async def retrieve_story(story_id: int, cookies: Optional[dict] = None) -> dict:
async with session.get( 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" 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: ) as response:
if not response.ok:
if response.status in [404, 400]:
return {}
response.raise_for_status() response.raise_for_status()
body = await response.json() 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( async with session.get(
f"https://www.wattpad.com/apiv2/?m=storytext&id={part_id}" f"https://www.wattpad.com/apiv2/?m=storytext&id={part_id}"
) as response: ) as response:
if not response.ok:
if response.status in [404, 400]:
return ""
response.raise_for_status() response.raise_for_status()
body = await response.text() 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) else ClientSession(headers=headers, cookies=cookies)
) as session: # Don't cache requests with Cookies. ) as session: # Don't cache requests with Cookies.
async with session.get(url) as response: async with session.get(url) as response:
if not response.ok:
if response.status in [404, 400]:
return bytes()
response.raise_for_status() response.raise_for_status()
body = await response.read() body = await response.read()
+57 -61
View File
@@ -1,8 +1,14 @@
"""WattpadDownloader API Server."""
from typing import Optional from typing import Optional
import tempfile
from pathlib import Path from pathlib import Path
from io import BytesIO
from enum import Enum 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.responses import FileResponse, HTMLResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
from ebooklib import epub from ebooklib import epub
from create_book import ( from create_book import (
retrieve_story, retrieve_story,
@@ -13,10 +19,6 @@ from create_book import (
wp_get_cookies, wp_get_cookies,
fetch_story_id, fetch_story_id,
) )
import tempfile
from io import BytesIO
from fastapi.staticfiles import StaticFiles
from aiohttp import ClientResponseError
app = FastAPI() app = FastAPI()
BUILD_PATH = Path(__file__).parent / "build" BUILD_PATH = Path(__file__).parent / "build"
@@ -37,6 +39,28 @@ def home():
return FileResponse(BUILD_PATH / "index.html") 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 <a href="https://discord.gg/P9RHC4KCwd" target="_blank">Discord</a>',
)
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 <a href="https://discord.gg/P9RHC4KCwd" target="_blank">Discord</a>',
)
case _:
# Unhandled error
return HTMLResponse(
status_code=500,
content='Something went wrong. Yell at me on the <a href="https://discord.gg/P9RHC4KCwd" target="_blank">Discord</a>',
)
@app.get("/download/{download_id}") @app.get("/download/{download_id}")
async def handle_download( async def handle_download(
download_id: int, download_id: int,
@@ -45,7 +69,6 @@ async def handle_download(
username: Optional[str] = None, username: Optional[str] = None,
password: Optional[str] = None, password: Optional[str] = None,
): ):
if username and not password or password and not username: if username and not password or password and not username:
return HTMLResponse( return HTMLResponse(
status_code=422, status_code=422,
@@ -64,69 +87,42 @@ async def handle_download(
else: else:
cookies = None cookies = None
try: match mode:
match mode: case DownloadMode.story:
case DownloadMode.story: story_id = download_id
story_id = download_id case DownloadMode.part:
case DownloadMode.part: story_id = await fetch_story_id(download_id, cookies)
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: metadata = await retrieve_story(story_id, cookies)
# Invalid ID set_metadata(book, metadata)
return HTMLResponse(
status_code=404,
content='The story you tried to download does not exist or has been deleted. Support is available on the <a href="https://discord.gg/P9RHC4KCwd" target="_blank">Discord</a>',
)
set_metadata(book, metadata) await set_cover(book, metadata, cookies=cookies)
await set_cover(book, metadata, cookies=cookies)
async for title in add_chapters( async for title in add_chapters(
book, metadata, download_images=download_images, cookies=cookies book, metadata, download_images=download_images, cookies=cookies
): ):
... ...
# Book is compiled # Book is compiled
temp_file = tempfile.NamedTemporaryFile( temp_file = tempfile.NamedTemporaryFile(
suffix=".epub", delete=True suffix=".epub", delete=True
) # Thanks https://stackoverflow.com/a/75398222 ) # Thanks https://stackoverflow.com/a/75398222
# create epub file # create epub file
epub.write_epub(temp_file, book, {}) epub.write_epub(temp_file, book, {})
temp_file.file.seek(0) temp_file.file.seek(0)
book_data = temp_file.file.read() book_data = temp_file.file.read()
return StreamingResponse( return StreamingResponse(
BytesIO(book_data), BytesIO(book_data),
media_type="application/epub+zip", media_type="application/epub+zip",
headers={ headers={
"Content-Disposition": f'attachment; filename="{slugify(metadata["title"])}_{story_id}_{"images" if download_images else ""}.epub"' # Thanks https://stackoverflow.com/a/72729058 "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 <a href="https://discord.gg/P9RHC4KCwd" target="_blank">Discord</a>',
)
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 <a href="https://discord.gg/P9RHC4KCwd" target="_blank">Discord</a>',
)
else:
# Unhandled error
return HTMLResponse(
status_code=500,
content='Something went wrong. Support is available on the <a href="https://discord.gg/P9RHC4KCwd" target="_blank">Discord</a>',
)
app.mount("/", StaticFiles(directory=BUILD_PATH), "static") app.mount("/", StaticFiles(directory=BUILD_PATH), "static")