Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
2daf9b3ed8 | |||
a6d55cd21a | |||
4034123e6d | |||
5587bfac31 | |||
4b6d35fd3a | |||
3cf75cf2ec | |||
30dbe758d4 | |||
55384790f8 | |||
acaf5ed510 | |||
d213db3129 | |||
6a717377df | |||
904561fb8e | |||
be6b71dec7 | |||
63b654a173 | |||
bc25acde9f | |||
03677ce4b8 | |||
535afcb4c6 | |||
06255f7848 | |||
00e649bb4c | |||
078f569ec6 | |||
315cf7d920 | |||
e9cc6a16a8 | |||
26eb6985fe |
14
.github/workflows/build.yaml
vendored
14
.github/workflows/build.yaml
vendored
@ -80,6 +80,20 @@ jobs:
|
||||
run: |
|
||||
choco install -y zip
|
||||
if: runner.os == 'Windows'
|
||||
- name: Install macos dependencies
|
||||
run: |
|
||||
brew install icu4c pkg-config
|
||||
export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig";
|
||||
export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH"
|
||||
python -m pip install --no-binary=pyicu pyicu
|
||||
if: runner.os == 'macOS'
|
||||
- name: Install linux dependencies
|
||||
run: |
|
||||
sudo apt-get install pkg-config libicu-dev
|
||||
export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig";
|
||||
export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH"
|
||||
python -m pip install --no-binary=pyicu pyicu
|
||||
if: runner.os == 'Linux'
|
||||
|
||||
- name: Build and install PyPi packages
|
||||
run: |
|
||||
|
24
.github/workflows/package.yaml
vendored
24
.github/workflows/package.yaml
vendored
@ -41,12 +41,25 @@ jobs:
|
||||
run: |
|
||||
choco install -y zip
|
||||
if: runner.os == 'Windows'
|
||||
- name: Install macos dependencies
|
||||
run: |
|
||||
brew install icu4c pkg-config
|
||||
export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig";
|
||||
export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH"
|
||||
python -m pip install --no-binary=pyicu pyicu
|
||||
if: runner.os == 'macOS'
|
||||
- name: Install linux dependencies
|
||||
run: |
|
||||
sudo apt-get install pkg-config libicu-dev
|
||||
export PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig";
|
||||
export PATH="/usr/local/opt/icu4c/bin:/usr/local/opt/icu4c/sbin:$PATH"
|
||||
python -m pip install --no-binary=pyicu pyicu
|
||||
if: runner.os == 'Linux'
|
||||
|
||||
- name: Build, Install and Test PyPi packages
|
||||
run: |
|
||||
make clean pydist
|
||||
python -m pip install "dist/$(python setup.py --fullname)-py3-none-any.whl[all]"
|
||||
echo "CT_FULL_NAME=$(python setup.py --fullname)" >> $GITHUB_ENV
|
||||
python -m flake8
|
||||
python -m pytest
|
||||
|
||||
@ -61,12 +74,19 @@ jobs:
|
||||
run: |
|
||||
make dist
|
||||
|
||||
- name: Get release name
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
shell: bash
|
||||
run: |
|
||||
echo "release_name=$(git tag -l --format "%(refname:strip=2): %(contents:lines=1)" ${{ github.ref_name }})" >> $GITHUB_ENV
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
name: env.release_name
|
||||
prerelease: "${{ contains(github.ref, '-') }}" # alpha-releases should be 1.3.0-alpha.x full releases should be 1.3.0
|
||||
draft: false
|
||||
files: |
|
||||
dist/!(*Linux).zip
|
||||
dist/*.whl
|
||||
dist/*${{ fromJSON('["never", ""]')[runner.os == 'Linux'] }}.whl
|
||||
|
@ -2,6 +2,7 @@
|
||||
[](https://github.com/comictagger/comictagger/releases/latest)
|
||||
[](https://pypi.org/project/comictagger/)
|
||||
[](https://pypistats.org/packages/comictagger)
|
||||
[](https://community.chocolatey.org/packages/comictagger)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
|
||||
[](https://github.com/comictagger/comictagger/discussions)
|
||||
@ -48,6 +49,12 @@ A pip package is provided, you can install it with:
|
||||
|
||||
There are two optional dependencies GUI and CBR. You can install the optional dependencies by specifying one or more of `GUI`,`CBR` or `all` in braces e.g. `comictagger[CBR,GUI]`
|
||||
|
||||
### Chocolatey installation (Windows only)
|
||||
|
||||
A [Chocolatey package](https://community.chocolatey.org/packages/comictagger), maintained by @Xav83, is provided, you can install it with:
|
||||
```powershell
|
||||
choco install comictagger
|
||||
```
|
||||
### From source
|
||||
|
||||
1. Ensure you have python 3.9 installed
|
||||
|
@ -88,9 +88,9 @@ class CoMet:
|
||||
assign("readingDirection", "rtl")
|
||||
|
||||
if md.year is not None:
|
||||
date_str = str(md.year).zfill(4)
|
||||
date_str = f"{md.year:04}"
|
||||
if md.month is not None:
|
||||
date_str += "-" + str(md.month).zfill(2)
|
||||
date_str += f"-{md.month:02}"
|
||||
assign("date", date_str)
|
||||
|
||||
assign("coverImage", md.cover_image)
|
||||
|
@ -148,7 +148,7 @@ class SevenZipArchiver(UnknownArchiver):
|
||||
def get_filename_list(self) -> list[str]:
|
||||
try:
|
||||
with py7zr.SevenZipFile(self.path, "r") as zf:
|
||||
namelist: list[str] = zf.getnames()
|
||||
namelist: list[str] = [file.filename for file in zf.list() if not file.is_directory]
|
||||
|
||||
return namelist
|
||||
except (py7zr.Bad7zFile, OSError) as e:
|
||||
@ -248,7 +248,7 @@ class ZipArchiver(UnknownArchiver):
|
||||
def get_filename_list(self) -> list[str]:
|
||||
try:
|
||||
with zipfile.ZipFile(self.path, mode="r") as zf:
|
||||
namelist = zf.namelist()
|
||||
namelist = [file.filename for file in zf.infolist() if not file.is_dir()]
|
||||
return namelist
|
||||
except (zipfile.BadZipfile, OSError) as e:
|
||||
logger.error("Error listing files in zip archive [%s]: %s", e, self.path)
|
||||
@ -379,14 +379,15 @@ class RarArchiver(UnknownArchiver):
|
||||
|
||||
def get_comment(self) -> str:
|
||||
rarc = self.get_rar_obj()
|
||||
return str(rarc.comment) if rarc else ""
|
||||
return rarc.comment.decode("utf-8") if rarc else ""
|
||||
|
||||
def set_comment(self, comment: str) -> bool:
|
||||
if rar_support and self.rar_exe_path:
|
||||
try:
|
||||
# write comment to temp file
|
||||
with tempfile.NamedTemporaryFile() as tmp_file:
|
||||
tmp_file.write(comment.encode("utf-8"))
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
tmp_file = pathlib.Path(tmp_dir) / "rar_comment.txt"
|
||||
tmp_file.write_text(comment, encoding="utf-8")
|
||||
|
||||
working_dir = os.path.dirname(os.path.abspath(self.path))
|
||||
|
||||
@ -396,7 +397,7 @@ class RarArchiver(UnknownArchiver):
|
||||
"c",
|
||||
f"-w{working_dir}",
|
||||
"-c-",
|
||||
f"-z{tmp_file.name}",
|
||||
f"-z{tmp_file}",
|
||||
str(self.path),
|
||||
]
|
||||
subprocess.run(
|
||||
@ -683,7 +684,7 @@ class ComicArchive:
|
||||
self._has_cbi: bool | None = None
|
||||
self._has_cix: bool | None = None
|
||||
self._has_comet: bool | None = None
|
||||
self.path = pathlib.Path(path)
|
||||
self.path = pathlib.Path(path).absolute()
|
||||
self.page_count: int | None = None
|
||||
self.page_list: list[str] = []
|
||||
|
||||
@ -934,7 +935,7 @@ class ComicArchive:
|
||||
# seems like some archive creators are on Windows, and don't know about case-sensitivity!
|
||||
if sort_list:
|
||||
|
||||
files = cast(list[str], natsort.natsorted(files, alg=natsort.ns.IC | natsort.ns.I | natsort.ns.U))
|
||||
files = cast(list[str], natsort.os_sorted(files))
|
||||
|
||||
# make a sub-list of image files
|
||||
self.page_list = []
|
||||
|
@ -112,8 +112,8 @@ class ComicBookInfo:
|
||||
metadata.credits = []
|
||||
|
||||
# need the language string to be ISO
|
||||
if metadata.language is not None:
|
||||
metadata.language = utils.get_language(metadata.language)
|
||||
if metadata.language:
|
||||
metadata.language = utils.get_language_iso(metadata.language)
|
||||
|
||||
metadata.is_empty = False
|
||||
|
||||
|
@ -32,10 +32,13 @@ from text2digits import text2digits
|
||||
|
||||
from comicapi import filenamelexer, issuestring
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
t2d = text2digits.Text2Digits(add_ordinal_ending=False)
|
||||
t2do = text2digits.Text2Digits(add_ordinal_ending=True)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
placeholders_no_dashes = [re.compile(r"[-_]"), re.compile(r" +")]
|
||||
placeholders_allow_dashes = [re.compile(r"[_]"), re.compile(r" +")]
|
||||
|
||||
|
||||
class FileNameParser:
|
||||
@ -54,9 +57,9 @@ class FileNameParser:
|
||||
|
||||
def fix_spaces(self, string: str, remove_dashes: bool = True) -> str:
|
||||
if remove_dashes:
|
||||
placeholders = [r"[-_]", r" +"]
|
||||
placeholders = placeholders_no_dashes
|
||||
else:
|
||||
placeholders = [r"[_]", r" +"]
|
||||
placeholders = placeholders_allow_dashes
|
||||
for ph in placeholders:
|
||||
string = re.sub(ph, self.repl, string)
|
||||
return string
|
||||
|
@ -167,12 +167,12 @@ def titles_match(search_title: str, record_title: str, threshold: int = 90) -> b
|
||||
|
||||
|
||||
def unique_file(file_name: pathlib.Path) -> pathlib.Path:
|
||||
name = file_name.name
|
||||
name = file_name.stem
|
||||
counter = 1
|
||||
while True:
|
||||
if not file_name.exists():
|
||||
return file_name
|
||||
file_name = file_name.with_name(name + " (" + str(counter) + ")")
|
||||
file_name = file_name.with_stem(name + " (" + str(counter) + ")")
|
||||
counter += 1
|
||||
|
||||
|
||||
@ -193,18 +193,15 @@ def get_language_from_iso(iso: str | None) -> str | None:
|
||||
return languages[iso]
|
||||
|
||||
|
||||
def get_language(string: str | None) -> str | None:
|
||||
def get_language_iso(string: str | None) -> str | None:
|
||||
if string is None:
|
||||
return None
|
||||
string = string.casefold()
|
||||
lang = string.casefold()
|
||||
|
||||
lang = get_language_from_iso(string)
|
||||
|
||||
if lang is None:
|
||||
try:
|
||||
return str(pycountry.languages.lookup(string).name)
|
||||
except LookupError:
|
||||
return None
|
||||
try:
|
||||
return getattr(pycountry.languages.lookup(string), "alpha_2", None)
|
||||
except LookupError:
|
||||
pass
|
||||
return lang
|
||||
|
||||
|
||||
|
@ -472,7 +472,7 @@ def process_file_cli(
|
||||
match_results.good_matches.append(str(ca.path.absolute()))
|
||||
|
||||
elif opts.rename:
|
||||
|
||||
original_path = ca.path
|
||||
msg_hdr = ""
|
||||
if batch_mode:
|
||||
msg_hdr = f"{ca.path}: "
|
||||
@ -525,11 +525,14 @@ def process_file_cli(
|
||||
suffix = ""
|
||||
if not opts.dryrun:
|
||||
# rename the file
|
||||
ca.rename(utils.unique_file(full_path))
|
||||
try:
|
||||
ca.rename(utils.unique_file(full_path))
|
||||
except OSError:
|
||||
logger.exception("Failed to rename comic archive: %s", ca.path)
|
||||
else:
|
||||
suffix = " (dry-run, no change)"
|
||||
|
||||
print(f"renamed '{os.path.basename(ca.path)}' -> '{new_name}' {suffix}")
|
||||
print(f"renamed '{original_path.name}' -> '{new_name}' {suffix}")
|
||||
|
||||
elif opts.export_to_zip:
|
||||
msg_hdr = ""
|
||||
|
@ -20,10 +20,9 @@ import logging
|
||||
import os
|
||||
import pathlib
|
||||
import string
|
||||
import sys
|
||||
from typing import Any, cast
|
||||
from typing import Any, NamedTuple, cast
|
||||
|
||||
from pathvalidate import sanitize_filename
|
||||
from pathvalidate import Platform, normalize_platform, sanitize_filename
|
||||
|
||||
from comicapi.comicarchive import ComicArchive
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
@ -32,6 +31,31 @@ from comicapi.issuestring import IssueString
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Replacement(NamedTuple):
|
||||
find: str
|
||||
replce: str
|
||||
strict_only: bool
|
||||
|
||||
|
||||
class Replacements(NamedTuple):
|
||||
literal_text: list[Replacement]
|
||||
format_value: list[Replacement]
|
||||
|
||||
|
||||
REPLACEMENTS = Replacements(
|
||||
literal_text=[
|
||||
Replacement(": ", " - ", True),
|
||||
Replacement(":", "-", True),
|
||||
],
|
||||
format_value=[
|
||||
Replacement(": ", " - ", True),
|
||||
Replacement(":", "-", True),
|
||||
Replacement("/", "-", False),
|
||||
Replacement("\\", "-", True),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def get_rename_dir(ca: ComicArchive, rename_dir: str | pathlib.Path | None) -> pathlib.Path:
|
||||
folder = ca.path.parent.absolute()
|
||||
if rename_dir is not None:
|
||||
@ -42,16 +66,55 @@ def get_rename_dir(ca: ComicArchive, rename_dir: str | pathlib.Path | None) -> p
|
||||
|
||||
|
||||
class MetadataFormatter(string.Formatter):
|
||||
def __init__(self, smart_cleanup: bool = False, platform: str = "auto") -> None:
|
||||
def __init__(
|
||||
self, smart_cleanup: bool = False, platform: str = "auto", replacements: Replacements = REPLACEMENTS
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.smart_cleanup = smart_cleanup
|
||||
self.platform = platform
|
||||
self.platform = normalize_platform(platform)
|
||||
self.replacements = replacements
|
||||
|
||||
def format_field(self, value: Any, format_spec: str) -> str:
|
||||
if value is None or value == "":
|
||||
return ""
|
||||
return cast(str, super().format_field(value, format_spec))
|
||||
|
||||
def convert_field(self, value: Any, conversion: str) -> str:
|
||||
if conversion == "u":
|
||||
return str(value).upper()
|
||||
if conversion == "l":
|
||||
return str(value).casefold()
|
||||
if conversion == "c":
|
||||
return str(value).capitalize()
|
||||
if conversion == "S":
|
||||
return str(value).swapcase()
|
||||
if conversion == "t":
|
||||
return str(value).title()
|
||||
return cast(str, super().convert_field(value, conversion))
|
||||
|
||||
def handle_replacements(self, string: str, replacements: list[Replacement]) -> str:
|
||||
for find, replace, strict_only in replacements:
|
||||
if self.is_strict() or not strict_only:
|
||||
string = string.replace(find, replace)
|
||||
return string
|
||||
|
||||
def none_replacement(self, value: Any, replacement: str, r: str) -> Any:
|
||||
if r == "-" and value is None or value == "":
|
||||
return replacement
|
||||
if r == "+" and value is not None:
|
||||
return replacement
|
||||
return value
|
||||
|
||||
def split_replacement(self, field_name: str) -> tuple[str, str, str]:
|
||||
if "-" in field_name:
|
||||
return field_name.rpartition("-")
|
||||
if "+" in field_name:
|
||||
return field_name.rpartition("+")
|
||||
return field_name, "", ""
|
||||
|
||||
def is_strict(self) -> bool:
|
||||
return self.platform in [Platform.UNIVERSAL, Platform.WINDOWS]
|
||||
|
||||
def _vformat(
|
||||
self,
|
||||
format_string: str,
|
||||
@ -72,6 +135,7 @@ class MetadataFormatter(string.Formatter):
|
||||
if lstrip:
|
||||
literal_text = literal_text.lstrip("-_)}]#")
|
||||
if self.smart_cleanup:
|
||||
literal_text = self.handle_replacements(literal_text, self.replacements.literal_text)
|
||||
lspace = literal_text[0].isspace() if literal_text else False
|
||||
rspace = literal_text[-1].isspace() if literal_text else False
|
||||
literal_text = " ".join(literal_text.split())
|
||||
@ -87,6 +151,7 @@ class MetadataFormatter(string.Formatter):
|
||||
lstrip = False
|
||||
# if there's a field, output it
|
||||
if field_name is not None and field_name != "":
|
||||
field_name, r, replacement = self.split_replacement(field_name)
|
||||
field_name = field_name.casefold()
|
||||
# this is some markup, find the object and do the formatting
|
||||
|
||||
@ -99,6 +164,8 @@ class MetadataFormatter(string.Formatter):
|
||||
obj, arg_used = self.get_field(field_name, args, kwargs)
|
||||
used_args.add(arg_used)
|
||||
|
||||
obj = self.none_replacement(obj, replacement, r)
|
||||
|
||||
# do any conversion on the resulting object
|
||||
obj = self.convert_field(obj, conversion) # type: ignore
|
||||
|
||||
@ -117,6 +184,8 @@ class MetadataFormatter(string.Formatter):
|
||||
result[-1], _, _ = result[-1].rstrip().rpartition(" ")
|
||||
result[-1] = result[-1].rstrip("-_({[#")
|
||||
if self.smart_cleanup:
|
||||
# colons and slashes get special treatment
|
||||
fmt_obj = self.handle_replacements(fmt_obj, self.replacements.format_value)
|
||||
fmt_obj = " ".join(fmt_obj.split())
|
||||
fmt_obj = str(sanitize_filename(fmt_obj, platform=self.platform))
|
||||
result.append(fmt_obj)
|
||||
@ -179,13 +248,6 @@ class FileRenamer:
|
||||
|
||||
new_basename = ""
|
||||
for component in pathlib.PureWindowsPath(template).parts:
|
||||
if (
|
||||
self.platform.casefold() in ["universal", "windows"] or sys.platform.casefold() in ["windows"]
|
||||
) and self.smart_cleanup:
|
||||
# colons get special treatment
|
||||
component = component.replace(": ", " - ")
|
||||
component = component.replace(":", "-")
|
||||
|
||||
new_basename = str(
|
||||
sanitize_filename(fmt.vformat(component, args=[], kwargs=Default(md_dict)), platform=self.platform)
|
||||
).strip()
|
||||
|
@ -524,7 +524,7 @@ class IssueIdentifier:
|
||||
self.log_msg("")
|
||||
|
||||
if len(self.match_list) == 0:
|
||||
self.log_msg(":-(no matches!")
|
||||
self.log_msg(":-( no matches!")
|
||||
self.search_result = self.result_no_matches
|
||||
return self.match_list
|
||||
|
||||
|
@ -159,7 +159,7 @@ def ctmain() -> None:
|
||||
|
||||
logger.debug("Installed Packages")
|
||||
for pkg in sorted(importlib_metadata.distributions(), key=lambda x: x.name):
|
||||
logger.debug("%s\t%s", pkg.name, pkg.version)
|
||||
logger.debug("%s\t%s", pkg.metadata["Name"], pkg.metadata["Version"])
|
||||
|
||||
utils.load_publishers()
|
||||
update_publishers()
|
||||
|
@ -340,9 +340,9 @@ class PageListEditor(QtWidgets.QWidget):
|
||||
else:
|
||||
text += " (Error: " + page_dict["Type"] + ")"
|
||||
if "DoublePage" in page_dict:
|
||||
text += " " + "\U00002461"
|
||||
text += " ②"
|
||||
if "Bookmark" in page_dict:
|
||||
text += " " + "\U0001F516"
|
||||
text += " 🔖"
|
||||
return text
|
||||
|
||||
def get_page_list(self) -> list[ImageMetadata]:
|
||||
|
@ -175,29 +175,37 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
center_window_on_parent(prog_dialog)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
for idx, comic in enumerate(zip(self.comic_archive_list, self.rename_list)):
|
||||
try:
|
||||
for idx, comic in enumerate(zip(self.comic_archive_list, self.rename_list)):
|
||||
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
if prog_dialog.wasCanceled():
|
||||
break
|
||||
idx += 1
|
||||
prog_dialog.setValue(idx)
|
||||
prog_dialog.setLabelText(comic[1])
|
||||
center_window_on_parent(prog_dialog)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
if prog_dialog.wasCanceled():
|
||||
break
|
||||
idx += 1
|
||||
prog_dialog.setValue(idx)
|
||||
prog_dialog.setLabelText(comic[1])
|
||||
center_window_on_parent(prog_dialog)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
folder = get_rename_dir(comic[0], self.settings.rename_dir if self.settings.rename_move_dir else None)
|
||||
folder = get_rename_dir(comic[0], self.settings.rename_dir if self.settings.rename_move_dir else None)
|
||||
|
||||
full_path = folder / comic[1]
|
||||
full_path = folder / comic[1]
|
||||
|
||||
if full_path == comic[0].path:
|
||||
logger.info("%s: Filename is already good!", comic[1])
|
||||
continue
|
||||
if full_path == comic[0].path:
|
||||
logger.info("%s: Filename is already good!", comic[1])
|
||||
continue
|
||||
|
||||
if not comic[0].is_writable(check_rar_status=False):
|
||||
continue
|
||||
if not comic[0].is_writable(check_rar_status=False):
|
||||
continue
|
||||
|
||||
comic[0].rename(utils.unique_file(full_path))
|
||||
comic[0].rename(utils.unique_file(full_path))
|
||||
except Exception as e:
|
||||
logger.exception("Failed to rename comic archive: %s", comic[0].path)
|
||||
QtWidgets.QMessageBox.critical(
|
||||
self,
|
||||
"There was an issue when renaming!",
|
||||
f"Renaming failed!<br/><br/>{type(e).__name__}: {e}<br/><br/>",
|
||||
)
|
||||
|
||||
prog_dialog.hide()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
@ -1712,7 +1712,7 @@ Have fun!
|
||||
)
|
||||
if dlg.ignore_leading_digits_in_filename and md.series is not None:
|
||||
# remove all leading numbers
|
||||
md.series = re.sub(r"([\d.]*)(.*)", "\\2", md.series)
|
||||
md.series = re.sub(r"([\d.]*)(.*)", r"\2", md.series)
|
||||
|
||||
# use the dialog specified search string
|
||||
if dlg.search_string:
|
||||
|
@ -1,10 +1,11 @@
|
||||
beautifulsoup4 >= 4.1
|
||||
importlib_metadata
|
||||
beautifulsoup4>=4.1
|
||||
importlib_metadata>=3.3.0
|
||||
natsort>=8.1.0
|
||||
pathvalidate
|
||||
pillow>=9.1.0
|
||||
py7zr
|
||||
pycountry
|
||||
pyicu; sys_platform == 'linux' or sys_platform == 'darwin'
|
||||
requests==2.*
|
||||
text2digits
|
||||
thefuzz>=0.19.0
|
||||
|
Binary file not shown.
Binary file not shown.
@ -750,6 +750,76 @@ fnames = [
|
||||
]
|
||||
|
||||
rnames = [
|
||||
(
|
||||
"{series!c} {price} {year}", # Capitalize
|
||||
False,
|
||||
"universal",
|
||||
"Cory doctorow's futuristic tales of the here and now 2007.cbz",
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
"{series!t} {price} {year}", # Title Case
|
||||
False,
|
||||
"universal",
|
||||
"Cory Doctorow'S Futuristic Tales Of The Here And Now 2007.cbz",
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
"{series!S} {price} {year}", # Swap Case
|
||||
False,
|
||||
"universal",
|
||||
"cORY dOCTOROW'S fUTURISTIC tALES OF THE hERE AND nOW 2007.cbz",
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
"{title!l} {price} {year}", # Lowercase
|
||||
False,
|
||||
"universal",
|
||||
"anda's game 2007.cbz",
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
"{title!u} {price} {year}", # Upper Case
|
||||
False,
|
||||
"universal",
|
||||
"ANDA'S GAME 2007.cbz",
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
"{title} {price} {year+}", # Empty alternate value
|
||||
False,
|
||||
"universal",
|
||||
"Anda's Game.cbz",
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
"{title} {price} {year+year!u}", # Alternate value Upper Case
|
||||
False,
|
||||
"universal",
|
||||
"Anda's Game YEAR.cbz",
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
"{title} {price} {year+year}", # Alternate Value
|
||||
False,
|
||||
"universal",
|
||||
"Anda's Game year.cbz",
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
"{title} {price-0} {year}", # Default value
|
||||
False,
|
||||
"universal",
|
||||
"Anda's Game 0 2007.cbz",
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
"{title} {price+0} {year}", # Alternate Value
|
||||
False,
|
||||
"universal",
|
||||
"Anda's Game 2007.cbz",
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
"{series} #{issue} - {title} ({year}) ({price})", # price should be none
|
||||
False,
|
||||
@ -757,6 +827,13 @@ rnames = [
|
||||
"Cory Doctorow's Futuristic Tales of the Here and Now #001 - Anda's Game (2007).cbz",
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
"{series} #{issue} - {title} {volume:02} ({year})", # Ensure format specifier works
|
||||
False,
|
||||
"universal",
|
||||
"Cory Doctorow's Futuristic Tales of the Here and Now #001 - Anda's Game 01 (2007).cbz",
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
"{series} #{issue} - {title} ({year})({price})", # price should be none, test no space between ')('
|
||||
False,
|
||||
@ -778,6 +855,27 @@ rnames = [
|
||||
"Cory Doctorow's Futuristic Tales of the Here and Now #001 - Anda's Game (2007).cbz",
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
"{title} {web_link}", # Ensure colon is replaced in metadata
|
||||
False,
|
||||
"universal",
|
||||
"Anda's Game https---comicvine.gamespot.com-cory-doctorows-futuristic-tales-of-the-here-and-no-4000-140529-.cbz",
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
"{title} {web_link}", # Ensure slashes are replaced in metadata on linux/macos
|
||||
False,
|
||||
"Linux",
|
||||
"Anda's Game https:--comicvine.gamespot.com-cory-doctorows-futuristic-tales-of-the-here-and-no-4000-140529-.cbz",
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
"{series}:{title} #{issue} ({year})", # on windows the ':' is replaced
|
||||
False,
|
||||
"universal",
|
||||
"Cory Doctorow's Futuristic Tales of the Here and Now-Anda's Game #001 (2007).cbz",
|
||||
does_not_raise(),
|
||||
),
|
||||
(
|
||||
"{series}: {title} #{issue} ({year})", # on windows the ':' is replaced
|
||||
False,
|
||||
|
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import platform
|
||||
import shutil
|
||||
|
||||
import pytest
|
||||
@ -15,6 +16,8 @@ def test_getPageNameList():
|
||||
pageNameList = c.get_page_name_list()
|
||||
|
||||
assert pageNameList == [
|
||||
"!cover.jpg",
|
||||
"00.jpg",
|
||||
"page0.jpg",
|
||||
"Page1.jpeg",
|
||||
"Page2.png",
|
||||
@ -44,6 +47,58 @@ def test_save_cix(tmp_comic):
|
||||
md = tmp_comic.read_cix()
|
||||
|
||||
|
||||
def test_save_cbi(tmp_comic):
|
||||
md = tmp_comic.read_cix()
|
||||
md.set_default_page_list(tmp_comic.get_number_of_pages())
|
||||
|
||||
assert tmp_comic.write_cbi(md)
|
||||
|
||||
md = tmp_comic.read_cbi()
|
||||
|
||||
|
||||
@pytest.mark.xfail(not (comicapi.comicarchive.rar_support and shutil.which("rar")), reason="rar support")
|
||||
def test_save_cix_rar(tmp_path):
|
||||
cbr_path = datadir / "fake_cbr.cbr"
|
||||
shutil.copy(cbr_path, tmp_path)
|
||||
|
||||
tmp_comic = comicapi.comicarchive.ComicArchive(tmp_path / cbr_path.name)
|
||||
assert tmp_comic.write_cix(comicapi.genericmetadata.md_test)
|
||||
|
||||
md = tmp_comic.read_cix()
|
||||
assert md.replace(pages=[]) == comicapi.genericmetadata.md_test.replace(pages=[])
|
||||
|
||||
|
||||
@pytest.mark.xfail(not (comicapi.comicarchive.rar_support and shutil.which("rar")), reason="rar support")
|
||||
def test_save_cbi_rar(tmp_path):
|
||||
cbr_path = datadir / "fake_cbr.cbr"
|
||||
shutil.copy(cbr_path, tmp_path)
|
||||
|
||||
tmp_comic = comicapi.comicarchive.ComicArchive(tmp_path / cbr_path.name)
|
||||
assert tmp_comic.write_cbi(comicapi.genericmetadata.md_test)
|
||||
|
||||
md = tmp_comic.read_cbi()
|
||||
assert md.replace(pages=[]) == comicapi.genericmetadata.md_test.replace(
|
||||
pages=[],
|
||||
day=None,
|
||||
alternate_series=None,
|
||||
alternate_number=None,
|
||||
alternate_count=None,
|
||||
imprint=None,
|
||||
notes=None,
|
||||
web_link=None,
|
||||
format=None,
|
||||
manga=None,
|
||||
page_count=None,
|
||||
maturity_rating=None,
|
||||
story_arc=None,
|
||||
series_group=None,
|
||||
scan_info=None,
|
||||
characters=None,
|
||||
teams=None,
|
||||
locations=None,
|
||||
)
|
||||
|
||||
|
||||
def test_page_type_save(tmp_comic):
|
||||
md = tmp_comic.read_cix()
|
||||
t = md.pages[0]
|
||||
@ -96,3 +151,17 @@ def test_rename(tmp_comic, tmp_path):
|
||||
assert not old_path.exists()
|
||||
assert tmp_comic.path.exists()
|
||||
assert tmp_comic.path != old_path
|
||||
|
||||
|
||||
def test_rename_ro_dest(tmp_comic, tmp_path):
|
||||
old_path = tmp_comic.path
|
||||
dest = tmp_path / "tmp"
|
||||
dest.mkdir(mode=0o000)
|
||||
with pytest.raises(OSError):
|
||||
if platform.system() == "Windows":
|
||||
raise OSError("Windows sucks")
|
||||
tmp_comic.rename(dest / "test.cbz")
|
||||
dest.chmod(mode=0o777)
|
||||
assert old_path.exists()
|
||||
assert tmp_comic.path.exists()
|
||||
assert tmp_comic.path == old_path
|
||||
|
@ -18,7 +18,6 @@ def test_cbi():
|
||||
md = CBI.metadata_from_string(string)
|
||||
md_test = comicapi.genericmetadata.md_test.replace(
|
||||
day=None,
|
||||
language="English",
|
||||
page_count=None,
|
||||
maturity_rating=None,
|
||||
story_arc=None,
|
||||
|
@ -63,25 +63,26 @@ def test_xlate(value, result):
|
||||
|
||||
|
||||
language_values = [
|
||||
("en", "English"),
|
||||
("EN", "English"),
|
||||
("En", "English"),
|
||||
("", None),
|
||||
("english", "en"),
|
||||
("ENGLISH", "en"),
|
||||
("EnglisH", "en"),
|
||||
("", ""),
|
||||
("aaa", None), # does not have a 2-letter code
|
||||
(None, None),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value, result", language_values)
|
||||
def test_get_language(value, result):
|
||||
assert result == comicapi.utils.get_language(value)
|
||||
def test_get_language_iso(value, result):
|
||||
assert result == comicapi.utils.get_language_iso(value)
|
||||
|
||||
|
||||
def test_unique_file(tmp_path):
|
||||
file = tmp_path / "test"
|
||||
file = tmp_path / "test.cbz"
|
||||
assert file == comicapi.utils.unique_file(file)
|
||||
|
||||
file.mkdir()
|
||||
assert (tmp_path / "test (1)") == comicapi.utils.unique_file(file)
|
||||
assert (tmp_path / "test (1).cbz") == comicapi.utils.unique_file(file)
|
||||
|
||||
|
||||
def test_add_to_path(monkeypatch):
|
||||
|
Reference in New Issue
Block a user