Compare commits
6 Commits
766fca2c39
...
930dbe9ba4
| Author | SHA1 | Date | |
|---|---|---|---|
| 930dbe9ba4 | |||
| c67ca0e1df | |||
| 3a5ea1d158 | |||
| 8117f0dbfe | |||
| 67fab3f9c6 | |||
| a19c944b6e |
@@ -85,7 +85,7 @@ class BookController(Controller):
|
||||
|
||||
"""
|
||||
|
||||
result = await books_service.create(data, library)
|
||||
result = await books_service.create_book(data, library)
|
||||
book = await books_service.get(result.id)
|
||||
return books_service.to_schema(book, schema_type=s.BookRead)
|
||||
|
||||
@@ -213,7 +213,7 @@ class BookController(Controller):
|
||||
Returns:
|
||||
The updated book as a BookRead schema.
|
||||
"""
|
||||
await books_service.update(book_id, data, library)
|
||||
await books_service.update_book(book_id, data, library)
|
||||
book = await books_service.get(book_id)
|
||||
return books_service.to_schema(book, schema_type=s.BookRead)
|
||||
|
||||
@@ -244,7 +244,7 @@ class BookController(Controller):
|
||||
The updated book as a BookRead schema.
|
||||
"""
|
||||
|
||||
await books_service.update(book_id, {"cover_image": data}, library)
|
||||
await books_service.update_book(book_id, {"cover_image": data}, library)
|
||||
updated_book = await books_service.get(book_id)
|
||||
return books_service.to_schema(updated_book, schema_type=s.BookRead)
|
||||
|
||||
@@ -386,7 +386,7 @@ class BookController(Controller):
|
||||
|
||||
"""
|
||||
|
||||
await books_service.delete(book_ids, library, delete_files=delete_files)
|
||||
await books_service.delete_books(book_ids, library, delete_files=delete_files)
|
||||
|
||||
@post(path="/progress/{book_id:int}")
|
||||
async def update_progress(
|
||||
|
||||
@@ -17,6 +17,7 @@ from advanced_alchemy.service import (
|
||||
SQLAlchemyAsyncRepositoryService,
|
||||
ModelDictT,
|
||||
is_dict,
|
||||
schema_dump
|
||||
)
|
||||
from advanced_alchemy.repository import SQLAlchemyAsyncRepository
|
||||
from advanced_alchemy.filters import CollectionFilter
|
||||
@@ -37,18 +38,14 @@ from chitai.database.models import (
|
||||
BookSeries,
|
||||
FileMetadata,
|
||||
Identifier,
|
||||
BookList,
|
||||
Library,
|
||||
)
|
||||
from chitai.database.models.book_progress import BookProgress
|
||||
from chitai.schemas.book import BooksCreateFromFiles
|
||||
from chitai.services.filesystem_library import BookPathGenerator
|
||||
from chitai.services.metadata_extractor import Extractor as MetadataExtractor
|
||||
from chitai.services.utils import (
|
||||
cleanup_empty_parent_directories,
|
||||
delete_directory,
|
||||
delete_file,
|
||||
is_empty,
|
||||
move_dir_contents,
|
||||
move_file,
|
||||
save_image,
|
||||
@@ -67,7 +64,7 @@ class BookService(SQLAlchemyAsyncRepositoryService[Book]):
|
||||
|
||||
|
||||
|
||||
async def create(self, data: ModelDictT[Book], library: Library, **kwargs) -> Book:
|
||||
async def create_book(self, data: ModelDictT[Book], library: Library, **kwargs) -> Book:
|
||||
"""
|
||||
Create a new book entity.
|
||||
|
||||
@@ -83,9 +80,7 @@ class BookService(SQLAlchemyAsyncRepositoryService[Book]):
|
||||
Returns:
|
||||
The created Book entity.
|
||||
"""
|
||||
if not is_dict(data):
|
||||
data = data.model_dump()
|
||||
|
||||
data = schema_dump(data)
|
||||
await self._parse_metadata_from_files(data)
|
||||
|
||||
await self._save_cover_image(data)
|
||||
@@ -126,7 +121,7 @@ class BookService(SQLAlchemyAsyncRepositoryService[Book]):
|
||||
books[filepath].append(file)
|
||||
|
||||
return [
|
||||
await self.create(
|
||||
await self.create_book(
|
||||
{"files": [file for file in files], "library_id": library.id},
|
||||
library,
|
||||
**kwargs,
|
||||
@@ -211,7 +206,7 @@ class BookService(SQLAlchemyAsyncRepositoryService[Book]):
|
||||
|
||||
return books
|
||||
|
||||
async def delete(
|
||||
async def delete_books(
|
||||
self,
|
||||
book_ids: list[int],
|
||||
library: Library,
|
||||
@@ -263,6 +258,9 @@ class BookService(SQLAlchemyAsyncRepositoryService[Book]):
|
||||
ValueError: If the file is missing or not found for the given book.
|
||||
"""
|
||||
book = await self.get(book_id)
|
||||
if book.path is None:
|
||||
raise ValueError("Cannot download file: book has no path")
|
||||
|
||||
for file in book.files:
|
||||
if file.id != file_id:
|
||||
continue
|
||||
@@ -309,7 +307,7 @@ class BookService(SQLAlchemyAsyncRepositoryService[Book]):
|
||||
break
|
||||
yield chunk
|
||||
|
||||
async def update(
|
||||
async def update_book(
|
||||
self, book_id: int, update_data: ModelDictT[Book], library: Library
|
||||
) -> Book:
|
||||
"""
|
||||
@@ -343,7 +341,8 @@ class BookService(SQLAlchemyAsyncRepositoryService[Book]):
|
||||
await self._save_cover_image(data)
|
||||
|
||||
# TODO: extract out into its own function _update_book_path
|
||||
# Check if file path must be updated
|
||||
# Check if file path must be updated (only for books with files)
|
||||
if book.path is not None:
|
||||
path_gen = BookPathGenerator(library.root_path)
|
||||
updated_path = path_gen.generate_path(book.to_dict() | data)
|
||||
if str(updated_path) != book.path:
|
||||
@@ -368,9 +367,9 @@ class BookService(SQLAlchemyAsyncRepositoryService[Book]):
|
||||
book = await self.get(book_id)
|
||||
data = book.to_dict()
|
||||
data["files"] = files
|
||||
await self._save_book_files(library, data)
|
||||
book.files.extend(data["files"])
|
||||
await self.update(book.id, {"files": [file for file in book.files]}, library)
|
||||
new_files = await self._save_book_files(library, data)
|
||||
book.files.extend(new_files)
|
||||
await self.update_book(book.id, {"files": [file for file in book.files]}, library)
|
||||
|
||||
async def remove_files(
|
||||
self, book_id: int, file_ids: list[int], delete_files: bool, library: Library
|
||||
@@ -388,7 +387,7 @@ class BookService(SQLAlchemyAsyncRepositoryService[Book]):
|
||||
"""
|
||||
book = await self.get_one(Book.id == book_id, Book.library_id == library.id)
|
||||
|
||||
if delete_files:
|
||||
if delete_files and book.path is not None:
|
||||
# TODO: Extract this out into its own function
|
||||
for file in (file for file in book.files if file.id in file_ids):
|
||||
full_path = Path(book.path) / Path(file.path)
|
||||
@@ -447,7 +446,7 @@ class BookService(SQLAlchemyAsyncRepositoryService[Book]):
|
||||
|
||||
return data
|
||||
|
||||
async def _populate_with_unique_relationships(self, data: ModelDictT[Book]):
|
||||
async def _populate_with_unique_relationships(self, data: ModelDictT[Book]) -> ModelDictT[Book]:
|
||||
"""
|
||||
Ensure relationship entities (authors, series, tags, etc.) are unique in the database.
|
||||
|
||||
@@ -509,7 +508,7 @@ class BookService(SQLAlchemyAsyncRepositoryService[Book]):
|
||||
|
||||
return model_data
|
||||
|
||||
async def _save_book_files(self, library: Library, data: dict) -> dict:
|
||||
async def _save_book_files(self, library: Library, data: dict) -> list[FileMetadata]:
|
||||
"""
|
||||
Save uploaded book files to the filesystem.
|
||||
|
||||
@@ -559,7 +558,7 @@ class BookService(SQLAlchemyAsyncRepositoryService[Book]):
|
||||
)
|
||||
|
||||
data["files"] = file_metadata
|
||||
return data
|
||||
return data["files"]
|
||||
|
||||
async def _parse_metadata_from_files(self, data: dict, root_path: Path | None = None) -> dict:
|
||||
"""
|
||||
|
||||
@@ -28,7 +28,7 @@ from pathlib import Path
|
||||
),
|
||||
(
|
||||
Path(
|
||||
"/home/patrick/projects/chitai-api/tests/data_files/On The Origin of Species By Means of Natural Selection - Charles Darwin.epub"
|
||||
"tests/data_files/On The Origin of Species By Means of Natural Selection - Charles Darwin.epub"
|
||||
),
|
||||
2,
|
||||
"On the Origin of Species By Means of Natural Selection / Or, the Preservation of Favoured Races in the Struggle for Life",
|
||||
@@ -107,7 +107,7 @@ async def test_upload_book_without_data(
|
||||
),
|
||||
(
|
||||
Path(
|
||||
"/home/patrick/projects/chitai-api/tests/data_files/On The Origin of Species By Means of Natural Selection - Charles Darwin.epub"
|
||||
"tests/data_files/On The Origin of Species By Means of Natural Selection - Charles Darwin.epub"
|
||||
),
|
||||
2,
|
||||
"On the Origin of Species By Means of Natural Selection",
|
||||
@@ -327,7 +327,7 @@ async def test_update_reading_progress(
|
||||
|
||||
# Update progress
|
||||
progress_data = {
|
||||
"progress": 0.5,
|
||||
"percentage": 0.5,
|
||||
}
|
||||
|
||||
response = await populated_authenticated_client.post(
|
||||
@@ -343,7 +343,7 @@ async def test_update_reading_progress(
|
||||
assert response.status_code == 200
|
||||
|
||||
book = response.json()
|
||||
assert book["progress"]["progress"] == 0.5
|
||||
assert book["progress"]["percentage"] == 0.5
|
||||
|
||||
|
||||
async def test_create_multiple_books_from_directory(
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
"""Tests for BookService"""
|
||||
|
||||
import pytest
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
import aiofiles.os as aios
|
||||
|
||||
from chitai.schemas import BookCreate
|
||||
@@ -29,16 +25,17 @@ class TestBookServiceCRUD:
|
||||
)
|
||||
|
||||
book = await books_service.to_model_on_create(book_data.model_dump())
|
||||
assert isinstance(book, m.Book)
|
||||
|
||||
# Add path manually as it won't be generated (not using the create function, but manually inserting into db)
|
||||
book.path = f"{test_library.root_path}/J.R.R Tolkien/The Fellowship of the Ring"
|
||||
await aios.makedirs(book.path)
|
||||
await aios.makedirs(book.path) # type: ignore[arg-type]
|
||||
|
||||
books_service.repository.session.add(book)
|
||||
await books_service.repository.session.commit()
|
||||
await books_service.repository.session.refresh(book)
|
||||
|
||||
await books_service.update(
|
||||
await books_service.update_book(
|
||||
book.id,
|
||||
{
|
||||
"title": "The Fellowship of the Ring",
|
||||
@@ -66,9 +63,7 @@ class TestBookServiceCRUD:
|
||||
assert updated_book.identifiers[0].value == "9780261102354"
|
||||
|
||||
assert updated_book.edition == 3
|
||||
assert updated_book.publisher is not None
|
||||
assert updated_book.publisher.name == "Tolkien Estate"
|
||||
|
||||
assert len(updated_book.tags) == 2
|
||||
|
||||
|
||||
# book = await books_service.create(book_data.model_dump())
|
||||
|
||||
@@ -14,42 +14,42 @@
|
||||
"lint": "prettier --check . && eslint ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.4.0",
|
||||
"@eslint/js": "^9.38.0",
|
||||
"@iconify/svelte": "^5.0.2",
|
||||
"@internationalized/date": "^3.10.0",
|
||||
"@eslint/compat": "^1.4.1",
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@iconify/svelte": "^5.2.1",
|
||||
"@internationalized/date": "^3.12.0",
|
||||
"@lucide/svelte": "^0.544.0",
|
||||
"@sveltejs/adapter-node": "^5.4.0",
|
||||
"@sveltejs/kit": "^2.48.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@types/node": "^22.18.12",
|
||||
"bits-ui": "^2.14.0",
|
||||
"@sveltejs/adapter-node": "^5.5.4",
|
||||
"@sveltejs/kit": "^2.53.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@types/node": "^22.19.15",
|
||||
"bits-ui": "^2.16.3",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint": "^9.38.0",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.12.5",
|
||||
"globals": "^16.4.0",
|
||||
"jsrepo": "^2.5.0",
|
||||
"openapi-typescript": "^7.10.1",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"eslint-plugin-svelte": "^3.15.0",
|
||||
"globals": "^16.5.0",
|
||||
"jsrepo": "^2.5.2",
|
||||
"openapi-typescript": "^7.13.0",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-svelte": "^3.5.1",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"svelte": "^5.41.0",
|
||||
"svelte-check": "^4.3.3",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"svelte": "^5.53.7",
|
||||
"svelte-check": "^4.4.5",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
"tailwind-variants": "^3.1.1",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.2",
|
||||
"vite": "^7.1.12"
|
||||
"typescript-eslint": "^8.56.1",
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"epubjs": "^0.3.93",
|
||||
"mode-watcher": "^1.1.0",
|
||||
"svelte-sonner": "^1.0.5",
|
||||
"zod": "^4.1.12"
|
||||
"svelte-sonner": "^1.0.8",
|
||||
"zod": "^4.3.6"
|
||||
}
|
||||
}
|
||||
|
||||
2499
frontend/pnpm-lock.yaml
generated
2499
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
import { command, form, getRequestEvent, query } from '$app/server';
|
||||
import { loginSchema, signupSchema } from '$lib/schema/auth';
|
||||
import { BACKEND_API_URL } from '$lib/server/config';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { invalid, redirect } from '@sveltejs/kit';
|
||||
|
||||
export const login = form(loginSchema, async (data, invalid) => {
|
||||
export const login = form(loginSchema, async (data, issue) => {
|
||||
const { cookies, locals } = getRequestEvent();
|
||||
|
||||
// Create URL-encoded form data
|
||||
@@ -19,11 +19,11 @@ export const login = form(loginSchema, async (data, invalid) => {
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
invalid(invalid.email('Invalid login credentials'));
|
||||
invalid(issue.email('Invalid login credentials'));
|
||||
} else {
|
||||
const message = await response.text();
|
||||
console.error('Unknown error: ', message);
|
||||
invalid(invalid.email('An unknown error occurred'));
|
||||
invalid(issue.email('An unknown error occurred'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export const login = form(loginSchema, async (data, invalid) => {
|
||||
redirect(303, '/');
|
||||
});
|
||||
|
||||
export const signup = form(signupSchema, async (data, invalid) => {
|
||||
export const signup = form(signupSchema, async (data, issue) => {
|
||||
const response = await fetch(`${BACKEND_API_URL}/access/signup`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
@@ -49,11 +49,11 @@ export const signup = form(signupSchema, async (data, invalid) => {
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status == 409) {
|
||||
invalid(invalid.email('Email is already in use by another account'));
|
||||
invalid(issue.email('Email is already in use by another account'));
|
||||
} else {
|
||||
const message = await response.text();
|
||||
console.error('Unknown error: ', message);
|
||||
invalid(invalid.email('An unknown error occurred'));
|
||||
invalid(issue.email('An unknown error occurred'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user