1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124 | """Unparse comic filenames."""
from __future__ import annotations
from calendar import month_abbr
from collections.abc import Callable, Mapping, Sequence
from contextlib import suppress
from types import MappingProxyType
from comicfn2dict.log import print_log_header
def issue_formatter(issue: str) -> str:
"""Formatter to zero pad issues."""
i = 0
issue = issue.lstrip("0")
for c in issue:
if not c.isdigit():
break
i += 1
pad = 3 + len(issue) - i
return "#{:0>" + str(pad) + "}"
_PAREN_FMT: str = "({})"
_FILENAME_FORMAT_TAGS: tuple[tuple[str, str | Callable], ...] = (
("series", "{}"),
("volume", "v{}"),
("volume_count", "(of {:03})"),
("issue", issue_formatter),
("issue_count", "(of {:03})"),
("date", _PAREN_FMT),
("title", "{}"),
("publisher", _PAREN_FMT),
("original_format", _PAREN_FMT),
("scan_info", _PAREN_FMT),
)
_EMPTY_VALUES: tuple[None, str] = (None, "")
_DEFAULT_EXT = "cbz"
_DATE_KEYS = ("year", "month", "day")
class ComicFilenameSerializer:
"""Serialize Comic Filenames from dict."""
def _log(self, label: str, fn: str) -> None:
"""Log progress."""
if not self._debug:
return
print_log_header(label)
print(fn) # noqa: T201
def _add_date(self) -> None:
"""Construct date from Y-m-D if they exist."""
if "date" in self.metadata:
return
parts = []
for key in _DATE_KEYS:
if part := self.metadata.get(key):
if key == "month" and not parts:
with suppress(TypeError):
part = month_abbr[int(part)]
parts.append(part)
if key == "month" and not parts:
# noop if only day.
break
if parts:
parts = (str(part) for part in parts)
date = "-".join(parts)
self._log("After date", date)
self.metadata = MappingProxyType({**self.metadata, "date": date})
def _tokenize_tag(self, tag: str, fmt: str | Callable[[str], str]) -> str:
"""Add tags to the string."""
val = self.metadata.get(tag)
if val in _EMPTY_VALUES:
return ""
final_fmt = fmt if isinstance(fmt, str) else fmt(val)
return final_fmt.format(val).strip()
def _add_remainder(self) -> str:
"""Add the remainders specially."""
if remainders := self.metadata.get("remainders"):
if isinstance(remainders, Sequence):
remainders = (str(remainder) for remainder in remainders)
remainder = " ".join(remainders)
else:
remainder = str(remainders)
return f"[{remainder}]"
return ""
def serialize(self) -> str:
"""Get our preferred basename from a metadata dict."""
self._add_date()
tokens = []
for tag, fmt in _FILENAME_FORMAT_TAGS:
if token := self._tokenize_tag(tag, fmt):
tokens.append(token)
self._log(f"After {tag}", str(tokens))
fn = " ".join(tokens)
fn += self._add_remainder()
self._log("After remainder", fn)
if self._ext:
ext = self.metadata.get("ext", _DEFAULT_EXT)
fn += f".{ext}"
self._log("After ext", fn)
return fn
def __init__(self, metadata: Mapping, ext: bool = True, verbose: int = 0) -> None: # noqa: FBT001,FBT002
"""Initialize."""
self.metadata: Mapping = metadata
self._ext: bool = ext
self._debug: bool = bool(verbose)
def dict2comicfn(md: Mapping, ext: bool = True, verbose: int = 0) -> str: # noqa: FBT001,FBT002
"""Simplify API."""
serializer = ComicFilenameSerializer(md, ext=ext, verbose=verbose)
return serializer.serialize()
|