diff --git a/src/ComicInfoBuilder.py b/src/ComicInfoBuilder.py
index 9f03eb4..a87d237 100644
--- a/src/ComicInfoBuilder.py
+++ b/src/ComicInfoBuilder.py
@@ -789,7 +789,7 @@ class ComicInfoBuilder:
if favs is not None: rows.append(f"
| Favorites | {favs:,} |
")
if rows:
- link = f'MyAnimeList' if url else "MyAnimeList"
+ link = f'MyAnimeList' if url else "MyAnimeList"
parts.append(f"{link} stats as of {as_of}:
")
# 2. Description — Markdown → HTML (middle) -----------------------
diff --git a/src/KavitaPersonUpdater.py b/src/KavitaPersonUpdater.py
index cb5c8ef..0bd93cf 100644
--- a/src/KavitaPersonUpdater.py
+++ b/src/KavitaPersonUpdater.py
@@ -432,7 +432,7 @@ def _build_character_description(details: dict) -> str:
url = details.get("url") or ""
favorites = details.get("favorites")
if url and favorites is not None:
- parts.append(f'Favorites: {favorites:,}
')
+ parts.append(f'Favorites: {favorites:,}
')
about = (details.get("about") or "").strip()
if about:
parts.append(_plain_to_html(about))
@@ -469,7 +469,7 @@ def _build_person_description(details: dict) -> str:
f'{website} | '
)
if favorites is not None:
- fav_cell = (f'{favorites:,}' if url
+ fav_cell = (f'{favorites:,}' if url
else f"{favorites:,}")
rows.append(
f"| Member Favorites | {fav_cell} |
")
diff --git a/src/SuwayomiMover.py b/src/SuwayomiMover.py
index ab29468..644b86f 100644
--- a/src/SuwayomiMover.py
+++ b/src/SuwayomiMover.py
@@ -69,12 +69,30 @@ _SOURCE_LABEL_RE = re.compile(
re.IGNORECASE,
)
+# Characters that Windows (and SMB shares) forbid in path components.
+_WIN_ILLEGAL_RE = re.compile(r'[\\/*?"<>|]')
+
def _natural_key(name: str) -> list:
return [int(p) if p.isdigit() else p.lower()
for p in re.split(r"(\d+)", name)]
+def _sanitize_dirname(name: str) -> str:
+ """
+ Makes a string safe to use as a Windows (or SMB) directory name.
+
+ Rules applied:
+ - ": " or ":" surrounded by optional spaces -> " - "
+ ("Call of the Night: Paradise Arc" -> "Call of the Night - Paradise Arc")
+ - Remaining Windows-illegal chars (\\ / * ? " < > |) are stripped.
+ - Leading/trailing dots and spaces are removed (Windows restriction).
+ """
+ name = re.sub(r"\s*:\s*", " - ", name)
+ name = _WIN_ILLEGAL_RE.sub("", name)
+ return name.strip(". ")
+
+
_SUWAYOMI_WANTED = {"Title", "Series", "Number", "Summary",
"Writer", "Penciller", "Genre", "Web",
"Year", "Month", "Day"}
@@ -326,8 +344,9 @@ class SuwayomiMover:
except Exception as exc:
print(f" [warn] metadata fetch failed: {exc}")
- # Destination folder uses the MangaBaka canonical title.
- dest_series = self._dst / mangabaka_title
+ # Destination folder uses the MangaBaka canonical title, sanitized for
+ # Windows / SMB paths (no colons, illegal chars, leading/trailing dots).
+ dest_series = self._dst / _sanitize_dirname(mangabaka_title)
dest_series.mkdir(parents=True, exist_ok=True)
chapter_results: list[dict] = []
@@ -403,7 +422,7 @@ if __name__ == "__main__":
)
# Process a single series
- result = mover.process_series("Yofukashi no Uta")
+ result = mover.process_series("Yofukashi no Uta_ Rakuen-hen")
ok = sum(1 for c in result["chapters"] if c["ok"])
failed = sum(1 for c in result["chapters"] if not c["ok"])
print(f"\nDone: {ok} ok, {failed} failed")