Skip to content

Commit 59f4b62

Browse files
committed
support for novelsave_sources v0.2.0
1 parent 4d0f3b4 commit 59f4b62

File tree

11 files changed

+80
-56
lines changed

11 files changed

+80
-56
lines changed

novelsave/cli/helpers/novel.py

+13-11
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def retrieve_novel_info(source_gateway: BaseSourceGateway, url: str, browser: st
4343
logger.info(f"Retrieving novel information ({url=})...")
4444
try:
4545
output = source_gateway.novel_by_url(url)
46-
except requests.ConnectionError as e:
46+
except requests.ConnectionError:
4747
raise NSError(f"Connection terminated unexpectedly; Make sure you are connected to the internet.")
4848

4949
return output
@@ -61,13 +61,14 @@ def create_novel(
6161
this includes chapter list and metadata.
6262
"""
6363
source_gateway = get_source_gateway(url)
64-
novel_dto, chapter_dtos, metadata_dtos = retrieve_novel_info(source_gateway, url, browser)
64+
novel_dto = retrieve_novel_info(source_gateway, url, browser)
6565

6666
novel = novel_service.insert_novel(novel_dto)
67-
novel_service.insert_chapters(novel, chapter_dtos)
68-
novel_service.insert_metadata(novel, metadata_dtos)
67+
novel_service.insert_chapters(novel, novel_dto.volumes)
68+
novel_service.insert_metadata(novel, novel_dto.metadata)
6969

70-
logger.info(f"Added new novel (id={novel.id}, title='{novel.title}', chapters={len(chapter_dtos)}').")
70+
chapters = [c for v in novel_dto.volumes for c in v.chapters]
71+
logger.info(f"Added new novel (id={novel.id}, title='{novel.title}', chapters={len(chapters)}').")
7172

7273
data_dir = path_service.novel_data_path(novel)
7374
if data_dir.exists():
@@ -87,13 +88,14 @@ def update_novel(
8788
logger.debug(f"Using primary url ({url=})")
8889

8990
source_gateway = get_source_gateway(url)
90-
novel_dto, chapter_dtos, metadata_dtos = retrieve_novel_info(source_gateway, url, browser)
91+
novel_dto = retrieve_novel_info(source_gateway, url, browser)
9192

9293
novel_service.update_novel(novel, novel_dto)
93-
novel_service.update_chapters(novel, chapter_dtos)
94-
novel_service.update_metadata(novel, metadata_dtos)
94+
novel_service.update_chapters(novel, novel_dto.volumes)
95+
novel_service.update_metadata(novel, novel_dto.metadata)
9596

96-
logger.info(f"Updated novel (id={novel.id}, title='{novel.title}', chapters={len(chapter_dtos)})")
97+
chapters = [c for v in novel_dto.volumes for c in v.chapters]
98+
logger.info(f"Updated novel (id={novel.id}, title='{novel.title}', chapters={len(chapters)})")
9799
return novel
98100

99101

@@ -147,7 +149,7 @@ def download_chapters(
147149
logger.debug(f"Using primary novel url ({url=}).")
148150

149151
source_gateway = get_source_gateway(url)
150-
thread_count = 1 or min(threads, os.cpu_count())
152+
thread_count = min(threads, os.cpu_count())
151153

152154
def download(dto: ChapterDTO):
153155
try:
@@ -238,7 +240,7 @@ def get_novel(
238240
if not novel:
239241
quote = "'" if is_url else ''
240242
logger.error(f"Novel not found ({'url' if is_url else 'id'}={quote}{id_or_url}{quote}).")
241-
raise ValueError()
243+
raise ValueError("Novel was not found.")
242244

243245
if not silent:
244246
logger.info(f"Acquired novel from database (id={novel.id}, title='{novel.title}').")

novelsave/core/dtos/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from .novel_dto import NovelDTO
2+
from .volume_dto import VolumeDTO
23
from .chapter_dto import ChapterDTO
34
from .metadata_dto import MetaDataDTO

novelsave/core/dtos/chapter_dto.py

-1
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,3 @@ class ChapterDTO:
99
title: str
1010
url: str
1111
content: Union[str, List[str]] = None
12-
volume: Tuple[int, str] = None

novelsave/core/dtos/novel_dto.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
1-
from dataclasses import dataclass
1+
from dataclasses import dataclass, field
22
from datetime import datetime
3-
from typing import Optional
3+
from typing import Optional, List
4+
5+
from .metadata_dto import MetaDataDTO
6+
from .volume_dto import VolumeDTO
47

58

69
@dataclass
710
class NovelDTO:
811
id: Optional[int]
912
title: str
1013
url: str
11-
author: str = '<Not specified>'
14+
author: str = None
1215
synopsis: str = None
1316

1417
thumbnail_url: str = None
1518
thumbnail_path: str = None
1619

1720
lang: str = 'en'
1821
last_updated: datetime = None
22+
23+
volumes: List[VolumeDTO] = field(default_factory=lambda: [])
24+
metadata: List[MetaDataDTO] = field(default_factory=lambda: [])

novelsave/core/dtos/volume_dto.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from dataclasses import dataclass, field
2+
from typing import List, Optional
3+
4+
from .chapter_dto import ChapterDTO
5+
6+
7+
@dataclass
8+
class VolumeDTO:
9+
id: Optional[int]
10+
index: int
11+
name: str
12+
13+
chapters: List[ChapterDTO] = field(default_factory=lambda: [])

novelsave/core/services/novel/base_novel_service.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from pathlib import Path
33
from typing import Optional, List, Dict
44

5-
from novelsave.core.dtos import NovelDTO, ChapterDTO, MetaDataDTO
5+
from novelsave.core.dtos import NovelDTO, ChapterDTO, MetaDataDTO, VolumeDTO
66
from novelsave.core.entities.novel import Novel, Chapter, Volume, MetaData, NovelUrl
77

88

@@ -53,7 +53,7 @@ def insert_novel(self, novel_dto: NovelDTO) -> Novel:
5353
"""insert a new novel into the database"""
5454

5555
@abstractmethod
56-
def insert_chapters(self, novel: Novel, chapter_dtos: List[ChapterDTO]):
56+
def insert_chapters(self, novel: Novel, volume_dtos: List[VolumeDTO]):
5757
"""insert new chapters into the database as well as their volumes"""
5858

5959
@abstractmethod
@@ -69,7 +69,7 @@ def update_novel(self, novel: Novel, novel_dto: NovelDTO):
6969
"""update existing novel with newer values"""
7070

7171
@abstractmethod
72-
def update_chapters(self, novel: Novel, chapter_dtos: List[ChapterDTO]):
72+
def update_chapters(self, novel: Novel, volume_dtos: List[VolumeDTO]):
7373
"""update and replace existing novel chapters and their corresponding volumes"""
7474

7575
@abstractmethod

novelsave/core/services/source/base_source_gateway.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def login(self, username: str, password: str):
3030
"""login to the source website, making available services or novels which might otherwise be absent"""
3131

3232
@abstractmethod
33-
def novel_by_url(self, url: str) -> Tuple[dtos.NovelDTO, List[dtos.ChapterDTO], List[dtos.MetaDataDTO]]:
33+
def novel_by_url(self, url: str) -> dtos.NovelDTO:
3434
"""scrape and parse a novel by its url"""
3535

3636
@abstractmethod

novelsave/services/novel/novel_service.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from sqlalchemy import delete, select, update
66
from sqlalchemy.orm import Session
77

8-
from novelsave.core.dtos import NovelDTO, ChapterDTO, MetaDataDTO
8+
from novelsave.core.dtos import NovelDTO, ChapterDTO, MetaDataDTO, VolumeDTO
99
from novelsave.core.entities.novel import Novel, NovelUrl, Chapter, Volume, MetaData
1010
from novelsave.core.services import BaseNovelService
1111
from novelsave.services import FileService
@@ -83,10 +83,10 @@ def insert_novel(self, novel_dto: NovelDTO) -> Novel:
8383
def insert_chapters(
8484
self,
8585
novel: Novel,
86-
chapter_dtos: List[ChapterDTO],
86+
volume_dtos: List[VolumeDTO],
8787
previous: Dict[str, Chapter] = None
8888
):
89-
volume_mapped_chapters = self.dto_adapter.volumes_from_chapter_dtos(novel, chapter_dtos)
89+
volume_mapped_chapters = self.dto_adapter.volumes_from_dto(novel, volume_dtos)
9090

9191
# add volumes
9292
self.session.add_all(volume_mapped_chapters.keys())
@@ -121,10 +121,10 @@ def set_thumbnail_asset(self, novel: Novel, r_path: Path):
121121
novel.thumbnail_path = str(r_path)
122122
self.session.commit()
123123

124-
def update_chapters(self, novel: Novel, chapter_dtos: List[ChapterDTO]):
124+
def update_chapters(self, novel: Novel, volume_dtos: List[VolumeDTO]):
125125
volumes = self.session.execute(select(Volume).where(Volume.novel_id == novel.id)).scalars().all()
126126
chapters = self.get_chapters(novel)
127-
volume_mapped_chapters = self.dto_adapter.volumes_from_chapter_dtos(novel, chapter_dtos)
127+
volume_mapped_chapters = self.dto_adapter.volumes_from_dto(novel, volume_dtos)
128128

129129
indexed_volumes = {v.index: v for v in volumes}
130130
volumes_to_add = []
@@ -220,7 +220,6 @@ def update_content(self, chapter_dto: ChapterDTO):
220220
self.session.commit()
221221

222222
def add_url(self, novel: Novel, url: str):
223-
# TODO strip trailing '/'
224223
if url in [novel_url.url for novel_url in self.get_urls(novel)]:
225224
raise ValueError(f"Url already exists in novel (id={novel.id}, title='{novel.title}', {url=}).")
226225

novelsave/services/source/source_gateway.py

+3-7
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,10 @@ def search(self, keyword: str):
3434
def login(self, username: str, password: str):
3535
self.source.login(username, password)
3636

37-
def novel_by_url(self, url: str) -> Tuple[dtos.NovelDTO, List[dtos.ChapterDTO], List[dtos.MetaDataDTO]]:
38-
novel, chapters, metadata = self.source.novel(url)
37+
def novel_by_url(self, url: str) -> dtos.NovelDTO:
38+
novel = self.source.novel(url)
3939

40-
internal_novel = self.source_adapter.novel_to_internal(novel)
41-
internal_chapters = [self.source_adapter.chapter_to_internal(c) for c in chapters]
42-
internal_metadata = [self.source_adapter.metadata_to_internal(m) for m in metadata]
43-
44-
return internal_novel, internal_chapters, internal_metadata
40+
return self.source_adapter.novel_to_internal(novel)
4541

4642
def update_chapter_content(self, chapter: dtos.ChapterDTO) -> dtos.ChapterDTO:
4743
source_chapter = self.source_adapter.chapter_to_external(chapter)

novelsave/utils/adapters/dto_adapter.py

+14-20
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import json
2+
from pathlib import Path
23
from typing import Tuple, List, Dict
34

4-
from novelsave.core.dtos import NovelDTO, ChapterDTO, MetaDataDTO
5+
from novelsave.core.dtos import NovelDTO, ChapterDTO, MetaDataDTO, VolumeDTO
56
from novelsave.core.entities.novel import Novel, NovelUrl, Chapter, Volume, MetaData
67

78

@@ -18,8 +19,8 @@ def novel_from_dto(self, novel_dto: NovelDTO) -> Tuple[Novel, NovelUrl]:
1819

1920
url = NovelUrl(
2021
# ensure trailing forward slash, it is preferred that a uri that isn't
21-
# a file end in a slash to signify an endpoint
22-
url=novel_dto.url.rstrip('/') + '/',
22+
# a file end in a slash to signify a path
23+
url=novel_dto.url.rstrip('/') + ('/' if Path(novel_dto.url).suffix else ''),
2324
)
2425

2526
return novel, url
@@ -33,23 +34,16 @@ def update_novel_from_dto(self, novel: Novel, novel_dto: NovelDTO) -> Novel:
3334

3435
return novel
3536

36-
def volumes_from_chapter_dtos(self, novel: Novel, chapter_dtos: List[ChapterDTO]) -> Dict[Volume, List[ChapterDTO]]:
37-
volumes = {}
38-
for dto in chapter_dtos:
39-
if dto.volume is None:
40-
try:
41-
volumes[-1][1].append(dto)
42-
except KeyError:
43-
volumes[-1] = (Volume(index=-1, name='_default', novel_id=novel.id), [dto])
44-
elif len(dto.volume) < 2:
45-
continue
46-
else:
47-
try:
48-
volumes[dto.volume[0]][1].append(dto)
49-
except KeyError:
50-
volumes[dto.volume[0]] = (Volume(index=dto.volume[0], name=dto.volume[1], novel_id=novel.id), [dto])
51-
52-
return {volume: chapters for volume_index, (volume, chapters) in volumes.items()}
37+
def volumes_from_dto(self, novel: Novel, volume_dtos: List[VolumeDTO]) -> Dict[Volume, List[ChapterDTO]]:
38+
return {
39+
Volume(
40+
id=dto.id,
41+
index=dto.index,
42+
name=dto.name,
43+
novel_id=novel.id,
44+
): dto.chapters
45+
for dto in volume_dtos
46+
}
5347

5448
def chapter_from_dto(self, volume: Volume, chapter_dto: ChapterDTO) -> Chapter:
5549
return Chapter(

novelsave/utils/adapters/source_adapter.py

+18-4
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@ def novel_to_internal(self, novel: sm.Novel) -> dtos.NovelDTO:
1111
arguments = {
1212
key: value
1313
for key, value in vars(novel).items()
14-
if key in {'title', 'url', 'author', 'synopsis', 'thumbnail_url', 'lang'}
14+
if key in {'title', 'url', 'author', 'thumbnail_url', 'lang'}
1515
}
1616

17-
return dtos.NovelDTO(id=None, **arguments)
17+
return dtos.NovelDTO(
18+
id=None,
19+
synopsis='\n'.join(novel.synopsis),
20+
volumes=[self.volume_to_internal(c) for c in novel.volumes],
21+
metadata=[self.metadata_to_internal(m) for m in novel.metadata],
22+
**arguments
23+
)
1824

1925
def novel_to_external(self, novel: dtos.NovelDTO) -> sm.Novel:
2026
"""convert novel from source to internal"""
@@ -27,6 +33,16 @@ def novel_to_external(self, novel: dtos.NovelDTO) -> sm.Novel:
2733

2834
return sm.Novel(**arguments)
2935

36+
def volume_to_internal(self, volume: sm.Volume) -> dtos.VolumeDTO:
37+
"""convert volume from source to internal"""
38+
39+
return dtos.VolumeDTO(
40+
id=None,
41+
index=volume.index,
42+
name=volume.name,
43+
chapters=[self.chapter_to_internal(c) for c in volume.chapters]
44+
)
45+
3046
def chapter_to_internal(self, chapter: sm.Chapter) -> dtos.ChapterDTO:
3147
"""convert chapter from source to internal"""
3248

@@ -35,7 +51,6 @@ def chapter_to_internal(self, chapter: sm.Chapter) -> dtos.ChapterDTO:
3551
title=chapter.title,
3652
url=chapter.url,
3753
content=chapter.paragraphs,
38-
volume=chapter.volume,
3954
)
4055

4156
def chapter_to_external(self, chapter: dtos.ChapterDTO) -> sm.Chapter:
@@ -46,7 +61,6 @@ def chapter_to_external(self, chapter: dtos.ChapterDTO) -> sm.Chapter:
4661
title=chapter.title,
4762
url=chapter.url,
4863
paragraphs=chapter.content,
49-
volume=chapter.volume,
5064
)
5165

5266
def metadata_to_internal(self, metadata: sm.Metadata) -> dtos.MetaDataDTO:

0 commit comments

Comments
 (0)