Skip to content

Commit f84bcf4

Browse files
Refinements (#166)
- Re-add setting for filtering image formats - Re-add setting for handling pages in comicinfo - Refine logging - Refine settings usage - Tidy up bugs and fixes --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 0c663b9 commit f84bcf4

File tree

16 files changed

+233
-153
lines changed

16 files changed

+233
-153
lines changed

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
[![PyPI - Version](https://img.shields.io/pypi/v/Perdoo.svg?logo=PyPI&label=Version&style=flat-square)](https://pypi.python.org/pypi/Perdoo/)
66
[![PyPI - License](https://img.shields.io/pypi/l/Perdoo.svg?logo=PyPI&label=License&style=flat-square)](https://opensource.org/licenses/MIT)
77

8-
[![Pre-Commit](https://img.shields.io/badge/pre--commit-enabled-informational?logo=pre-commit&style=flat-square)](https://github.com/pre-commit/pre-commit)
8+
[![prek](https://img.shields.io/badge/prek-enabled-informational?logo=prek&style=flat-square)](https://github.com/j178/prek)
99
[![Ruff](https://img.shields.io/badge/ruff-enabled-informational?logo=ruff&style=flat-square)](https://github.com/astral-sh/ruff)
10+
[![Ty](https://img.shields.io/badge/ty-enabled-informational?logo=ruff&style=flat-square)](https://github.com/astral-sh/ty)
1011

11-
[![Github - Contributors](https://img.shields.io/github/contributors/Buried-In-Code/Perdoo.svg?logo=Github&label=Contributors&style=flat-square)](https://github.com/Buried-In-Code/Perdoo/graphs/contributors)
12-
[![Github Action - Testing](https://img.shields.io/github/actions/workflow/status/Buried-In-Code/Perdoo/testing.yaml?branch=main&logo=Github&label=Testing&style=flat-square)](https://github.com/Buried-In-Code/Perdoo/actions/workflows/testing.yaml)
13-
[![Github Action - Publishing](https://img.shields.io/github/actions/workflow/status/Buried-In-Code/Perdoo/publishing.yaml?branch=main&logo=Github&label=Publishing&style=flat-square)](https://github.com/Buried-In-Code/Perdoo/actions/workflows/publishing.yaml)
12+
[![Linting](https://github.com/Buried-In-Code/Perdoo/actions/workflows/linting.yaml/badge.svg)](https://github.com/Buried-In-Code/Perdoo/actions/workflows/linting.yaml)
13+
[![Testing](https://github.com/Buried-In-Code/Perdoo/actions/workflows/testing.yaml/badge.svg)](https://github.com/Buried-In-Code/Perdoo/actions/workflows/testing.yaml)
14+
[![Publishing](https://github.com/Buried-In-Code/Perdoo/actions/workflows/publishing.yaml/badge.svg)](https://github.com/Buried-In-Code/Perdoo/actions/workflows/publishing.yaml)
1415

1516
Perdoo is designed to assist in sorting and organizing your comic collection by utilizing metadata files stored within comic archives.\
1617
Perdoo standardizes all your digital comics into a unified format (cbz).\
@@ -135,6 +136,7 @@ File will be created on first run.
135136
[output]
136137
folder = "~/.local/share/perdoo"
137138
format = "cbz"
139+
image_extensions = [".png", ".jpg", ".jpeg", ".webp", ".jxl"]
138140

139141
[output.comic_info]
140142
create = true
@@ -179,6 +181,10 @@ password = "<Metron Password>"
179181
Defaults to `cbz`.
180182
Options are `cbz`, `cbt` or `cb7`
181183

184+
- `output.image_extensions`
185+
The list of extensions perdoo determines to be images as part of the cleanup step.
186+
Defaults to `[".png", ".jpg", ".jpeg", ".webp", ".jxl"]`
187+
182188
- `output.comic_info.create`
183189
Whether to create a ComicInfo.xml file in the output archive.
184190
Defaults to `true`.

docs/img/perdoo-commands.svg

Lines changed: 82 additions & 54 deletions
Loading

perdoo/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def setup_logging(debug: bool = False) -> None:
5858
omit_repeated_times=False,
5959
show_level=True,
6060
show_time=False,
61-
show_path=debug,
61+
show_path=True,
6262
console=CONSOLE,
6363
)
6464
console_handler.setLevel(logging.DEBUG if debug else logging.INFO)
@@ -72,4 +72,6 @@ def setup_logging(debug: bool = False) -> None:
7272
handlers=[console_handler, file_handler],
7373
)
7474

75-
logging.getLogger("PIL").setLevel(logging.WARNING)
75+
logging.getLogger("PIL").setLevel(logging.INFO if debug else logging.WARNING)
76+
logging.getLogger("httpx").setLevel(logging.INFO if debug else logging.WARNING)
77+
logging.getLogger("httpcore").setLevel(logging.INFO if debug else logging.WARNING)

perdoo/cli/process.py

Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
from datetime import date
33
from enum import Enum
4+
from io import BytesIO
45
from pathlib import Path
56
from platform import python_version
67
from typing import Annotated
@@ -14,10 +15,11 @@
1415
from perdoo.comic.archives import ArchiveSession
1516
from perdoo.comic.errors import ComicArchiveError, ComicMetadataError
1617
from perdoo.comic.metadata import ComicInfo, MetronInfo
18+
from perdoo.comic.metadata.comic_info import Page, PageType
1719
from perdoo.comic.metadata.metron_info import Id, InformationSource
1820
from perdoo.console import CONSOLE
1921
from perdoo.services import BaseService, Comicvine, Metron
20-
from perdoo.settings import Naming, Output, Service, Services, Settings
22+
from perdoo.settings import SETTINGS, Service
2123
from perdoo.utils import (
2224
IssueSearch,
2325
Search,
@@ -36,19 +38,19 @@ class SyncOption(str, Enum):
3638
SKIP = "Skip"
3739

3840

39-
def get_services(settings: Services) -> dict[Service, BaseService]:
41+
def get_services() -> dict[Service, BaseService]:
4042
output = {}
41-
if settings.comicvine.api_key:
42-
output[Service.COMICVINE] = Comicvine(api_key=settings.comicvine.api_key)
43-
if settings.metron.username and settings.metron.password:
43+
if SETTINGS.services.comicvine.api_key:
44+
output[Service.COMICVINE] = Comicvine(api_key=SETTINGS.services.comicvine.api_key)
45+
if SETTINGS.services.metron.username and SETTINGS.services.metron.password:
4446
output[Service.METRON] = Metron(
45-
username=settings.metron.username, password=settings.metron.password
47+
username=SETTINGS.services.metron.username, password=SETTINGS.services.metron.password
4648
)
4749
return output
4850

4951

5052
def setup_environment(
51-
clean_cache: bool, sync: SyncOption, settings: Settings, debug: bool = False
53+
clean_cache: bool, sync: SyncOption, debug: bool = False
5254
) -> tuple[dict[Service, BaseService], SyncOption]:
5355
setup_logging(debug=debug)
5456
LOGGER.info("Python v%s", python_version())
@@ -58,7 +60,7 @@ def setup_environment(
5860
LOGGER.info("Cleaning Cache")
5961
recursive_delete(path=get_cache_root())
6062

61-
services = get_services(settings=settings.services)
63+
services = get_services()
6264
if not services and sync is not SyncOption.SKIP:
6365
LOGGER.warning("No external services configured")
6466
sync = SyncOption.SKIP
@@ -76,9 +78,9 @@ def load_comics(target: Path) -> list[Comic]:
7678
return comics
7779

7880

79-
def prepare_comic(entry: Comic, settings: Settings, skip_convert: bool) -> bool:
81+
def prepare_comic(entry: Comic, skip_convert: bool) -> bool:
8082
if not skip_convert:
81-
entry.convert_to(settings.output.format)
83+
entry.convert_to(SETTINGS.output.format)
8284
if not entry.archive.IS_WRITEABLE:
8385
LOGGER.warning("Archive format %s is not writeable", entry.archive.EXTENSION)
8486
return False
@@ -163,40 +165,64 @@ def sync_metadata(
163165

164166

165167
def resolve_metadata(
166-
entry: Comic,
167-
session: ArchiveSession,
168-
services: dict[Service, BaseService],
169-
settings: Services,
170-
sync: SyncOption,
168+
entry: Comic, session: ArchiveSession, services: dict[Service, BaseService], sync: SyncOption
171169
) -> tuple[MetronInfo | None, ComicInfo | None]:
172170
metron_info, comic_info = entry.read_metadata(session=session)
173171
if not should_sync_metadata(sync=sync, metron_info=metron_info):
174172
return metron_info, comic_info
175173
search = build_search(
176174
metron_info=metron_info, comic_info=comic_info, filename=entry.filepath.stem
177175
)
178-
return sync_metadata(search=search, services=services, service_order=settings.order)
176+
return sync_metadata(search=search, services=services, service_order=SETTINGS.services.order)
179177

180178

181-
def generate_naming(
182-
settings: Naming, metron_info: MetronInfo | None, comic_info: ComicInfo | None
183-
) -> str | None:
179+
def generate_naming(metron_info: MetronInfo | None, comic_info: ComicInfo | None) -> str | None:
184180
filepath = None
185181
if metron_info:
186-
filepath = metron_info.get_filename(settings=settings)
182+
filepath = metron_info.get_filename()
187183
if not filepath and comic_info:
188-
filepath = comic_info.get_filename(settings=settings)
184+
filepath = comic_info.get_filename()
189185
return filepath.lstrip("/") if filepath else None
190186

191187

188+
def load_page_info(entry: Comic, session: ArchiveSession, comic_info: ComicInfo) -> None:
189+
from PIL import Image # noqa: PLC0415
190+
191+
pages = set()
192+
image_files = entry.list_images(image_extensions=SETTINGS.output.image_extensions)
193+
for idx, file in enumerate(image_files):
194+
page = next((x for x in comic_info.pages if x.image == idx), None)
195+
if page:
196+
page_type = page.type
197+
elif idx == 0:
198+
page_type = PageType.FRONT_COVER
199+
elif idx == len(image_files) - 1:
200+
page_type = PageType.BACK_COVER
201+
else:
202+
page_type = PageType.STORY
203+
if not page:
204+
page = Page(image=idx)
205+
page.type = page_type
206+
page_bytes = entry.read_file(session=session, filename=file.name)
207+
if not page_bytes:
208+
continue
209+
page.image_size = len(page_bytes)
210+
with Image.open(BytesIO(page_bytes)) as page_data:
211+
width, height = page_data.size
212+
page.double_page = width >= height
213+
page.image_height = height
214+
page.image_width = width
215+
pages.add(page)
216+
comic_info.pages = sorted(pages)
217+
218+
192219
def apply_changes(
193220
entry: Comic,
194221
session: ArchiveSession,
195222
metron_info: MetronInfo | None,
196223
comic_info: ComicInfo | None,
197224
skip_clean: bool,
198225
skip_rename: bool,
199-
settings: Output,
200226
) -> str | None:
201227
local_metron_info, local_comic_info = entry.read_metadata(session=session)
202228
if local_metron_info != metron_info:
@@ -205,23 +231,23 @@ def apply_changes(
205231
else:
206232
session.delete(filename=MetronInfo.FILENAME)
207233

234+
if comic_info and SETTINGS.output.comic_info.handle_pages:
235+
load_page_info(entry=entry, session=session, comic_info=comic_info)
208236
if local_comic_info != comic_info:
209237
if comic_info:
210238
session.write(filename=ComicInfo.FILENAME, data=comic_info.to_bytes())
211239
else:
212240
session.delete(filename=ComicInfo.FILENAME)
213241

214242
if not skip_clean:
215-
for extra in entry.list_extras():
243+
for extra in entry.list_extras(image_extensions=SETTINGS.output.image_extensions):
216244
session.delete(filename=extra.name)
217245

218246
naming = None
219247
if not skip_rename and (
220-
naming := generate_naming(
221-
settings=settings.naming, metron_info=metron_info, comic_info=comic_info
222-
)
248+
naming := generate_naming(metron_info=metron_info, comic_info=comic_info)
223249
):
224-
images = entry.list_images()
250+
images = entry.list_images(image_extensions=SETTINGS.output.image_extensions)
225251
stem = Path(naming).stem
226252
pad = len(str(len(images)))
227253
for idx, img in enumerate(images):
@@ -268,11 +294,7 @@ def process(
268294
bool, Option("--debug", help="Enable debug mode to show extra information.")
269295
] = False,
270296
) -> None:
271-
settings = Settings.load()
272-
settings.save()
273-
services, sync = setup_environment(
274-
clean_cache=clean_cache, sync=sync, settings=settings, debug=debug
275-
)
297+
services, sync = setup_environment(clean_cache=clean_cache, sync=sync, debug=debug)
276298

277299
comics = load_comics(target=target)
278300
total = len(comics)
@@ -281,15 +303,11 @@ def process(
281303
f"[{index}/{total}] Importing {entry.filepath.name}", align="left", style="subtitle"
282304
)
283305

284-
if not prepare_comic(entry=entry, settings=settings, skip_convert=skip_convert):
306+
if not prepare_comic(entry=entry, skip_convert=skip_convert):
285307
continue
286308
with entry.open_session() as session:
287309
metron_info, comic_info = resolve_metadata(
288-
entry=entry,
289-
session=session,
290-
services=services,
291-
settings=settings.services,
292-
sync=sync,
310+
entry=entry, session=session, services=services, sync=sync
293311
)
294312
naming = apply_changes(
295313
entry=entry,
@@ -298,10 +316,9 @@ def process(
298316
comic_info=comic_info,
299317
skip_clean=skip_clean,
300318
skip_rename=skip_rename,
301-
settings=settings.output,
302319
)
303320
if naming:
304-
entry.move_to(naming=naming, output_folder=settings.output.folder)
321+
entry.move_to(naming=naming, output_folder=SETTINGS.output.folder)
305322
with CONSOLE.status("Cleaning up empty folders"):
306323
delete_empty_folders(folder=target)
307324

perdoo/cli/settings.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
__all__ = []
22

33
from perdoo.cli._typer import app
4-
from perdoo.settings import Settings
4+
from perdoo.settings import SETTINGS
55

66

77
@app.command(help="Display app settings and defaults.")
88
def settings() -> None:
9-
settings = Settings.load()
10-
settings.display()
9+
SETTINGS.display()

perdoo/comic/comic.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__all__ = ["IMAGE_EXTENSIONS", "Comic"]
1+
__all__ = ["Comic"]
22

33
import logging
44
import shutil
@@ -13,7 +13,6 @@
1313
LOGGER = logging.getLogger(__name__)
1414

1515
METADATA_FILENAMES: Final[frozenset[str]] = frozenset([MetronInfo.FILENAME, ComicInfo.FILENAME])
16-
IMAGE_EXTENSIONS: Final[frozenset[str]] = frozenset([".png", ".jpg", ".jpeg", ".webp", ".jxl"])
1716

1817

1918
class Comic:
@@ -45,33 +44,43 @@ def read_metadata(self, session: ArchiveSession) -> tuple[MetronInfo | None, Com
4544
comic_info = ComicInfo.from_bytes(content=session.read(filename=ComicInfo.FILENAME))
4645
return metron_info, comic_info
4746

48-
def list_images(self) -> list[Path]:
47+
def read_file(self, session: ArchiveSession, filename: str) -> bytes | None:
48+
if session.contains(filename=filename):
49+
return session.read(filename=filename)
50+
return None
51+
52+
def list_images(self, image_extensions: tuple[str, ...]) -> list[Path]:
4953
return humansorted(
5054
[
5155
Path(name)
5256
for name in self.archive.list_filenames()
53-
if Path(name).suffix.lower() in IMAGE_EXTENSIONS
57+
if Path(name).suffix.lower() in image_extensions
5458
],
5559
alg=ns.NA | ns.G | ns.P,
5660
)
5761

58-
def list_extras(self) -> list[Path]:
62+
def list_extras(self, image_extensions: tuple[str, ...]) -> list[Path]:
5963
return humansorted(
6064
[
6165
Path(name)
6266
for name in self.archive.list_filenames()
6367
if name not in METADATA_FILENAMES
64-
and Path(name).suffix.lower() not in IMAGE_EXTENSIONS
68+
and Path(name).suffix.lower() not in image_extensions
6569
],
6670
alg=ns.NA | ns.G | ns.P,
6771
)
6872

69-
def validate_naming(self, naming: str) -> bool:
73+
def validate_naming(self, naming: str, image_extensions: tuple[str, ...]) -> bool:
7074
template = Path(naming).stem
71-
return all(img.name.startswith(template) for img in self.list_images())
75+
return all(
76+
img.name.startswith(template)
77+
for img in self.list_images(image_extensions=image_extensions)
78+
)
7279

7380
def move_to(self, naming: str, output_folder: Path) -> None:
7481
output = output_folder / (naming + self.archive.EXTENSION)
82+
if output == self.archive.filepath.resolve():
83+
return
7584
if output.exists():
7685
LOGGER.warning("'%s' already exists, skipping", output)
7786
return

perdoo/comic/metadata/_base.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from rich.panel import Panel
1414

1515
from perdoo.console import CONSOLE
16-
from perdoo.settings import Naming
1716
from perdoo.utils import flatten_dict
1817

1918
try:
@@ -52,7 +51,7 @@ class Metadata(PascalModel, ABC):
5251
FILENAME: ClassVar[str] = ""
5352

5453
@abstractmethod
55-
def get_filename(self, settings: Naming) -> str: ...
54+
def get_filename(self) -> str: ...
5655

5756
@classmethod
5857
def from_bytes(cls, content: bytes) -> Self:

perdoo/comic/metadata/comic_info.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from pydantic_xml import attr, computed_attr, element, wrapped
1212

1313
from perdoo.comic.metadata._base import Metadata, PascalModel
14-
from perdoo.settings import Naming
14+
from perdoo.settings import SETTINGS
1515

1616
LOGGER = logging.getLogger(__name__)
1717

@@ -280,9 +280,11 @@ def story_arc_list(self) -> list[str]:
280280
def story_arc_list(self, value: list[str]) -> None:
281281
self.story_arc = list_to_str(value=value)
282282

283-
def get_filename(self, settings: Naming) -> str:
283+
def get_filename(self) -> str:
284284
from perdoo.comic.metadata.metron_info import Format # noqa: PLC0415
285285

286+
settings = SETTINGS.output.naming
287+
286288
return self.evaluate_pattern(
287289
pattern_map=PATTERN_MAP,
288290
pattern={

0 commit comments

Comments
 (0)