13 Commits

Author SHA1 Message Date
TheOnlyWayUp dd32a03bf8 feat(frontend): Dark Mode 2024-09-03 17:22:05 +00:00
AaronBenDaniel 232795b050 fix(frontend): Download more button (#12 - @AaronBenDaniel)
* Fixed "Download More" button

* Revert "Fixed "Download More" button"

This reverts commit 620ad6afff.

* Reworked page reset

* fix(frontend): Download more button

---------

Co-authored-by: TheOnlyWayUp <hi@towu.dev>
2024-08-31 13:56:54 +05:30
TheOnlyWayUp 85bc4609c2 fix(frontend): Remove Query Params from ID-from-URL extraction 2024-07-11 15:28:45 +00:00
TheOnlyWayUp 3369325d03 fix(frontend): Populate download URL, accidentally removed 2024-07-10 14:06:06 +00:00
TheOnlyWayUp e16496ca94 fix(frontend): Reference @AaronBenDaniel's code for Story ID fetching from Part IDs 2024-07-09 15:29:19 +00:00
TheOnlyWayUp 3f9641d76a feat(frontend): Split pasted URLs to derive Story ID. Warn if Part ID 2024-07-09 15:08:46 +00:00
TheOnlyWayUp 868e02992b Update README 2024-07-08 12:59:49 +00:00
AaronBenDaniel 0184c786ce fix(frontend): URL Encode Username and Password (#9 - @AaronBenDaniel)
* add URI encoding to credentials

* chore(api): Comment on FastAPI's automatic URL Decode

---------

Co-authored-by: TheOnlyWayUp <hi@towu.dev>
2024-07-08 18:23:43 +05:30
TheOnlyWayUp b663448103 fix(frontend): Add todo for changelog on smaller screen sizes 2024-07-08 12:32:02 +00:00
AaronBenDaniel 0983c13da7 fix(api): Use HTML formatting consistently (#7 - @AaronBenDaniel) 2024-07-06 23:42:53 +05:30
TheOnlyWayUp 55763c1b99 fix(frontend): Update changelog 2024-06-30 20:13:52 +00:00
TheOnlyWayUp 9f24d437cb fix(frontend): Arabic language support 2024-06-30 20:09:07 +00:00
TheOnlyWayUp 79c9447cbe fix(frontend): Update changelog 2024-06-30 19:54:35 +00:00
10 changed files with 157 additions and 98 deletions
+1
View File
@@ -1,5 +1,6 @@
__pycache__ __pycache__
venv venv
*epub
data data
*ipynb *ipynb
build build
+1 -7
View File
@@ -1,9 +1,3 @@
{ {
"python.analysis.autoImportCompletions": true, "python.analysis.autoImportCompletions": true
"vscord.app.privacyMode.enable": false,
"python.testing.pytestArgs": [
"src"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
} }
+4 -3
View File
@@ -9,11 +9,12 @@ Stars ⭐ are appreciated. Thanks!
## Features ## Features
- ⚡ Lightweight Frontend and Minimal Javascript. - ⚡ Lightweight Frontend and Minimal Javascript.
- 🪙 Supports Authentication (Download paid stories from your account!)
- 🌐 API Support (Visit the `/docs` path on your instance for more.) - 🌐 API Support (Visit the `/docs` path on your instance for more.)
- 🐇 Fast Generation, Basic Ratelimit Handling. - 🐇 Fast Generation, Ratelimit Handling.
- 🐳 Docker Support - 🐳 Docker Support
- 🏷️ Generated EPUB File includes Metadata. (Dublin Core Spec) - 🏷️ Generated EPUB File includes Metadata. (Dublin Core Spec)
- 📖 Plays well with E-Readers. (Kindle Support if KOReader present) - 📖 Plays well with E-Readers. (Kindle Support if KOReader present, ReMarkable, KOBO, ...)
- 💻 Easily Hackable. Extend with ease. - 💻 Easily Hackable. Extend with ease.
@@ -31,5 +32,5 @@ My thanks to [aerkalov/ebooklib](https://github.com/aerkalov/ebooklib) for a fas
--- ---
<div align="center"> <div align="center">
<p>TheOnlyWayUp © 2023</p> <p>TheOnlyWayUp © 2024</p>
</div> </div>
View File
+4 -4
View File
@@ -174,7 +174,7 @@ async def add_chapters(
): ):
chapters = [] chapters = []
for part in data["parts"]: for cidx, part in enumerate(data["parts"]):
content = await fetch_part_content(part["id"], cookies=cookies) content = await fetch_part_content(part["id"], cookies=cookies)
title = part["title"] title = part["title"]
clean_title = slugify(title) clean_title = slugify(title)
@@ -182,7 +182,7 @@ async def add_chapters(
# Thanks https://eu17.proxysite.com/process.php?d=5VyWYcoQl%2BVF0BYOuOavtvjOloFUZz2BJ%2Fepiusk6Nz7PV%2B9i8rs7cFviGftrBNll%2B0a3qO7UiDkTt4qwCa0fDES&b=1 # Thanks https://eu17.proxysite.com/process.php?d=5VyWYcoQl%2BVF0BYOuOavtvjOloFUZz2BJ%2Fepiusk6Nz7PV%2B9i8rs7cFviGftrBNll%2B0a3qO7UiDkTt4qwCa0fDES&b=1
chapter = epub.EpubHtml( chapter = epub.EpubHtml(
title=title, title=title,
file_name=f"{clean_title}.xhtml", file_name=f"{cidx}.xhtml", # Used to be clean_title.xhtml, but that broke Arabic support as slugify turns arabic strings into '', leading to multiple files with the same name, breaking those chapters.
lang=data["language"]["name"], lang=data["language"]["name"],
) )
@@ -200,11 +200,11 @@ async def add_chapters(
img = epub.EpubImage( img = epub.EpubImage(
media_type="image/jpeg", media_type="image/jpeg",
content=await response.read(), content=await response.read(),
file_name=f"static/{clean_title}/{idx}.jpeg", file_name=f"static/{cidx}/{idx}.jpeg",
) )
book.add_item(img) book.add_item(img)
content = content.replace( content = content.replace(
str(image), f'<img src="static/{clean_title}/{idx}.jpeg"/>' str(image), f'<img src="static/{cidx}/{idx}.jpeg"/>'
) )
chapter.set_content(f"<h1>{title}</h1>" + content) chapter.set_content(f"<h1>{title}</h1>" + content)
+2 -1
View File
@@ -34,10 +34,11 @@ async def download_book(
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,
content='Include both the username _and_ password, or neither. Support is available on the <a href="https://discord.gg/P9RHC4KCwd" target="_blank">Discord</a>', content='Include both the username <u>and</u> password, or neither. Support is available on the <a href="https://discord.gg/P9RHC4KCwd" target="_blank">Discord</a>',
) )
if username and password: if username and password:
# username and password are URL-Encoded by the frontend. FastAPI automatically decodes them.
try: try:
cookies = await wp_get_cookies(username=username, password=password) cookies = await wp_get_cookies(username=username, password=password)
except ValueError: except ValueError:
View File
-31
View File
@@ -1,31 +0,0 @@
from .. import create_book
import pytest
STORY_ID = 372219540
@pytest.mark.asyncio
async def test_retrieve_story():
story_data = await create_book.retrieve_story(STORY_ID)
story_data.pop("modifyDate", None) # Subject to change
response = {
"id": "372219540",
"title": "WPD Test",
"createDate": "2024-07-02T15:29:13Z",
# "modifyDate": "2024-07-02T15:41:26Z",
"language": {"name": "English"},
"user": {"username": "KindaAssNgl"},
"description": "Testing story for WPD.",
"cover": r"https:\/\/img.wattpad.com\/cover\/372219540-256-k908955.jpg",
"completed": False,
"tags": ["testing", "towu", "wpd"],
"mature": False,
"url": r"https:\/\/www.wattpad.com\/story\/372219540-wpd-test",
"parts": [{"id": 1458516761, "title": "Ganesh"}],
"isPaywalled": False,
}
assert story_data == response
File diff suppressed because one or more lines are too long
+111 -49
View File
@@ -6,10 +6,12 @@
username: "", username: "",
password: "", password: "",
}; };
let after_download_page = false; let after_download_page = false;
let url = ""; let url = "";
let raw_story_id = "";
let is_part_id = false;
let button_disabled = false; let button_disabled = false;
$: button_disabled = $: button_disabled =
!story_id || !story_id ||
@@ -19,14 +21,42 @@
`/download/${story_id}?om=1` + `/download/${story_id}?om=1` +
(download_images ? "&download_images=true" : "") + (download_images ? "&download_images=true" : "") +
(is_paid_story (is_paid_story
? `&username=${credentials.username}&password=${credentials.password}` ? `&username=${encodeURIComponent(credentials.username)}&password=${encodeURIComponent(credentials.password)}`
: ""); : "");
$: {
is_part_id = false;
if (raw_story_id.includes("wattpad.com")) {
// Originally, I was going to call the Wattpad API (wattpad.com/api/v3/stories/${story_id}), but Wattpad kept blocking those requests. I suspect it has something to do with the Origin header, I wasn't able to remove it.
// In the future, if this is considered, it would be cool if we could derive the Story ID from a pasted Part URL. Refer to @AaronBenDaniel's https://github.com/AaronBenDaniel/WattpadDownloader/blob/49b29b245188149f2d24c0b1c59e4c7f90f289a9/src/api/src/create_book.py#L156 (https://www.wattpad.com/api/v3/story_parts/{part_id}?fields=url).
if (raw_story_id.includes("/story/")) {
// https://wattpad.com/story/237369078-wattpad-books-presents
story_id = raw_story_id.split("/story/")[1].split("-")[0].split("?")[0]; // removes tracking fields
raw_story_id = story_id;
} else if (raw_story_id.includes("/stories/")) {
// https://www.wattpad.com/api/v3/stories/237369078?fields=...
story_id = raw_story_id.split("/stories/")[1].split("?")[0]; // removes params
raw_story_id = story_id;
} else {
// https://www.wattpad.com/939051741-wattpad-books-presents-part-name
is_part_id = true;
raw_story_id = "";
story_id = "";
}
} else {
story_id = parseInt(raw_story_id) || ""; // parseInt returns NaN for undefined values.
raw_story_id = story_id;
}
}
</script> </script>
<slot></slot>
<div> <div>
<div class="hero min-h-screen"> <div class="hero min-h-screen">
<div <div
class="hero-content flex-col lg:flex-row-reverse bg-base-100/50 p-16 rounded shadow-sm" class="hero-content flex-col lg:flex-row-reverse light:bg-base-100/50 dark:bg-[hsl(39,20%,5%)]/30 p-16 rounded shadow-sm"
> >
{#if !after_download_page} {#if !after_download_page}
<div class="text-center lg:text-left lg:p-10"> <div class="text-center lg:text-left lg:p-10">
@@ -35,31 +65,51 @@
> >
Wattpad Downloader Wattpad Downloader
</h1> </h1>
<p class="pt-6 text-lg"> <p class="pt-4 text-xl dark:text-white">
Download your favourite books with a single click! Download your favourite books with a single click!
</p> </p>
<ul class="pt-4 list list-inside text-xl"> <ul
<li>06/24 - 🎉 Image Downloading!</li> class="pt-6 list list-inside text-xl dark:text-[hsl(133,13%,85%)] text-left"
>
<!-- TODO: 'max-lg: hidden' to hide on screen sizes smaller than lg. I'll do this when I figure out how to make this show up _below_ the card on smaller screen sizes. -->
<li>08 '24 - 🌙 Dark Mode</li>
<li>07 '24 - 🔡 RTL Language support! (Arabic, etc.)</li>
<li>06 '24 - 🔑 Authenticated Downloads!</li>
<li>06 '24 - 🖼️ Image Downloading!</li>
</ul> </ul>
</div> </div>
<div class="card shrink-0 w-full max-w-sm shadow-2xl bg-base-100"> <div
class="card shrink-0 w-full max-w-sm shadow-2xl light:bg-base-100 dark:bg-[hsl(133,15%,9%)] dark:shadow-[0px_0px_150px_-100px_hsl(39,100%,50%)]"
>
<form class="card-body"> <form class="card-body">
<div class="form-control"> <div class="form-control">
<input <input
type="number" type="text"
placeholder="Story ID" placeholder="Story ID"
class="input input-bordered" class="input input-bordered dark:text-white"
bind:value={story_id} class:input-warning={is_part_id}
bind:value={raw_story_id}
required required
name="story_id" name="story_id"
/> />
<label class="label" for="story_id"> <label class="label" for="story_id">
<button {#if is_part_id}
class="label-text link font-semibold" <p class="text-red-500">
onclick="StoryIDTutorialModal.showModal()" Refer to (<button
data-umami-event="StoryIDTutorialModal Open" class="link font-semibold"
>How to get a Story ID</button onclick="StoryIDTutorialModal.showModal()"
> data-umami-event="Part StoryIDTutorialModal Open"
>How to get a Story ID</button
>).
</p>
{:else}
<button
class="label-text link font-semibold"
onclick="StoryIDTutorialModal.showModal()"
data-umami-event="StoryIDTutorialModal Open"
>How to get a Story ID</button
>
{/if}
</label> </label>
<label class="cursor-pointer label"> <label class="cursor-pointer label">
<span class="label-text" <span class="label-text"
@@ -72,35 +122,38 @@
/> />
</label> </label>
{#if is_paid_story} {#if is_paid_story}
<label class="input input-bordered flex items-center gap-2"> <div class="grid grid-rows-2 gap-y-1">
Username <label class="input input-bordered flex items-center gap-2">
<input <span class="label-text">Username</span>
type="text" <input
class="grow" type="text"
name="username" class="grow dark:text-white in_input p-1 rounded"
placeholder="foxtail.chicken" name="username"
bind:value={credentials.username} placeholder="foxtail.chicken"
required bind:value={credentials.username}
/> required
</label> />
<label class="input input-bordered flex items-center gap-2"> </label>
Password <label class="input input-bordered flex items-center gap-2">
<input <span class="label-text">Password</span>
type="password" <input
class="grow" type="password"
placeholder="supersecretpassword" class="grow dark:text-white in_input p-1 rounded"
name="password" placeholder="supersecretpassword"
bind:value={credentials.password} name="password"
required bind:value={credentials.password}
/> required
</label> />
</label>
</div>
{/if} {/if}
</div> </div>
<div class="form-control mt-6"> <div class="form-control mt-6">
<a <a
class="btn btn-primary rounded-l-none" class="btn light:btn-primary dark:btn-accent"
class:btn-disabled={button_disabled} class:btn-disabled={button_disabled}
class:disabled={button_disabled}
data-umami-event="Download" data-umami-event="Download"
href={url} href={url}
on:click={() => (after_download_page = true)}>Download</a on:click={() => (after_download_page = true)}>Download</a
@@ -121,7 +174,7 @@
<button <button
data-feedback-fish data-feedback-fish
class="link pb-4" class="link pb-4 label-text"
data-umami-event="Feedback">Feedback</button data-umami-event="Feedback">Feedback</button
> >
</div> </div>
@@ -151,7 +204,13 @@
>, where we release features early and discuss updates. >, where we release features early and discuss updates.
</p> </p>
</div> </div>
<a href="/" class="btn btn-outline btn-lg mt-10">Download More</a> <button
on:click={() => {
after_download_page = false;
raw_story_id = "";
}}
class="btn btn-outline btn-lg mt-10">Download More</button
>
</div> </div>
{/if} {/if}
</div> </div>
@@ -161,26 +220,29 @@
<!-- Open the modal using ID.showModal() method --> <!-- Open the modal using ID.showModal() method -->
<dialog id="StoryIDTutorialModal" class="modal"> <dialog id="StoryIDTutorialModal" class="modal">
<div class="modal-box"> <div class="modal-box dark:bg-[hsl(133,15%,9%)] dark:text-white/80">
<form method="dialog"> <form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
>✕</button >✕</button
> >
</form> </form>
<h3 class="font-bold text-lg">Downloading a Story</h3> <h3 class="font-bold text-lg">Retrieving a Story ID</h3>
<ol class="list list-disc list-inside py-4 space-y-2"> <ol class="list list-disc list-inside py-4 space-y-4">
<li> <li>
Open the Story URL (For example, <span Open the Story URL, this page includes the story description and tags.
class="font-mono bg-slate-100 p-1" (For example, <span
class="font-mono bg-slate-100 dark:bg-[hsl(133,40%,90%)]/90 text-black p-[5px] rounded"
>wattpad.com/story/237369078-wattpad-books-presents</span >wattpad.com/story/237369078-wattpad-books-presents</span
>) >).
</li> </li>
<li> <li>
Copy the numbers after the <span class="font-mono bg-slate-100 p-1" Copy the numbers after the <span
class="font-mono bg-slate-100 dark:bg-[hsl(133,40%,90%)]/90 text-black p-[5px] rounded"
>/</span >/</span
> >
(In the example, that'd be, (In the example, that'd be,
<span class="font-mono bg-slate-100 p-1" <span
class="font-mono bg-slate-100 dark:bg-[hsl(133,40%,90%)]/90 text-black p-[5px] rounded"
>wattpad.com/story/<span class="bg-amber-200 p-1">237369078</span >wattpad.com/story/<span class="bg-amber-200 p-1">237369078</span
>-wattpad-books-presents</span >-wattpad-books-presents</span
>) >)