Skip to content

Feature/#19 list downloads#98

Open
AaronBenDaniel wants to merge 8 commits into
masterfrom
feature/#19-list-downloads
Open

Feature/#19 list downloads#98
AaronBenDaniel wants to merge 8 commits into
masterfrom
feature/#19-list-downloads

Conversation

@AaronBenDaniel

@AaronBenDaniel AaronBenDaniel commented Apr 24, 2026

Copy link
Copy Markdown
Collaborator

Closes #19

Summary by CodeRabbit

  • New Features
    • Download entire lists, user libraries, and archives as single ZIPs or individual files; supports list URLs and collection links.
    • Optional authentication to access personal collections; UI now offers paid/library/archive selection and conditional login fields.
  • Refactor
    • Centralized and improved download/streaming logic for single and bulk exports with clearer filenames and format handling.

@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b7bd7fe4-35ab-4ecc-95ef-272fb674249d

📥 Commits

Reviewing files that changed from the base of the PR and between 0dc4d3b and a56a558.

📒 Files selected for processing (3)
  • src/api/src/create_book/create_book.py
  • src/api/src/main.py
  • src/frontend/src/routes/+page.svelte
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/api/src/create_book/create_book.py
  • src/frontend/src/routes/+page.svelte
  • src/api/src/main.py

📝 Walkthrough

Walkthrough

Adds Wattpad list/archive/library download support: new List model, four async Wattpad helpers (fetch_list, fetch_archive, fetch_library, fetch_username), package re-exports, refactored download flow to produce single- or multi-story ZIPs, and frontend UI to select paid/list/library/archive modes with conditional auth.

Changes

List & Collection Download Flow

Layer / File(s) Summary
Data Shape
src/api/src/create_book/models.py
Adds List TypedDict: name: str, stories: list[Story].
Wattpad API Helpers
src/api/src/create_book/create_book.py
Adds async helpers with exponential backoff: fetch_list(list_id) -> List, fetch_archive(username, cookies) -> list[Story], fetch_library(username, cookies) -> list[Story], fetch_username(cookies) -> str. Archive/library paginate nextUrl and aggregate stories.
Package Exports
src/api/src/create_book/__init__.py
Re-exports the new helper functions and models (fetch_archive, fetch_library, fetch_list, fetch_username, Story, List).
Download Engine / Endpoint
src/api/src/main.py
Adds download_story(...) and download_many_stories(...); extends DownloadMode with list, archive, library; refactors handle_download() to use helpers, require cookies for archive/library, build per-mode filenames, and stream single-file or ZIP responses.
Frontend UI
src/frontend/src/routes/+page.svelte
Replaces paid-only checkbox with radio selector for paid/library/archive, adds list URL parsing, toggles ID input/credentials for special modes, remembers prior paid mode, and updates tutorial examples.

Sequence Diagram

sequenceDiagram
    participant User as Frontend User
    participant Frontend as Frontend UI
    participant Endpoint as Download Endpoint
    participant Wattpad as Wattpad API
    participant Generator as Generator
    participant Storage as In-Memory ZIP

    User->>Frontend: choose mode (list/archive/library) + submit (optional cookies)
    Frontend->>Endpoint: POST /download (mode, id?, cookies?)
    Endpoint->>Wattpad: fetch_list / fetch_archive / fetch_library / fetch_username
    Wattpad-->>Endpoint: list metadata / stories[]
    loop per story
        Endpoint->>Generator: download_story(story, download_images?, cookies?)
        Generator->>Wattpad: fetch cover & parts ZIP
        Wattpad-->>Generator: parts + images
        Generator->>Generator: build parts tree, generate EPUB/PDF
        Generator-->>Endpoint: BytesIO (story file) or error
        Endpoint->>Storage: write story file or _FAILURE.txt into ZIP
    end
    Endpoint->>Frontend: stream ZIP or single file (BytesIO)
    Frontend-->>User: file download
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through lists and archives bright,

Collected stories late at night,
Cookies tucked for secret doors,
Zipped them up in tidy stores,
Here’s a bundle — hop delight! 🥕📚

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature/#19 list downloads' clearly describes the main change—adding support for downloading Wattpad lists—and directly references the related issue.
Linked Issues check ✅ Passed The pull request successfully implements support for downloading Wattpad lists as required by issue #19, with new API functions, models, and UI controls to handle list downloads.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing list download functionality; no unrelated modifications were introduced beyond the stated objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/#19-list-downloads
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feature/#19-list-downloads

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (4)
src/api/src/main.py (2)

97-102: Tighten cookies annotation: Optional[dict], not dict.

cookies: dict = None says "dict" but the default is None, and both callers pass None in unauthenticated paths. Use Optional[dict] = None to match the rest of the module (fetch_story, fetch_story_content_zip, fetch_list).

🔧 Proposed fix
-async def download_story(
-    metadata: Story,
-    download_images: bool = False,
-    format: DownloadFormat = DownloadFormat.epub,
-    cookies: dict = None,
-) -> BytesIO:
+async def download_story(
+    metadata: Story,
+    download_images: bool = False,
+    format: DownloadFormat = DownloadFormat.epub,
+    cookies: Optional[dict] = None,
+) -> BytesIO:
...
-async def download_many_stories(
-    stories: list[Story],
-    download_images: bool = False,
-    format: DownloadFormat = DownloadFormat.epub,
-    cookies: dict = None,
-) -> BytesIO:
+async def download_many_stories(
+    stories: list[Story],
+    download_images: bool = False,
+    format: DownloadFormat = DownloadFormat.epub,
+    cookies: Optional[dict] = None,
+) -> BytesIO:

Also applies to: 171-176

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/src/main.py` around lines 97 - 102, The parameter annotation for
cookies should be Optional[dict] = None rather than dict = None; update the
function signature of download_story to use cookies: Optional[dict] = None and
make the same change for the other functions around lines 171–176 that currently
use cookies: dict = None (e.g., the fetch_* helpers noted in the review). Also
ensure Optional is imported from typing (add from typing import Optional if
missing) so the annotations are valid and consistent with the rest of the
module.

117-119: ZipFile is opened but never closed.

archive = ZipFile(story_zip, "r") keeps the zip file handle alive until GC runs. It's in-memory here so leakage is bounded, but a with block is cheap and matches the pattern used in download_many_stories.

♻️ Proposed fix
-        # Fetch parts archive
-        story_zip = await fetch_story_content_zip(metadata["id"], cookies)
-        archive = ZipFile(story_zip, "r")
-
-        # Transform part metadata into an easily-indexable dictionary
-        part_id_title_dictionary = {
-            str(part["id"]): part["title"] for part in metadata["parts"]
-        }
-
-        part_trees: list[BeautifulSoup] = []
-
-        for id in archive.namelist():
-            ...
-            part_trees.append(
-                clean_tree(
-                    part_id_title_dictionary[id],
-                    id,
-                    archive.read(id).decode("utf-8"),
-                )
-            )
+        # Fetch parts archive
+        story_zip = await fetch_story_content_zip(metadata["id"], cookies)
+        part_id_title_dictionary = {
+            str(part["id"]): part["title"] for part in metadata["parts"]
+        }
+        part_trees: list[BeautifulSoup] = []
+        with ZipFile(story_zip, "r") as archive:
+            for id in archive.namelist():
+                if id not in part_id_title_dictionary:
+                    continue
+                part_trees.append(
+                    clean_tree(
+                        part_id_title_dictionary[id],
+                        id,
+                        archive.read(id).decode("utf-8"),
+                    )
+                )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/src/main.py` around lines 117 - 119, The ZipFile opened in the block
that calls fetch_story_content_zip (where you assign archive =
ZipFile(story_zip, "r")) is not closed; replace that direct instantiation with a
context manager (use with ZipFile(story_zip, "r") as archive:) so the archive is
closed automatically after use, mirroring the pattern used in
download_many_stories and ensuring any operations that currently reference
archive are performed inside the with block.
src/api/src/create_book/models.py (1)

45-47: Optional: consider renaming List to avoid shadowing typing.List.

The new List TypedDict is syntactically fine here, but naming it List creates a namespace clash with typing.List (and with common mental models of the built-in). In create_book.py the annotation -> List(Story) was introduced at L151/L172 of the new code — very likely an attempt to "parameterize" this TypedDict like List[Story], which TypedDicts do not support. A less ambiguous name (e.g. StoryList, ListCollection) would make those downstream annotations harder to get wrong.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/src/create_book/models.py` around lines 45 - 47, The TypedDict class
named List shadows typing.List and leads to incorrect uses like the `->
List(Story)` annotation; rename the TypedDict (e.g., StoryList or
ListCollection) and update all references/usages (including the annotations in
create_book.py that currently use `List(Story)`) to either the new TypedDict
name or to the proper generic type `list[Story]` where a parameterized sequence
is intended; ensure imports and any type hints that referenced `List` (and the
symbols `Story` and the TypedDict class) are updated accordingly.
src/api/src/create_book/create_book.py (1)

150-189: Deduplicate fetch_archive and fetch_library.

Both functions are byte-for-byte identical aside from the endpoint path segment (archive vs library). Extracting the common paginator keeps behavior tweaks (caching, error handling, pagination key changes) in a single place.

♻️ Suggested shape
async def _fetch_user_collection(
    username: str, cookies: dict, collection: str
) -> list[Story]:
    with start_action(action_type=f"api_fetch_{collection}"):
        async with CachedSession(headers=headers, cookies=cookies, cache=None) as session:
            stories: list[Story] = []
            next_url = (
                f"https://www.wattpad.com/api/v3/users/{username}/{collection}"
                "?fields=stories(tags,id,title,createDate,modifyDate,language(name),"
                "description,completed,mature,url,isPaywalled,"
                "user(username,avatar,description),parts(id,title),cover,copyright)"
                "&limit=30"
            )
            while next_url:
                async with session.get(next_url) as response:
                    response.raise_for_status()
                    body = await response.json()
                    next_url = body.get("nextUrl")
                    stories.extend(body.get("stories") or [])
        return stories


`@backoff.on_exception`(backoff.expo, ClientResponseError, max_time=15)
async def fetch_archive(username: str, cookies: dict) -> list[Story]:
    return await _fetch_user_collection(username, cookies, "archive")


`@backoff.on_exception`(backoff.expo, ClientResponseError, max_time=15)
async def fetch_library(username: str, cookies: dict) -> list[Story]:
    return await _fetch_user_collection(username, cookies, "library")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/src/create_book/create_book.py` around lines 150 - 189, Both
fetch_archive and fetch_library are identical except for the endpoint segment;
extract the shared paginator into a helper like _fetch_user_collection(username,
cookies, collection) that uses
start_action(action_type=f"api_fetch_{collection}"), opens
CachedSession(headers=headers, cookies=cookies), builds the next_url for
f"https://www.wattpad.com/api/v3/users/{username}/{collection}" with the same
fields and &limit=30, loops while next_url, calls session.get(next_url),
response.raise_for_status(), body = await response.json(), sets next_url =
body.get("nextUrl") and extends stories with body.get("stories") or [], then
returns stories; keep the `@backoff.on_exception` decorator on fetch_archive and
fetch_library and have them simply return await _fetch_user_collection(username,
cookies, "archive") and "library" respectively.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/api/src/create_book/create_book.py`:
- Line 151: The return type annotations using the non-generic TypedDict name
"List" (e.g., in the function fetch_archive) are invalid; replace occurrences of
List(Story) with the proper generic builtin list type annotation list[Story]
(and similarly update any other functions around the same area using List(...),
e.g., the one reported at line ~172), then remove the unused List import from
the top of the file if it is no longer referenced; reference the fetch_archive
function name and the other reported annotated function(s) so you can locate and
update their signatures and imports accordingly.
- Around line 192-206: fetch_username is calling a fragile frontend data route
("/list?_data=routes/_currentReads.list") which can change; update
fetch_username to use a stable authenticated API (e.g., call a v3 endpoint like
"/api/v3/users?fields=username" or "/api/v3/internal/whoami" using the same
CachedSession and cookies) and parse the response for the username there, and
add a defensive fallback: use body.get("username") and raise/return a clear,
descriptive error if missing (so callers of fetch_username see an explicit
failure rather than a KeyError). If no stable API is available, implement an
alternative decode path from the token cookie as a fallback and log a clear
error when both methods fail.
- Around line 158-168: The pagination loop in the function that fetches Wattpad
stories uses body["stories"] which can KeyError on missing/empty pages; update
the loop in the async request block to use body.get("stories", []) and replace
the manual append loop with stories.extend(body.get("stories", [])) to simplify
and make it safe; leave the pagination handling (nextUrl = body.get("nextUrl"))
as-is but verify you update the block around the async with session.get(...) and
the variable names (stories, nextUrl) accordingly.
- Around line 150-168: In fetch_archive (and similarly in fetch_library and
fetch_username) update the CachedSession instantiation to disable shared caching
for authenticated requests by passing cache=None if cookies else cache; also fix
the return type annotations from the invalid List(Story) to Python typing
list[Story] (or from typing import List and use List[Story] consistently) so
function signatures are syntactically correct—locate the CachedSession(...)
calls and the type hints on fetch_archive/fetch_library/fetch_username to apply
these two changes.

In `@src/api/src/main.py`:
- Line 306: Fix the typo in the user-facing error strings: replace "Login
credenials required for archive downloads" with "Login credentials required for
archive downloads" (and the analogous string in the library branch) wherever the
literal "Login credenials" appears in main.py (update both occurrences
referenced in the diff/content attributes so the user-facing messages use
"credentials").
- Line 189: The filename assembly for file_name is adding an extra underscore
when download_images is false; update the construction so the "_images" segment
(including its leading underscore) is only appended when download_images is
true. Locate the file_name assignment that uses slugify(story['title']),
story['id'], the conditional {'images' if download_images else ''} and
DownloadFormat; change it to conditionally append f"_images" (or nothing) based
on download_images so you don't end up with a trailing underscore (match the
conditional style used in handle_download).
- Line 348: Replace the long inline f-string that builds the Content-Disposition
filename with a small helper function (e.g., build_download_filename(metadata,
mode, id_download, username, download_id, download_images, format, extension,
slugify)) that explicitly handles each case (DownloadMode.story/part:
"<slug>_<id>[_images].<epub|pdf>", DownloadMode.list:
"<name-slug>_<id>[_images]_<format>.zip", DownloadMode.archive/library:
"<username>_<archive|library>[_images]_<format>.zip"), use == for string
comparisons (not is) when checking extension == "zip", and return the final
filename to be used in the Content-Disposition header; update the call site
where slugify(...), metadata, mode, etc. were used to call this helper.
- Around line 171-194: download_many_stories currently awaits download_story for
each story without error handling so one failure aborts the whole ZIP; wrap the
per-story call inside a try/except around await download_story(story,
download_images, format, cookies) (and subsequent archive.writestr) to catch
exceptions, log the error via start_action/process logger with the story
id/title, append a short error entry to a local errors list, and continue to the
next story; after the loop, if errors is non-empty, write a human-readable
errors.txt into the ZipFile (e.g. using archive.writestr("errors.txt",
"\n".join(errors))) so the consumer sees which stories failed; ensure
output_buffer.seek(0) and return behavior remains the same (optionally decide to
raise if zero files were added).

In `@src/frontend/src/routes/`+page.svelte:
- Around line 238-270: These inputs are radio inputs but styled and handled like
checkboxes; fix by keeping radio semantics: add a fourth radio option with value
"" and label "Just a single story" to represent the default, change the input
classes from "checkbox checkbox-warning" to "radio radio-warning" for the three
existing options and the new default, and remove the "toggle" behavior that
clears specialSelector when clicking the already-selected option—update the
handlers so inputs simply set specialSelector (or use Svelte bind:group to bind
to the specialSelector variable) and remove logic in the toggle() function that
allows toggling off.
- Around line 92-98: The list URL branch currently leaves a trailing slug in the
ID because it only does input.split("?", 1)[0].split("/list/")[1]; update the
branch handling input.includes("/list/") to mirror the other URL branches by
further splitting on "-" and taking the first segment (e.g., use
.split("?",1)[0].split("/list/")[1].split("-",1)[0]) before calling
setInputAsValid; apply the same change for the other occurrence that parses list
URLs so setInputAsValid receives the numeric ID without any slug.
- Line 13: The JSDoc union type comment for the page type is missing a space
before the final empty-string member; update the comment string in the JSDoc
(the /** `@type` {...} */ line that currently reads {"story" | "part" | "list" |
"library" | "archive"| ""}) to include a space before the | for the empty string
so it becomes {"story" | "part" | "list" | "library" | "archive" | ""}.

---

Nitpick comments:
In `@src/api/src/create_book/create_book.py`:
- Around line 150-189: Both fetch_archive and fetch_library are identical except
for the endpoint segment; extract the shared paginator into a helper like
_fetch_user_collection(username, cookies, collection) that uses
start_action(action_type=f"api_fetch_{collection}"), opens
CachedSession(headers=headers, cookies=cookies), builds the next_url for
f"https://www.wattpad.com/api/v3/users/{username}/{collection}" with the same
fields and &limit=30, loops while next_url, calls session.get(next_url),
response.raise_for_status(), body = await response.json(), sets next_url =
body.get("nextUrl") and extends stories with body.get("stories") or [], then
returns stories; keep the `@backoff.on_exception` decorator on fetch_archive and
fetch_library and have them simply return await _fetch_user_collection(username,
cookies, "archive") and "library" respectively.

In `@src/api/src/create_book/models.py`:
- Around line 45-47: The TypedDict class named List shadows typing.List and
leads to incorrect uses like the `-> List(Story)` annotation; rename the
TypedDict (e.g., StoryList or ListCollection) and update all references/usages
(including the annotations in create_book.py that currently use `List(Story)`)
to either the new TypedDict name or to the proper generic type `list[Story]`
where a parameterized sequence is intended; ensure imports and any type hints
that referenced `List` (and the symbols `Story` and the TypedDict class) are
updated accordingly.

In `@src/api/src/main.py`:
- Around line 97-102: The parameter annotation for cookies should be
Optional[dict] = None rather than dict = None; update the function signature of
download_story to use cookies: Optional[dict] = None and make the same change
for the other functions around lines 171–176 that currently use cookies: dict =
None (e.g., the fetch_* helpers noted in the review). Also ensure Optional is
imported from typing (add from typing import Optional if missing) so the
annotations are valid and consistent with the rest of the module.
- Around line 117-119: The ZipFile opened in the block that calls
fetch_story_content_zip (where you assign archive = ZipFile(story_zip, "r")) is
not closed; replace that direct instantiation with a context manager (use with
ZipFile(story_zip, "r") as archive:) so the archive is closed automatically
after use, mirroring the pattern used in download_many_stories and ensuring any
operations that currently reference archive are performed inside the with block.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cd523920-6c55-4044-8d2c-9055497f4983

📥 Commits

Reviewing files that changed from the base of the PR and between 6c5a222 and 0dc4d3b.

📒 Files selected for processing (5)
  • src/api/src/create_book/__init__.py
  • src/api/src/create_book/create_book.py
  • src/api/src/create_book/models.py
  • src/api/src/main.py
  • src/frontend/src/routes/+page.svelte

Comment thread src/api/src/create_book/create_book.py
Comment thread src/api/src/create_book/create_book.py Outdated
Comment thread src/api/src/create_book/create_book.py
Comment thread src/api/src/create_book/create_book.py
Comment thread src/api/src/main.py
Comment thread src/api/src/main.py Outdated
Comment thread src/api/src/main.py Outdated
media_type=media_type,
headers={
"Content-Disposition": f'attachment; filename="{slugify(metadata["title"])}_{story_id}{"_images" if download_images else ""}.{format.value}"', # Thanks https://stackoverflow.com/a/72729058
"Content-Disposition": f'attachment; filename="{slugify(metadata["name" if mode==DownloadMode.list else "title"]) if id_download else (username+'_'+("archive" if mode is DownloadMode.archive else "library"))}{'_'+str(download_id) if id_download else ""}{"_images" if download_images else ""}{'_'+format.value if extension is "zip" else ""}.{extension}"', # Thanks https://stackoverflow.com/a/72729058

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Filename expression: is "zip" is wrong, and the whole line is hard to read/maintain.

Ruff F632 is correctly flagging extension is "zip"is compares identity, which happens to work here only because CPython interns short literals; this is not guaranteed behavior and should be ==. More broadly, this one-line f"" has three nested conditionals, an inline username + '_' + (...), and mixes escape conventions, which makes the supported filenames ambiguous:

  • DownloadMode.story / part: <slug>_<id>[_images].<epub|pdf>
  • DownloadMode.list: <name-slug>_<id>[_images]_<format>.zip
  • DownloadMode.archive / library: <username>_<archive|library>[_images]_<format>.zip

Please extract this into a small helper — it would also make the archive-vs-library branch explicit and remove the need for the is comparison.

🧹 Proposed refactor
-        file_size = output_buffer.getbuffer().nbytes
+        file_size = output_buffer.getbuffer().nbytes
+
+        if id_download:
+            name_part = slugify(
+                metadata["name" if mode == DownloadMode.list else "title"]
+            )
+            id_part = f"_{download_id}"
+        else:
+            collection = "archive" if mode == DownloadMode.archive else "library"
+            name_part = f"{username}_{collection}"
+            id_part = ""
+
+        images_part = "_images" if download_images else ""
+        format_part = f"_{format.value}" if extension == "zip" else ""
+        filename = f"{name_part}{id_part}{images_part}{format_part}.{extension}"

         return StreamingResponse(
             iterfile(file_size),
             media_type=media_type,
             headers={
-                "Content-Disposition": f'attachment; filename="{slugify(metadata["name" if mode==DownloadMode.list else "title"]) if id_download else (username+'_'+("archive" if mode is DownloadMode.archive else "library"))}{'_'+str(download_id) if id_download else ""}{"_images" if download_images else ""}{'_'+format.value if extension is "zip" else ""}.{extension}"',  # Thanks https://stackoverflow.com/a/72729058
+                "Content-Disposition": f'attachment; filename="{filename}"',
                 "Content-Length": str(file_size),
             },
         )
🧰 Tools
🪛 Ruff (0.15.11)

[error] 348-348: Use == to compare constant literals

Replace is with ==

(F632)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/src/main.py` at line 348, Replace the long inline f-string that
builds the Content-Disposition filename with a small helper function (e.g.,
build_download_filename(metadata, mode, id_download, username, download_id,
download_images, format, extension, slugify)) that explicitly handles each case
(DownloadMode.story/part: "<slug>_<id>[_images].<epub|pdf>", DownloadMode.list:
"<name-slug>_<id>[_images]_<format>.zip", DownloadMode.archive/library:
"<username>_<archive|library>[_images]_<format>.zip"), use == for string
comparisons (not is) when checking extension == "zip", and return the final
filename to be used in the Content-Disposition header; update the call site
where slugify(...), metadata, mode, etc. were used to call this helper.

Comment thread src/frontend/src/routes/+page.svelte Outdated
Comment thread src/frontend/src/routes/+page.svelte
Comment on lines 238 to 270
<label class="label cursor-pointer text-gray-800">
<span class="label-text">This is a Paid Story, and I've purchased it</span>
<span class="label-text">This contains paid content, and I've purchased it</span>
<input
type="checkbox"
type="radio"
name="specialSelector"
class="checkbox-warning checkbox shadow-md"
value="paid"
checked={specialSelector === "paid"}
onclick={() => toggle("paid")}
/>
</label>
<label class="label cursor-pointer text-gray-800">
<span class="label-text">I want to download my entire library</span>
<input
type="radio"
name="specialSelector"
class="checkbox-warning checkbox shadow-md"
value="library"
checked={specialSelector === "library"}
onclick={() => toggle("library")}
/>
</label>
<label class="label cursor-pointer text-gray-800">
<span class="label-text">I want to download my entire archive</span>
<input
type="radio"
name="specialSelector"
class="checkbox-warning checkbox shadow-md"
bind:checked={isPaidStory}
value="archive"
checked={specialSelector === "archive"}
onclick={() => toggle("archive")}
/>
</label>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

type="radio" inputs styled as checkboxes and manually toggled — inconsistent UX.

These three inputs are radios (they share name="specialSelector", so the browser will only allow one active at a time), but:

  1. They're styled with daisyUI's checkbox-warning checkbox classes, which render them visually as checkboxes.
  2. toggle() manually clears specialSelector when the already-selected option is clicked again, which radios otherwise can't do. The user has no keyboard-only way to unselect ("back to default") — tabbing to a radio and pressing Space won't toggle it off.

Pick one of the two semantics:

  • If users need an unselected state → switch to type="checkbox" (and enforce mutual exclusion in toggle()).
  • If they are mutually exclusive and must be in one state → add a 4th radio for the default (value="", "Just a single story"), remove the toggle-off behavior, and swap the classes to radio radio-warning.

Also applies to: 116-123

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/frontend/src/routes/`+page.svelte around lines 238 - 270, These inputs
are radio inputs but styled and handled like checkboxes; fix by keeping radio
semantics: add a fourth radio option with value "" and label "Just a single
story" to represent the default, change the input classes from "checkbox
checkbox-warning" to "radio radio-warning" for the three existing options and
the new default, and remove the "toggle" behavior that clears specialSelector
when clicking the already-selected option—update the handlers so inputs simply
set specialSelector (or use Svelte bind:group to bind to the specialSelector
variable) and remove logic in the toggle() function that allows toggling off.

@AaronBenDaniel
AaronBenDaniel force-pushed the feature/#19-list-downloads branch from cfe4f8a to c144a81 Compare April 24, 2026 01:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Download Lists

1 participant