Compare commits

..

48 Commits

Author SHA1 Message Date
bfe005cb63 Merge branch 'fixSerialization' into develop 2022-04-19 14:55:50 -07:00
48c2e91f7e Fix pip reference 2022-04-19 14:49:14 -07:00
02f365b93f Fix Makefile
make check now uses a venv
make CI uses the environment
Fix rar test
2022-04-19 14:45:36 -07:00
d78c3e3039 Fix serialization errors
Add tests to ensure issue is fixed
Add make check
Add pytest to make CI
2022-04-19 13:16:33 -07:00
f18513fd0e Fix Template help 2022-04-19 00:44:29 -07:00
caa94c4e28 Merge branch 'Renaming' into develop 2022-04-18 22:56:49 -07:00
7037877a77 Add a strict mode to file renaming
Strict renaming removes all reserved names and characters regardless
 of operating system, with out strict mode only for the current
 Operating System
Add more edge cases to smart cleanup
Add more tests for file renaming
2022-04-18 22:55:13 -07:00
6cccf22d54 Allow switching between old and new rename templates
Show a message dialog explaining that there is a new template format
Add a dynamic label to show the effect of a rename
Add tests for FileRenamer
Remove the filename parameter from the determine_name function
2022-04-18 20:12:20 -07:00
ceb2b2861e Merge branch 'filename_tests' into develop 2022-04-18 20:11:06 -07:00
298f50cb45 Merge branch 'configDir' into develop 2022-04-18 20:10:50 -07:00
e616aa8373 Merge branch 'CodeCleanup' into develop 2022-04-18 20:10:08 -07:00
0fe881df59 Code cleanup 2022-04-18 19:40:04 -07:00
f3f48ea958 Add the ability to specify a config directory 2022-04-18 19:08:38 -07:00
9a9d36dc65 Add more tests for parsing filenames 2022-04-18 19:06:09 -07:00
028b728d82 Improve file renaming
Moves to Python format strings for renaming, handles directory
structures, moving of files to a destination directory, sanitizes
file paths with pathvalidate and takes a different approach to
smart filename cleanup using the Python string.Formatter class

Moving to Python format strings means we can point to python
documentation for syntax and all we have to do is document the
properties and types that are attached to the GenericMetadata class.

Switching to pathvalidate allows comictagger to more simply handle both
directories and symbols in filenames.

The only changes to the string.Formatter class is:
1. format_field returns
an empty string if the value is none or an empty string regardless of
the format specifier.
2. _vformat drops the previous literal text if the field value
is an empty string and lstrips the following literal text of closing
special characters.
2022-04-18 18:52:53 -07:00
23f323f52d Add filename tests 2022-04-15 02:46:57 -07:00
49210e67c5 Fix rar_support variable 2022-04-14 16:25:25 -07:00
e519bf79be Merge branch 'MichaelFitzurka-feature/263-pages-keyboard' into develop 2022-04-14 16:23:51 -07:00
4f08610a28 Fix CI 2022-04-14 13:16:51 -07:00
544bdcb4e3 Using shortcuts and actions. 2022-04-14 12:22:53 -04:00
f3095144f5 Merge branch 'feature/149-add-tests' into develop 2022-04-12 15:20:58 -07:00
75f31c7cb2 Merge branch 'fileEncoding' into develop 2022-04-11 18:02:26 -07:00
f7f4e41c95 Catch exception when displaying raw tags 2022-04-11 17:16:07 -07:00
6da177471b Fix #242
Fix file encoding inconsistencies, windows defaults to cp1252, which is
not Unicode compatible.
Add logging for all exceptions in the comicapi package
Ensure that all exceptions are logged and shown to the user
2022-04-11 14:52:41 -07:00
8a74e5b02b Keyboard commands for the Pages tab to make editing easier. 2022-04-10 18:10:09 -04:00
5658f261b0 Merge branch 'MichaelFitzurka-feature/m-age-rating' into develop 2022-04-10 11:05:06 -07:00
6da3bf764e Merge branch 'feature/m-age-rating' of https://github.com/MichaelFitzurka/comictagger into MichaelFitzurka-feature/m-age-rating 2022-04-10 11:04:48 -07:00
5e06d35057 Merge branch 'feature/253-recalc-page-dims' of https://github.com/MichaelFitzurka/comictagger into MichaelFitzurka-feature/253-recalc-page-dims 2022-04-10 11:00:10 -07:00
82bcc876b3 Merge branch 'MichaelFitzurka-feature/183-comment-html-fix' into develop 2022-04-10 10:59:40 -07:00
d7a6882577 Merge branch 'feature/183-comment-html-fix' of https://github.com/MichaelFitzurka/comictagger into MichaelFitzurka-feature/183-comment-html-fix 2022-04-10 10:59:00 -07:00
5e7e1b1513 Merge branch 'MichaelFitzurka-feature/246-dbl-page' into develop 2022-04-10 10:57:46 -07:00
cd9a02c255 Merge branch 'feature/246-dbl-page' of https://github.com/MichaelFitzurka/comictagger into MichaelFitzurka-feature/246-dbl-page 2022-04-10 10:54:49 -07:00
b47f816dd5 Merge branch 'abuchanan920-develop' into develop 2022-04-10 10:50:41 -07:00
d1a649c0ba Adding "M" age rating for 2.0 schema 2022-04-06 11:49:54 -04:00
b7759506fe Menu command to clear out page size,height,width on demand, to then recalculate on save. 2022-04-05 16:23:26 -04:00
97777d61d2 Fixing some HTML to comment translations. 2022-04-05 16:16:27 -04:00
e622b56dae Adding attribs to ImageMetadata class. 2022-04-05 11:23:18 -04:00
a24251e5b4 Merge branch 'comictagger:develop' into develop 2022-04-05 10:38:36 -04:00
d4470a2015 Use more idiomatic regular expression string
Co-authored-by: Timmy Welch <timmy@narnian.us>
2022-04-05 10:37:33 -04:00
d37022b71f Merge branch 'comictagger:develop' into feature/246-dbl-page 2022-04-05 09:59:20 -04:00
5f38241bcb Double Page functionality. 2022-04-05 09:52:59 -04:00
c9b5bd625f Fix parsing of filenames that end with an ID such as [__######__] 2022-04-04 22:34:31 -04:00
beb7c57a6b fix: change accidental overwrite of reserved __dir__ 2019-10-20 00:36:13 +02:00
ce48730bd5 fix: choco install multiple packages breaks with version 2019-10-20 00:25:52 +02:00
806b65db24 freeze windows python version to 3.7.5 2019-10-20 00:20:57 +02:00
cdf9a40227 fix: add setup.py install before testing 2019-10-20 00:08:11 +02:00
0adac47968 add pytest run to travis ci 2019-10-20 00:02:03 +02:00
096a89eab4 add pytest 2019-10-19 23:57:49 +02:00
35 changed files with 1787 additions and 203 deletions

View File

@ -2,6 +2,15 @@ PIP ?= pip3
PYTHON ?= python3
VERSION_STR := $(shell $(PYTHON) setup.py --version)
SITE_PACKAGES := $(shell $(PYTHON) -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')
PACKAGE_PATH = $(SITE_PACKAGES)/comictagger-$(VERSION_STR).dist-info
VENV := $(shell echo $${VIRTUAL_ENV-venv})
PY3 := $(shell command -v python3 2> /dev/null)
PYTHON_VENV := $(VENV)/bin/python
INSTALL_STAMP := $(VENV)/.install.stamp
ifeq ($(OS),Windows_NT)
OS_VERSION=win-$(PROCESSOR_ARCHITECTURE)
APP_NAME=comictagger.exe
@ -15,26 +24,33 @@ else
FINAL_NAME=ComicTagger-$(VERSION_STR)
endif
.PHONY: all clean pydist upload dist CI
.PHONY: all clean pydist upload dist CI check
all: clean dist
$(PYTHON_VENV):
@if [ -z $(PY3) ]; then echo "Python 3 could not be found."; exit 2; fi
$(PY3) -m venv $(VENV)
clean:
rm -rf *~ *.pyc *.pyo
rm -rf scripts/*.pyc
cd comictaggerlib; rm -f *~ *.pyc *.pyo
find . -type d -name "__pycache__" | xargs rm -rf {};
rm -rf $(INSTALL_STAMP)
rm -rf dist MANIFEST
rm -rf *.deb
rm -rf logdict*.log
$(MAKE) -C mac clean
rm -rf build
rm -rf comictaggerlib/ui/__pycache__
rm comictaggerlib/ctversion.py
CI:
CI: ins
black .
isort .
flake8 .
pytest
check: install
$(VENV)/bin/black --check .
$(VENV)/bin/isort --check .
$(VENV)/bin/flake8 .
$(VENV)/bin/pytest
pydist: CI
make clean
@ -48,7 +64,16 @@ upload:
$(PYTHON) setup.py register
$(PYTHON) setup.py sdist --formats=gztar upload
dist: CI
install: $(INSTALL_STAMP)
$(INSTALL_STAMP): $(PYTHON_VENV) requirements.txt requirements_dev.txt
$(PYTHON_VENV) -m pip install -r requirements_dev.txt
$(PYTHON_VENV) -m pip install -e .
touch $(INSTALL_STAMP)
ins: $(PACKAGE_PATH)
$(PACKAGE_PATH):
$(PIP) install .
dist: CI
pyinstaller -y comictagger.spec
cd dist && zip -r $(FINAL_NAME).zip $(APP_NAME)

View File

@ -30,8 +30,10 @@ import py7zr
try:
from unrar.cffi import rarfile
rar_support = True
except:
pass
rar_support = False
try:
from PIL import Image
@ -92,6 +94,7 @@ class SevenZipArchiver:
try:
self.rebuild_zip_file([archive_file])
except:
logger.exception("Failed to remove %s from 7zip archive", archive_file)
return False
else:
return True
@ -110,6 +113,7 @@ class SevenZipArchiver:
zf.writestr(data, archive_file)
return True
except:
logger.exception("Writing zip file failed")
return False
def get_filename_list(self):
@ -131,8 +135,8 @@ class SevenZipArchiver:
os.close(tmp_fd)
try:
with py7zr.SevenZipFile(self.path, "r") as zip:
targets = [f for f in zip.getnames() if f not in exclude_list]
with py7zr.SevenZipFile(self.path, "r") as zin:
targets = [f for f in zin.getnames() if f not in exclude_list]
with py7zr.SevenZipFile(self.path, "r") as zin:
with py7zr.SevenZipFile(tmp_name, "w") as zout:
for fname, bio in zin.read(targets).items():
@ -194,6 +198,7 @@ class ZipArchiver:
try:
self.rebuild_zip_file([archive_file])
except:
logger.exception("Failed to remove %s from zip archive", archive_file)
return False
else:
return True
@ -306,7 +311,7 @@ class ZipArchiver:
else:
raise Exception("Failed to write comment to zip file!")
except Exception:
logger.exception()
logger.exception("Writing comment to %s failed", filename)
return False
else:
return True
@ -342,7 +347,7 @@ class RarArchiver:
self.rar_exe_path = rar_exe_path
if RarArchiver.devnull is None:
RarArchiver.devnull = open(os.devnull, "w")
RarArchiver.devnull = open(os.devnull, "bw")
# windows only, keeps the cmd.exe from popping up
if platform.system() == "Windows":
@ -360,9 +365,8 @@ class RarArchiver:
try:
# write comment to temp file
tmp_fd, tmp_name = tempfile.mkstemp()
f = os.fdopen(tmp_fd, "w+")
f.write(comment)
f.close()
with os.fdopen(tmp_fd, "wb") as f:
f.write(comment.encode("utf-8"))
working_dir = os.path.dirname(os.path.abspath(self.path))
@ -441,7 +445,7 @@ class RarArchiver:
# TODO: will this break if 'archive_file' is in a subfolder. i.e. "foo/bar.txt"
# will need to create the subfolder above, I guess...
with open(tmp_file, "w") as f:
with open(tmp_file, "wb") as f:
f.write(data)
# use external program to write file to Rar archive
@ -457,7 +461,9 @@ class RarArchiver:
time.sleep(1)
os.remove(tmp_file)
os.rmdir(tmp_folder)
except:
except Exception as e:
logger.info(str(e))
logger.exception("Failed write %s to rar archive", archive_file)
return False
else:
return True
@ -479,6 +485,7 @@ class RarArchiver:
if platform.system() == "Darwin":
time.sleep(1)
except:
logger.exception("Failed to remove %s from rar archive", archive_file)
return False
else:
return True
@ -543,7 +550,6 @@ class FolderArchiver:
try:
with open(fname, "rb") as f:
data = f.read()
f.close()
except IOError:
logger.exception("Failed to read: %s", fname)
@ -553,11 +559,10 @@ class FolderArchiver:
fname = os.path.join(self.path, archive_file)
try:
with open(fname, "w+") as f:
with open(fname, "wb") as f:
f.write(data)
f.close()
except:
logger.exception("Failed to read: %s", fname)
logger.exception("Failed to write: %s", fname)
return False
else:
return True
@ -568,7 +573,7 @@ class FolderArchiver:
try:
os.remove(fname)
except:
logger.exception("Failed to read: %s", fname)
logger.exception("Failed to remove: %s", fname)
return False
else:
return True
@ -876,7 +881,7 @@ class ComicArchive:
self.page_list = []
for name in files:
if (
os.path.splitext(name)[1].lower() in [".jpg", "jpeg", ".png", ".gif", ".webp"]
os.path.splitext(name)[1].lower() in [".jpg", ".jpeg", ".png", ".gif", ".webp"]
and os.path.basename(name)[0] != "."
):
self.page_list.append(name)
@ -976,7 +981,7 @@ class ComicArchive:
if raw_cix == "":
raw_cix = None
cix_string = ComicInfoXml().string_from_metadata(metadata, xml=raw_cix)
write_success = self.archiver.write_file(self.ci_xml_filename, cix_string)
write_success = self.archiver.write_file(self.ci_xml_filename, cix_string.encode("utf-8"))
if write_success:
self.has__cix = True
self.cix_md = metadata
@ -1088,8 +1093,7 @@ class ComicArchive:
data = self.archiver.read_file(n)
except Exception as e:
data = ""
err_msg = f"Error reading in Comet XML for validation!: {e}"
logger.warning(err_msg)
logger.warning("Error reading in Comet XML for validation!: %s", e)
if CoMet().validate_string(data):
# since we found it, save it!
self.comet_filename = n

View File

@ -119,5 +119,5 @@ class ComicBookInfo:
cbi_container = self.create_json_dictionary(metadata)
with open(filename, "w") as f:
with open(filename, "w", encoding="utf-8") as f:
f.write(json.dumps(cbi_container, indent=4))

View File

@ -18,7 +18,7 @@ import logging
import xml.etree.ElementTree as ET
from comicapi import utils
from comicapi.genericmetadata import GenericMetadata
from comicapi.genericmetadata import GenericMetadata, PageType
from comicapi.issuestring import IssueString
logger = logging.getLogger(__name__)
@ -170,6 +170,11 @@ class ComicInfoXml:
pages_node = ET.SubElement(root, "Pages")
for page_dict in md.pages:
page = page_dict
if "Type" in page:
page["Type"] = page["Type"].value
if "Image" in page:
page["Image"] = str(page["Image"])
page_node = ET.SubElement(pages_node, "Page")
page_node.attrib = dict(sorted(page_dict.items()))
@ -251,6 +256,10 @@ class ComicInfoXml:
pages_node = root.find("Pages")
if pages_node is not None:
for page in pages_node:
if "Type" in page.attrib:
page.attrib["Type"] = PageType(page.attrib["Type"])
if "Image" in page.attrib:
page.attrib["Image"] = int(page.attrib["Image"])
md.pages.append(page.attrib)
md.is_empty = False

View File

@ -89,7 +89,7 @@ class FileNameParser:
# is the series name followed by issue
filename = re.sub(r"--.*", self.repl, filename)
elif "__" in filename:
elif "__" in filename and not re.search(r"\[__\d+__]", filename):
# the pattern seems to be that anything to left of the first "__"
# is the series name followed by issue
filename = re.sub(r"__.*", self.repl, filename)

View File

@ -21,6 +21,7 @@ possible, however lossy it might be
# limitations under the License.
import logging
from enum import Enum
from typing import List, TypedDict
from comicapi import utils
@ -28,7 +29,7 @@ from comicapi import utils
logger = logging.getLogger(__name__)
class PageType:
class PageType(Enum):
"""
These page info classes are exactly the same as the CIX scheme, since
@ -48,8 +49,10 @@ class PageType:
Deleted = "Deleted"
class ImageMetadata(TypedDict):
class ImageMetadata(TypedDict, total=False):
Type: PageType
Bookmark: str
DoublePage: bool
Image: int
ImageSize: str
ImageHeight: str
@ -211,8 +214,7 @@ class GenericMetadata:
def set_default_page_list(self, count):
# generate a default page list, with the first page marked as the cover
for i in range(count):
page_dict = {}
page_dict["Image"] = str(i)
page_dict = ImageMetadata(Image=i)
if i == 0:
page_dict["Type"] = PageType.FrontCover
self.pages.append(page_dict)
@ -239,11 +241,7 @@ class GenericMetadata:
def add_credit(self, person, role, primary=False):
credit = {}
credit["person"] = person
credit["role"] = role
if primary:
credit["primary"] = primary
credit: CreditMetadata = {"person": person, "role": role, "primary": primary}
# look to see if it's not already there...
found = False
@ -257,6 +255,15 @@ class GenericMetadata:
if not found:
self.credits.append(credit)
def get_primary_credit(self, role):
primary = ""
for credit in self.credits:
if (primary == "" and credit["role"].lower() == role.lower()) or (
credit["role"].lower() == role.lower() and credit["primary"]
):
primary = credit["person"]
return primary
def __str__(self):
vals = []
if self.is_empty:
@ -330,3 +337,93 @@ class GenericMetadata:
outstr += fmt_str.format(i[0] + ":", i[1])
return outstr
md_test = GenericMetadata()
md_test.is_empty = False
md_test.tag_origin = None
md_test.series = "Cory Doctorow's Futuristic Tales of the Here and Now"
md_test.issue = "1"
md_test.title = "Anda's Game"
md_test.publisher = "IDW Publishing"
md_test.month = 10
md_test.year = 2007
md_test.day = 1
md_test.issue_count = 6
md_test.volume = 1
md_test.genre = "Sci-Fi"
md_test.language = "en"
md_test.comments = (
"For 12-year-old Anda, getting paid real money to kill the characters of players who were cheating in her favorite online "
"computer game was a win-win situation. Until she found out who was paying her, and what those characters meant to the "
"livelihood of children around the world."
)
md_test.volume_count = None
md_test.critical_rating = None
md_test.country = None
md_test.alternate_series = "Tales"
md_test.alternate_number = "2"
md_test.alternate_count = 7
md_test.imprint = "craphound.com"
md_test.notes = "Tagged with ComicTagger 1.3.2a5 using info from Comic Vine on 2022-04-16 15:52:26. [Issue ID 140529]"
md_test.web_link = "https://comicvine.gamespot.com/cory-doctorows-futuristic-tales-of-the-here-and-no/4000-140529/"
md_test.format = "Series"
md_test.manga = "No"
md_test.black_and_white = None
md_test.page_count = 24
md_test.maturity_rating = "Everyone 10+"
md_test.story_arc = "Here and Now"
md_test.series_group = "Futuristic Tales"
md_test.scan_info = "(CC BY-NC-SA 3.0)"
md_test.characters = "Anda"
md_test.teams = "Fahrenheit"
md_test.locations = "lonely cottage "
md_test.credits = [
{"person": "Dara Naraghi", "role": "Writer"},
{"person": "Esteve Polls", "role": "Penciller"},
{"person": "Esteve Polls", "role": "Inker"},
{"person": "Neil Uyetake", "role": "Letterer"},
{"person": "Sam Kieth", "role": "Cover"},
{"person": "Ted Adams", "role": "Editor"},
]
md_test.tags = []
md_test.pages = [
{"Image": 0, "ImageHeight": "1280", "ImageSize": "195977", "ImageWidth": "800", "Type": PageType.FrontCover},
{"Image": 1, "ImageHeight": "2039", "ImageSize": "611993", "ImageWidth": "1327"},
{"Image": 2, "ImageHeight": "2039", "ImageSize": "783726", "ImageWidth": "1327"},
{"Image": 3, "ImageHeight": "2039", "ImageSize": "679584", "ImageWidth": "1327"},
{"Image": 4, "ImageHeight": "2039", "ImageSize": "788179", "ImageWidth": "1327"},
{"Image": 5, "ImageHeight": "2039", "ImageSize": "864433", "ImageWidth": "1327"},
{"Image": 6, "ImageHeight": "2039", "ImageSize": "765606", "ImageWidth": "1327"},
{"Image": 7, "ImageHeight": "2039", "ImageSize": "876427", "ImageWidth": "1327"},
{"Image": 8, "ImageHeight": "2039", "ImageSize": "852622", "ImageWidth": "1327"},
{"Image": 9, "ImageHeight": "2039", "ImageSize": "800205", "ImageWidth": "1327"},
{"Image": 10, "ImageHeight": "2039", "ImageSize": "746243", "ImageWidth": "1326"},
{"Image": 11, "ImageHeight": "2039", "ImageSize": "718062", "ImageWidth": "1327"},
{"Image": 12, "ImageHeight": "2039", "ImageSize": "532179", "ImageWidth": "1326"},
{"Image": 13, "ImageHeight": "2039", "ImageSize": "686708", "ImageWidth": "1327"},
{"Image": 14, "ImageHeight": "2039", "ImageSize": "641907", "ImageWidth": "1327"},
{"Image": 15, "ImageHeight": "2039", "ImageSize": "805388", "ImageWidth": "1327"},
{"Image": 16, "ImageHeight": "2039", "ImageSize": "668927", "ImageWidth": "1326"},
{"Image": 17, "ImageHeight": "2039", "ImageSize": "710605", "ImageWidth": "1327"},
{"Image": 18, "ImageHeight": "2039", "ImageSize": "761398", "ImageWidth": "1326"},
{"Image": 19, "ImageHeight": "2039", "ImageSize": "743807", "ImageWidth": "1327"},
{"Image": 20, "ImageHeight": "2039", "ImageSize": "552911", "ImageWidth": "1326"},
{"Image": 21, "ImageHeight": "2039", "ImageSize": "556827", "ImageWidth": "1327"},
{"Image": 22, "ImageHeight": "2039", "ImageSize": "675078", "ImageWidth": "1326"},
{
"Bookmark": "Interview",
"Image": "23",
"ImageHeight": "2032",
"ImageSize": "800965",
"ImageWidth": "1338",
"Type": PageType.Letters,
},
]
md_test.price = None
md_test.is_version_of = None
md_test.rights = None
md_test.identifier = None
md_test.last_mark = None
md_test.cover_image = None

View File

@ -27,7 +27,7 @@ from comicapi.comicarchive import ComicArchive, MetaDataStyle
from comicapi.genericmetadata import GenericMetadata
from comictaggerlib.cbltransformer import CBLTransformer
from comictaggerlib.comicvinetalker import ComicVineTalker, ComicVineTalkerException
from comictaggerlib.filerenamer import FileRenamer
from comictaggerlib.filerenamer import FileRenamer, FileRenamer2
from comictaggerlib.issueidentifier import IssueIdentifier
from comictaggerlib.resulttypes import MultipleMatch, OnlineMatchResults
from comictaggerlib.settings import ComicTaggerSettings
@ -155,6 +155,13 @@ def cli_mode(opts, settings):
logger.error("You must specify at least one filename. Use the -h option for more info")
return
if not settings.hide_rename_message:
print(
"There is a new rename template format available. "
"Please use the settings window to enable and test if you use this feature.\n\n"
"The old rename template format will be removed in the next release, "
"please reference the template help button in the settings or https://github.com/comictagger/comictagger/wiki/UserGuide#rename",
)
match_results = OnlineMatchResults()
for f in opts.file_list:
@ -445,24 +452,42 @@ def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults
elif ca.is_rar():
new_ext = ".cbr"
renamer = FileRenamer(md)
if settings.rename_new_renamer:
renamer = FileRenamer2(md, platform="universal" if settings.rename_strict else "auto")
else:
renamer = FileRenamer(md)
renamer.set_template(settings.rename_template)
renamer.set_issue_zero_padding(settings.rename_issue_number_padding)
renamer.set_smart_cleanup(settings.rename_use_smart_string_cleanup)
renamer.move = settings.rename_move_dir
new_name = renamer.determine_name(ca.path, ext=new_ext)
if new_name == os.path.basename(ca.path):
logger.error(msg_hdr + "Filename is already good!")
try:
new_name = renamer.determine_name(ext=new_ext)
except Exception as e:
print(
msg_hdr + "Invalid format string!\nYour rename template is invalid!\n\n"
"{}\n\nPlease consult the template help in the settings "
"and the documentation on the format at "
"https://docs.python.org/3/library/string.html#format-string-syntax".format(e),
file=sys.stderr,
)
return
folder = os.path.dirname(os.path.abspath(ca.path))
folder = os.path.dirname(os.path.abspath(filename))
if settings.rename_move_dir and len(settings.rename_dir.strip()) > 3:
folder = settings.rename_dir.strip()
new_abs_path = utils.unique_file(os.path.join(folder, new_name))
if os.path.join(folder, new_name) == os.path.abspath(filename):
print(msg_hdr + "Filename is already good!", file=sys.stderr)
return
suffix = ""
if not opts.dryrun:
# rename the file
os.rename(ca.path, new_abs_path)
os.makedirs(os.path.dirname(new_abs_path), 0o777, True)
os.rename(filename, new_abs_path)
else:
suffix = " (dry-run, no change)"

View File

@ -59,11 +59,11 @@ class ComicVineCacher:
def create_cache_db(self):
# create the version file
with open(self.version_file, "w") as f:
with open(self.version_file, "w", encoding="utf-8") as f:
f.write(ctversion.version)
# this will wipe out any existing version
open(self.db_file, "w").close()
open(self.db_file, "wb").close()
con = lite.connect(self.db_file)
@ -175,16 +175,15 @@ class ComicVineCacher:
rows = cur.fetchall()
# now process the results
for record in rows:
result = {}
result["id"] = record[1]
result["name"] = record[2]
result["start_year"] = record[3]
result["publisher"] = {}
result["publisher"]["name"] = record[4]
result["count_of_issues"] = record[5]
result["image"] = {}
result["image"]["super_url"] = record[6]
result["description"] = record[7]
result = {
"id": record[1],
"name": record[2],
"start_year": record[3],
"count_of_issues": record[5],
"description": record[7],
"publisher": {"name": record[4]},
"image": {"super_url": record[6]},
}
results.append(result)
@ -301,16 +300,15 @@ class ComicVineCacher:
if row is None:
return result
result = {}
# since ID is primary key, there is only one row
result["id"] = row[0]
result["name"] = row[1]
result["publisher"] = {}
result["publisher"]["name"] = row[2]
result["count_of_issues"] = row[3]
result["start_year"] = row[4]
result["issues"] = []
result = {
"id": row[0],
"name": row[1],
"count_of_issues": row[3],
"start_year": row[4],
"issues": [],
"publisher": {"name": row[2]},
}
return result
@ -337,17 +335,15 @@ class ComicVineCacher:
# now process the results
for row in rows:
record = {}
record["id"] = row[0]
record["name"] = row[1]
record["issue_number"] = row[2]
record["site_detail_url"] = row[3]
record["cover_date"] = row[4]
record["image"] = {}
record["image"]["super_url"] = row[5]
record["image"]["thumb_url"] = row[6]
record["description"] = row[7]
record = {
"id": row[0],
"name": row[1],
"issue_number": row[2],
"site_detail_url": row[3],
"cover_date": row[4],
"image": {"super_url": row[5], "thumb_url": row[6]},
"description": row[7],
}
results.append(record)

View File

@ -538,9 +538,20 @@ class ComicVineTalker:
# put in our own
string = string.replace("<br>", "\n")
string = string.replace("</li>", "\n")
string = string.replace("</p>", "\n\n")
string = string.replace("<h1>", "*")
string = string.replace("</h1>", "*\n")
string = string.replace("<h2>", "*")
string = string.replace("</h2>", "*\n")
string = string.replace("<h3>", "*")
string = string.replace("</h3>", "*\n")
string = string.replace("<h4>", "*")
string = string.replace("</h4>", "*\n")
string = string.replace("<h5>", "*")
string = string.replace("</h5>", "*\n")
string = string.replace("<h6>", "*")
string = string.replace("</h6>", "*\n")
# remove the tables
p = re.compile(r"<table[^<]*?>.*?</table>")
@ -633,16 +644,12 @@ class ComicVineTalker:
cv_response = self.get_cv_content(issue_url, params)
details: SelectDetails = {}
details["image_url"] = None
details["thumb_image_url"] = None
details["cover_date"] = None
details["site_detail_url"] = None
details["image_url"] = cv_response["results"]["image"]["super_url"]
details["thumb_image_url"] = cv_response["results"]["image"]["thumb_url"]
details["cover_date"] = cv_response["results"]["cover_date"]
details["site_detail_url"] = cv_response["results"]["site_detail_url"]
details: SelectDetails = {
"image_url": cv_response["results"]["image"]["super_url"],
"thumb_image_url": cv_response["results"]["image"]["thumb_url"],
"cover_date": cv_response["results"]["cover_date"],
"site_detail_url": cv_response["results"]["site_detail_url"],
}
if details["image_url"] is not None:
self.cache_issue_select_details(

View File

@ -14,10 +14,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import calendar
import datetime
import logging
import os
import re
import string
import sys
from pathvalidate import sanitize_filename
from comicapi.genericmetadata import GenericMetadata
from comicapi.issuestring import IssueString
@ -69,7 +74,7 @@ class FileRenamer:
return text.replace(token, "")
def determine_name(self, filename, ext=None):
def determine_name(self, ext):
md = self.metadata
new_name = self.template
@ -129,9 +134,6 @@ class FileRenamer:
# remove duplicate spaces (again!)
new_name = " ".join(new_name.split())
if ext is None:
ext = os.path.splitext(filename)[1]
new_name += ext
# some tweaks to keep various filesystems happy
@ -142,3 +144,155 @@ class FileRenamer:
new_name = new_name.replace("?", "")
return new_name
class MetadataFormatter(string.Formatter):
def __init__(self, smart_cleanup=False, platform="auto"):
super().__init__()
self.smart_cleanup = smart_cleanup
self.platform = platform
def format_field(self, value, format_spec):
if value is None or value == "":
return ""
return super().format_field(value, format_spec)
def _vformat(self, format_string, args, kwargs, used_args, recursion_depth, auto_arg_index=0):
if recursion_depth < 0:
raise ValueError("Max string recursion exceeded")
result = []
lstrip = False
for literal_text, field_name, format_spec, conversion in self.parse(format_string):
# output the literal text
if literal_text:
if lstrip:
literal_text = literal_text.lstrip("-_)}]#")
if self.smart_cleanup:
lspace = literal_text[0].isspace()
rspace = literal_text[-1].isspace()
literal_text = " ".join(literal_text.split())
if literal_text == "":
literal_text = " "
else:
if lspace:
literal_text = " " + literal_text
if rspace:
literal_text += " "
result.append(literal_text)
lstrip = False
# if there's a field, output it
if field_name is not None:
field_name = field_name.lower()
# this is some markup, find the object and do the formatting
# handle arg indexing when empty field_names are given.
if field_name == "":
if auto_arg_index is False:
raise ValueError("cannot switch from manual field specification to automatic field numbering")
field_name = str(auto_arg_index)
auto_arg_index += 1
elif field_name.isdigit():
if auto_arg_index:
raise ValueError("cannot switch from manual field specification to automatic field numbering")
# disable auto arg incrementing, if it gets used later on, then an exception will be raised
auto_arg_index = False
# given the field_name, find the object it references
# and the argument it came from
obj, arg_used = self.get_field(field_name, args, kwargs)
used_args.add(arg_used)
# do any conversion on the resulting object
obj = self.convert_field(obj, conversion)
# expand the format spec, if needed
format_spec, auto_arg_index = self._vformat(
format_spec, args, kwargs, used_args, recursion_depth - 1, auto_arg_index=auto_arg_index
)
# format the object and append to the result
fmt_obj = self.format_field(obj, format_spec)
if fmt_obj == "" and len(result) > 0 and self.smart_cleanup:
lstrip = True
result.pop()
if self.smart_cleanup:
fmt_obj = " ".join(fmt_obj.split())
fmt_obj = sanitize_filename(fmt_obj, platform=self.platform)
result.append(fmt_obj)
return "".join(result), auto_arg_index
class FileRenamer2:
def __init__(self, metadata, platform="auto"):
self.template = "{publisher}/{series}/{series} v{volume} #{issue} (of {issue_count}) ({year})"
self.smart_cleanup = True
self.issue_zero_padding = 3
self.metadata = metadata
self.move = False
self.platform = platform
def set_metadata(self, metadata: GenericMetadata):
self.metadata = metadata
def set_issue_zero_padding(self, count):
self.issue_zero_padding = count
def set_smart_cleanup(self, on):
self.smart_cleanup = on
def set_template(self, template: str):
self.template = template
def determine_name(self, ext):
class Default(dict):
def __missing__(self, key):
return "{" + key + "}"
md = self.metadata
# padding for issue
md.issue = IssueString(md.issue).as_string(pad=self.issue_zero_padding)
template = self.template
path_components = template.split(os.sep)
new_name = ""
fmt = MetadataFormatter(self.smart_cleanup, platform=self.platform)
md_dict = vars(md)
for role in ["writer", "penciller", "inker", "colorist", "letterer", "cover artist", "editor"]:
md_dict[role] = md.get_primary_credit(role)
if (isinstance(md.month, int) or isinstance(md.month, str) and md.month.isdigit()) and 0 < int(md.month) < 13:
md_dict["month_name"] = calendar.month_name[int(md.month)]
md_dict["month_abbr"] = calendar.month_abbr[int(md.month)]
else:
print(md.month)
md_dict["month_name"] = ""
md_dict["month_abbr"] = ""
for Component in path_components:
if (
self.platform.lower() in ["universal", "windows"] or sys.platform.lower() in ["windows"]
) and self.smart_cleanup:
# colons get special treatment
Component = Component.replace(": ", " - ")
Component = Component.replace(":", "-")
new_basename = sanitize_filename(
fmt.vformat(Component, args=[], kwargs=Default(md_dict)), platform=self.platform
).strip()
new_name = os.path.join(new_name, new_basename)
new_name += ext
new_basename += ext
# remove padding
md.issue = IssueString(md.issue).as_string()
if self.move:
return new_name.strip()
else:
return new_basename.strip()

View File

@ -120,7 +120,7 @@ class ImageFetcher:
def create_image_db(self):
# this will wipe out any existing version
open(self.db_file, "w").close()
open(self.db_file, "wb").close()
# wipe any existing image cache folder too
if os.path.isdir(self.cache_folder):

View File

@ -112,8 +112,8 @@ class IssueIdentifier:
def set_name_length_delta_threshold(self, delta):
self.length_delta_thresh = delta
def set_publisher_filter(self, filter):
self.publisher_filter = filter
def set_publisher_filter(self, flt):
self.publisher_filter = flt
def set_hasher_algorithm(self, algo):
self.image_hasher = algo
@ -164,12 +164,13 @@ class IssueIdentifier:
def get_search_keys(self):
ca = self.comic_archive
search_keys: SearchKeys = {}
search_keys["series"] = None
search_keys["issue_number"] = None
search_keys["month"] = None
search_keys["year"] = None
search_keys["issue_count"] = None
search_keys: SearchKeys = {
"series": None,
"issue_number": None,
"month": None,
"year": None,
"issue_count": None,
}
if ca is None:
return None
@ -274,10 +275,8 @@ class IssueIdentifier:
self.cover_url_callback(url_image_data)
remote_cover_list = []
item = {}
item["url"] = primary_img_url
item = {"url": primary_img_url, "hash": self.calculate_hash(url_image_data)}
item["hash"] = self.calculate_hash(url_image_data)
remote_cover_list.append(item)
if self.cancel:
@ -299,9 +298,7 @@ class IssueIdentifier:
if self.cover_url_callback is not None:
self.cover_url_callback(alt_url_image_data)
item = {}
item["url"] = alt_url
item["hash"] = self.calculate_hash(alt_url_image_data)
item = {"url": alt_url, "hash": self.calculate_hash(alt_url_image_data)}
remote_cover_list.append(item)
if self.cancel:
@ -317,10 +314,7 @@ class IssueIdentifier:
for local_cover_hash in local_cover_hash_list:
for remote_cover_item in remote_cover_list:
score = ImageHasher.hamming_distance(local_cover_hash, remote_cover_item["hash"])
score_item = {}
score_item["score"] = score
score_item["url"] = remote_cover_item["url"]
score_item["hash"] = remote_cover_item["hash"]
score_item = {"score": score, "url": remote_cover_item["url"], "hash": remote_cover_item["hash"]}
score_list.append(score_item)
if use_log:
self.log_msg(score, False)
@ -520,24 +514,25 @@ class IssueIdentifier:
self.match_list = []
return self.match_list
match: IssueResult = {}
match["series"] = f"{series['name']} ({series['start_year']})"
match["distance"] = score_item["score"]
match["issue_number"] = keys["issue_number"]
match["cv_issue_count"] = series["count_of_issues"]
match["url_image_hash"] = score_item["hash"]
match["issue_title"] = issue["name"]
match["issue_id"] = issue["id"]
match["volume_id"] = series["id"]
match["month"] = month
match["year"] = year
match["publisher"] = None
match: IssueResult = {
"series": f"{series['name']} ({series['start_year']})",
"distance": score_item["score"],
"issue_number": keys["issue_number"],
"cv_issue_count": series["count_of_issues"],
"url_image_hash": score_item["hash"],
"issue_title": issue["name"],
"issue_id": issue["id"],
"volume_id": series["id"],
"month": month,
"year": year,
"publisher": None,
"image_url": image_url,
"thumb_url": thumb_url,
"page_url": page_url,
"description": issue["description"],
}
if series["publisher"] is not None:
match["publisher"] = series["publisher"]["name"]
match["image_url"] = image_url
match["thumb_url"] = thumb_url
match["page_url"] = page_url
match["description"] = issue["description"]
self.match_list.append(match)

View File

@ -20,6 +20,7 @@ import logging
from PyQt5 import QtCore, QtWidgets, uic
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.ui import qtutils
logger = logging.getLogger(__name__)
@ -41,6 +42,9 @@ class LogWindow(QtWidgets.QDialog):
def set_text(self, text):
try:
text = text.decode()
except:
self.textEdit.setPlainText(text)
except AttributeError:
pass
self.textEdit.setPlainText(text)
except Exception as e:
logger.exception("Displaying raw tags failed")
qtutils.qt_error("Displaying raw tags failed:", e)

View File

@ -37,11 +37,55 @@ logger.setLevel(logging.DEBUG)
try:
qt_available = True
from PyQt5 import QtGui, QtWidgets
from PyQt5 import QtCore, QtGui, QtWidgets
def show_exception_box(log_msg):
"""Checks if a QApplication instance is available and shows a messagebox with the exception message.
If unavailable (non-console application), log an additional notice.
"""
if QtWidgets.QApplication.instance() is not None:
errorbox = QtWidgets.QMessageBox()
errorbox.setText("Oops. An unexpected error occured:\n{0}".format(log_msg))
errorbox.exec_()
QtWidgets.QApplication.exit(1)
else:
logger.debug("No QApplication instance available.")
class UncaughtHook(QtCore.QObject):
_exception_caught = QtCore.pyqtSignal(object)
def __init__(self, *args, **kwargs):
super(UncaughtHook, self).__init__(*args, **kwargs)
# this registers the exception_hook() function as hook with the Python interpreter
sys.excepthook = self.exception_hook
# connect signal to execute the message box function always on main thread
self._exception_caught.connect(show_exception_box)
def exception_hook(self, exc_type, exc_value, exc_traceback):
"""Function handling uncaught exceptions.
It is triggered each time an uncaught exception occurs.
"""
if issubclass(exc_type, KeyboardInterrupt):
# ignore keyboard interrupt to support console applications
sys.__excepthook__(exc_type, exc_value, exc_traceback)
else:
exc_info = (exc_type, exc_value, exc_traceback)
log_msg = "\n".join(
["".join(traceback.format_tb(exc_traceback)), "{0}: {1}".format(exc_type.__name__, exc_value)]
)
logger.critical(
"Uncaught exception: %s", "{0}: {1}".format(exc_type.__name__, exc_value), exc_info=exc_info
)
# trigger message box show
self._exception_caught.emit(log_msg)
qt_exception_hook = UncaughtHook()
from comictaggerlib.taggerwindow import TaggerWindow
except ImportError as e:
logging.debug(e)
logger.error(str(e))
qt_available = False
@ -51,6 +95,10 @@ def rotate(handler: logging.handlers.RotatingFileHandler, filename: pathlib.Path
def ctmain():
opts = Options()
opts.parse_cmd_line_args()
SETTINGS = ComicTaggerSettings(opts.config_path)
os.makedirs(ComicTaggerSettings.get_settings_folder() / "logs", exist_ok=True)
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.WARNING)
@ -67,11 +115,7 @@ def ctmain():
format="%(asctime)s | %(name)s | %(levelname)s | %(message)s",
datefmt="%Y-%m-%dT%H:%M:%S",
)
opts = Options()
opts.parse_cmd_line_args()
# Need to load setting before anything else
SETTINGS = ComicTaggerSettings()
# manage the CV API key
if opts.cv_api_key:
@ -106,7 +150,7 @@ def ctmain():
try:
cli.cli_mode(opts, SETTINGS)
except:
logger.exception()
logger.exception("CLI mode failed")
else:
os.environ["QtWidgets.QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
args = []
@ -149,7 +193,7 @@ def ctmain():
sys.exit(app.exec())
except Exception:
logger.exception()
logger.exception("GUI mode failed")
QtWidgets.QMessageBox.critical(
QtWidgets.QMainWindow(), "Error", "Unhandled exception in app:\n" + traceback.format_exc()
)

View File

@ -83,23 +83,24 @@ class PageListEditor(QtWidgets.QWidget):
self.reset_page()
# Add the entries to the manga combobox
self.comboBox.addItem("", "")
self.comboBox.addItem(self.pageTypeNames[PageType.FrontCover], PageType.FrontCover)
self.comboBox.addItem(self.pageTypeNames[PageType.InnerCover], PageType.InnerCover)
self.comboBox.addItem(self.pageTypeNames[PageType.Advertisement], PageType.Advertisement)
self.comboBox.addItem(self.pageTypeNames[PageType.Roundup], PageType.Roundup)
self.comboBox.addItem(self.pageTypeNames[PageType.Story], PageType.Story)
self.comboBox.addItem(self.pageTypeNames[PageType.Editorial], PageType.Editorial)
self.comboBox.addItem(self.pageTypeNames[PageType.Letters], PageType.Letters)
self.comboBox.addItem(self.pageTypeNames[PageType.Preview], PageType.Preview)
self.comboBox.addItem(self.pageTypeNames[PageType.BackCover], PageType.BackCover)
self.comboBox.addItem(self.pageTypeNames[PageType.Other], PageType.Other)
self.comboBox.addItem(self.pageTypeNames[PageType.Deleted], PageType.Deleted)
# Add the entries to the page type combobox
self.add_page_type_item("", "", "Alt+0", False)
self.add_page_type_item(self.pageTypeNames[PageType.FrontCover], PageType.FrontCover, "Alt+F")
self.add_page_type_item(self.pageTypeNames[PageType.InnerCover], PageType.InnerCover, "Alt+I")
self.add_page_type_item(self.pageTypeNames[PageType.Advertisement], PageType.Advertisement, "Alt+A")
self.add_page_type_item(self.pageTypeNames[PageType.Roundup], PageType.Roundup, "Alt+R")
self.add_page_type_item(self.pageTypeNames[PageType.Story], PageType.Story, "Alt+S")
self.add_page_type_item(self.pageTypeNames[PageType.Editorial], PageType.Editorial, "Alt+E")
self.add_page_type_item(self.pageTypeNames[PageType.Letters], PageType.Letters, "Alt+L")
self.add_page_type_item(self.pageTypeNames[PageType.Preview], PageType.Preview, "Alt+P")
self.add_page_type_item(self.pageTypeNames[PageType.BackCover], PageType.BackCover, "Alt+B")
self.add_page_type_item(self.pageTypeNames[PageType.Other], PageType.Other, "Alt+O")
self.add_page_type_item(self.pageTypeNames[PageType.Deleted], PageType.Deleted, "Alt+X")
self.listWidget.itemSelectionChanged.connect(self.change_page)
item_move_events(self.listWidget).connect(self.item_move_event)
self.comboBox.activated.connect(self.change_page_type)
self.cbPageType.activated.connect(self.change_page_type)
self.chkDoublePage.toggled.connect(self.toggle_double_page)
self.leBookmark.editingFinished.connect(self.save_bookmark)
self.btnUp.clicked.connect(self.move_current_up)
self.btnDown.clicked.connect(self.move_current_down)
@ -111,11 +112,27 @@ class PageListEditor(QtWidgets.QWidget):
def reset_page(self):
self.pageWidget.clear()
self.comboBox.setDisabled(True)
self.cbPageType.setDisabled(True)
self.chkDoublePage.setDisabled(True)
self.leBookmark.setDisabled(True)
self.comic_archive = None
self.pages_list = []
def add_page_type_item(self, text, user_data, shortcut, show_shortcut=True):
if show_shortcut:
text = text + " (" + shortcut + ")"
self.cbPageType.addItem(text, user_data)
actionItem = QtWidgets.QAction(
shortcut, self, triggered=lambda: self.select_page_type_item(self.cbPageType.findData(user_data))
)
actionItem.setShortcut(shortcut)
self.addAction(actionItem)
def select_page_type_item(self, idx):
if self.cbPageType.isEnabled():
self.cbPageType.setCurrentIndex(idx)
self.change_page_type(idx)
def get_new_indexes(self, movement):
selection = self.listWidget.selectionModel().selectedRows()
selection.sort(reverse=movement > 0)
@ -195,7 +212,7 @@ class PageListEditor(QtWidgets.QWidget):
self.modified.emit()
def change_page_type(self, i):
new_type = self.comboBox.itemData(i)
new_type = self.cbPageType.itemData(i)
if self.get_current_page_type() != new_type:
self.set_current_page_type(new_type)
self.emit_front_cover_change()
@ -205,8 +222,10 @@ class PageListEditor(QtWidgets.QWidget):
row = self.listWidget.currentRow()
pagetype = self.get_current_page_type()
i = self.comboBox.findData(pagetype)
self.comboBox.setCurrentIndex(i)
i = self.cbPageType.findData(pagetype)
self.cbPageType.setCurrentIndex(i)
self.chkDoublePage.setChecked("DoublePage" in self.listWidget.item(row).data(QtCore.Qt.UserRole)[0])
if "Bookmark" in self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]:
self.leBookmark.setText(self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]["Bookmark"])
@ -244,13 +263,30 @@ class PageListEditor(QtWidgets.QWidget):
if "Type" in page_dict:
del page_dict["Type"]
else:
page_dict["Type"] = str(t)
page_dict["Type"] = t
item = self.listWidget.item(row)
# wrap the dict in a tuple to keep from being converted to QtWidgets.QStrings
item.setData(QtCore.Qt.ItemDataRole.UserRole, (page_dict,))
item.setText(self.list_entry_text(page_dict))
def toggle_double_page(self):
row = self.listWidget.currentRow()
page_dict = self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]
if self.sender().isChecked():
page_dict["DoublePage"] = str("true")
elif "DoublePage" in page_dict:
del page_dict["DoublePage"]
self.modified.emit()
item = self.listWidget.item(row)
# wrap the dict in a tuple to keep from being converted to QStrings
item.setData(QtCore.Qt.UserRole, (page_dict,))
item.setText(self.list_entry_text(page_dict))
self.listWidget.setFocus()
def save_bookmark(self):
row = self.listWidget.currentRow()
page_dict = self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]
@ -279,7 +315,8 @@ class PageListEditor(QtWidgets.QWidget):
self.comic_archive = comic_archive
self.pages_list = pages_list
if pages_list is not None and len(pages_list) > 0:
self.comboBox.setDisabled(False)
self.cbPageType.setDisabled(False)
self.chkDoublePage.setDisabled(False)
self.leBookmark.setDisabled(False)
self.listWidget.itemSelectionChanged.disconnect(self.change_page)
@ -302,6 +339,8 @@ class PageListEditor(QtWidgets.QWidget):
text += " (" + self.pageTypeNames[page_dict["Type"]] + ")"
else:
text += " (Error: " + page_dict["Type"] + ")"
if "DoublePage" in page_dict:
text += " " + "\U00002461"
if "Bookmark" in page_dict:
text += " " + "\U0001F516"
return text
@ -322,15 +361,16 @@ class PageListEditor(QtWidgets.QWidget):
# depending on the current data style, certain fields are disabled
inactive_color = QtGui.QColor(255, 170, 150)
active_palette = self.comboBox.palette()
active_palette = self.cbPageType.palette()
inactive_palette3 = self.comboBox.palette()
inactive_palette3 = self.cbPageType.palette()
inactive_palette3.setColor(QtGui.QPalette.ColorRole.Base, inactive_color)
if data_style == MetaDataStyle.CIX:
self.btnUp.setEnabled(True)
self.btnDown.setEnabled(True)
self.comboBox.setEnabled(True)
self.cbPageType.setEnabled(True)
self.chkDoublePage.setEnabled(True)
self.leBookmark.setEnabled(True)
self.listWidget.setEnabled(True)
@ -340,7 +380,8 @@ class PageListEditor(QtWidgets.QWidget):
elif data_style == MetaDataStyle.CBI:
self.btnUp.setEnabled(False)
self.btnDown.setEnabled(False)
self.comboBox.setEnabled(False)
self.cbPageType.setEnabled(False)
self.chkDoublePage.setEnabled(False)
self.leBookmark.setEnabled(False)
self.listWidget.setEnabled(False)
@ -352,5 +393,6 @@ class PageListEditor(QtWidgets.QWidget):
# make sure combo is disabled when no list
if self.comic_archive is None:
self.comboBox.setEnabled(False)
self.cbPageType.setEnabled(False)
self.chkDoublePage.setEnabled(False)
self.leBookmark.setEnabled(False)

View File

@ -23,7 +23,7 @@ from PyQt5 import QtCore, QtWidgets, uic
import comicapi.comicarchive
from comicapi import utils
from comicapi.comicarchive import MetaDataStyle
from comictaggerlib.filerenamer import FileRenamer
from comictaggerlib.filerenamer import FileRenamer, FileRenamer2
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.settingswindow import SettingsWindow
from comictaggerlib.ui.qtutils import center_window_on_parent
@ -52,7 +52,11 @@ class RenameWindow(QtWidgets.QDialog):
self.rename_list = []
self.btnSettings.clicked.connect(self.modify_settings)
self.renamer = FileRenamer(None)
if self.settings.rename_new_renamer:
self.renamer = FileRenamer2(None, platform="universal" if self.settings.rename_strict else "auto")
else:
self.renamer = FileRenamer(None)
self.config_renamer()
self.do_preview()
@ -82,7 +86,22 @@ class RenameWindow(QtWidgets.QDialog):
if md.is_empty:
md = ca.metadata_from_filename(self.settings.parse_scan_info)
self.renamer.set_metadata(md)
new_name = self.renamer.determine_name(ca.path, ext=new_ext)
self.renamer.move = self.settings.rename_move_dir
try:
new_name = self.renamer.determine_name(new_ext)
except Exception as e:
QtWidgets.QMessageBox.critical(
self,
"Invalid format string!",
"Your rename template is invalid!"
"<br/><br/>{}<br/><br/>"
"Please consult the template help in the "
"settings and the documentation on the format at "
"<a href='https://docs.python.org/3/library/string.html#format-string-syntax'>"
"https://docs.python.org/3/library/string.html#format-string-syntax</a>".format(e),
)
return
row = self.twList.rowCount()
self.twList.insertRow(row)
@ -150,7 +169,13 @@ class RenameWindow(QtWidgets.QDialog):
center_window_on_parent(prog_dialog)
QtCore.QCoreApplication.processEvents()
if item["new_name"] == os.path.basename(item["archive"].path):
folder = os.path.dirname(os.path.abspath(item["archive"].path))
if self.settings.rename_move_dir and len(self.settings.rename_dir.strip()) > 3:
folder = self.settings.rename_dir.strip()
new_abs_path = utils.unique_file(os.path.join(folder, item["new_name"]))
if os.path.join(folder, item["new_name"]) == item["archive"].path:
print(item["new_name"], "Filename is already good!")
logger.info(item["new_name"], "Filename is already good!")
continue
@ -158,9 +183,7 @@ class RenameWindow(QtWidgets.QDialog):
if not item["archive"].is_writable(check_rar_status=False):
continue
folder = os.path.dirname(os.path.abspath(item["archive"].path))
new_abs_path = utils.unique_file(os.path.join(folder, item["new_name"]))
os.makedirs(os.path.dirname(new_abs_path), 0o777, True)
os.rename(item["archive"].path, new_abs_path)
item["archive"].rename(new_abs_path)

View File

@ -28,13 +28,16 @@ logger = logging.getLogger(__name__)
class ComicTaggerSettings:
folder = ""
@staticmethod
def get_settings_folder():
if platform.system() == "Windows":
folder = os.path.join(os.environ["APPDATA"], "ComicTagger")
else:
folder = os.path.join(os.path.expanduser("~"), ".ComicTagger")
return pathlib.Path(folder)
if not ComicTaggerSettings.folder:
if platform.system() == "Windows":
ComicTaggerSettings.folder = os.path.join(os.environ["APPDATA"], "ComicTagger")
else:
ComicTaggerSettings.folder = os.path.join(os.path.expanduser("~"), ".ComicTagger")
return pathlib.Path(ComicTaggerSettings.folder)
@staticmethod
def base_dir():
@ -83,6 +86,7 @@ class ComicTaggerSettings:
self.show_disclaimer = True
self.dont_notify_about_this_version = ""
self.ask_about_usage_stats = True
self.hide_rename_message = False
# filename parsing settings
self.parse_scan_info = True
@ -114,6 +118,10 @@ class ComicTaggerSettings:
self.rename_issue_number_padding = 3
self.rename_use_smart_string_cleanup = True
self.rename_extension_based_on_archive = True
self.rename_dir = ""
self.rename_move_dir = False
self.rename_new_renamer = False
self.rename_strict = False
# Auto-tag stickies
self.save_on_low_confidence = False
@ -123,10 +131,7 @@ class ComicTaggerSettings:
self.remove_archive_after_successful_match = False
self.wait_and_retry_on_rate_limit = False
def __init__(self):
self.settings_file = ""
self.folder = ""
def __init__(self, folder):
# General Settings
self.rar_exe_path = ""
self.allow_cbi_in_rar = True
@ -156,6 +161,7 @@ class ComicTaggerSettings:
self.show_disclaimer = True
self.dont_notify_about_this_version = ""
self.ask_about_usage_stats = True
self.hide_rename_message = False
# filename parsing settings
self.parse_scan_info = True
@ -187,6 +193,10 @@ class ComicTaggerSettings:
self.rename_issue_number_padding = 3
self.rename_use_smart_string_cleanup = True
self.rename_extension_based_on_archive = True
self.rename_dir = ""
self.rename_move_dir = False
self.rename_new_renamer = False
self.rename_strict = False
# Auto-tag stickies
self.save_on_low_confidence = False
@ -197,12 +207,15 @@ class ComicTaggerSettings:
self.wait_and_retry_on_rate_limit = False
self.config = configparser.RawConfigParser()
self.folder = ComicTaggerSettings.get_settings_folder()
if folder:
ComicTaggerSettings.folder = pathlib.Path(folder)
else:
ComicTaggerSettings.folder = ComicTaggerSettings.get_settings_folder()
if not os.path.exists(self.folder):
os.makedirs(self.folder)
if not os.path.exists(ComicTaggerSettings.folder):
os.makedirs(ComicTaggerSettings.folder)
self.settings_file = os.path.join(self.folder, "settings")
self.settings_file = os.path.join(ComicTaggerSettings.folder, "settings")
# if config file doesn't exist, write one out
if not os.path.exists(self.settings_file):
@ -239,7 +252,7 @@ class ComicTaggerSettings:
yield line
line = f.readline()
with open(self.settings_file, "r") as f:
with open(self.settings_file, "r", encoding="utf-8") as f:
self.config.read_file(readline_generator(f))
self.rar_exe_path = self.config.get("settings", "rar_exe_path")
@ -289,6 +302,8 @@ class ComicTaggerSettings:
self.dont_notify_about_this_version = self.config.get("dialogflags", "dont_notify_about_this_version")
if self.config.has_option("dialogflags", "ask_about_usage_stats"):
self.ask_about_usage_stats = self.config.getboolean("dialogflags", "ask_about_usage_stats")
if self.config.has_option("dialogflags", "hide_rename_message"):
self.hide_rename_message = self.config.getboolean("dialogflags", "hide_rename_message")
if self.config.has_option("comicvine", "use_series_start_as_volume"):
self.use_series_start_as_volume = self.config.getboolean("comicvine", "use_series_start_as_volume")
@ -344,6 +359,14 @@ class ComicTaggerSettings:
self.rename_extension_based_on_archive = self.config.getboolean(
"rename", "rename_extension_based_on_archive"
)
if self.config.has_option("rename", "rename_dir"):
self.rename_dir = self.config.get("rename", "rename_dir")
if self.config.has_option("rename", "rename_move_dir"):
self.rename_move_dir = self.config.getboolean("rename", "rename_move_dir")
if self.config.has_option("rename", "rename_new_renamer"):
self.rename_new_renamer = self.config.getboolean("rename", "rename_new_renamer")
if self.config.has_option("rename", "rename_strict"):
self.rename_strict = self.config.getboolean("rename", "rename_strict")
if self.config.has_option("autotag", "save_on_low_confidence"):
self.save_on_low_confidence = self.config.getboolean("autotag", "save_on_low_confidence")
@ -400,6 +423,7 @@ class ComicTaggerSettings:
self.config.set("dialogflags", "show_disclaimer", self.show_disclaimer)
self.config.set("dialogflags", "dont_notify_about_this_version", self.dont_notify_about_this_version)
self.config.set("dialogflags", "ask_about_usage_stats", self.ask_about_usage_stats)
self.config.set("dialogflags", "hide_rename_message", self.hide_rename_message)
if not self.config.has_section("filenameparser"):
self.config.add_section("filenameparser")
@ -441,6 +465,10 @@ class ComicTaggerSettings:
self.config.set("rename", "rename_issue_number_padding", self.rename_issue_number_padding)
self.config.set("rename", "rename_use_smart_string_cleanup", self.rename_use_smart_string_cleanup)
self.config.set("rename", "rename_extension_based_on_archive", self.rename_extension_based_on_archive)
self.config.set("rename", "rename_dir", self.rename_dir)
self.config.set("rename", "rename_move_dir", self.rename_move_dir)
self.config.set("rename", "rename_new_renamer", self.rename_new_renamer)
self.config.set("rename", "rename_strict", self.rename_strict)
if not self.config.has_section("autotag"):
self.config.add_section("autotag")
@ -451,5 +479,5 @@ class ComicTaggerSettings:
self.config.set("autotag", "remove_archive_after_successful_match", self.remove_archive_after_successful_match)
self.config.set("autotag", "wait_and_retry_on_rate_limit", self.wait_and_retry_on_rate_limit)
with open(self.settings_file, "w") as configfile:
with open(self.settings_file, "w", encoding="utf-8") as configfile:
self.config.write(configfile)

View File

@ -17,12 +17,15 @@
import logging
import os
import platform
import re
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comicapi import utils
from comicapi.genericmetadata import md_test
from comictaggerlib.comicvinecacher import ComicVineCacher
from comictaggerlib.comicvinetalker import ComicVineTalker
from comictaggerlib.filerenamer import FileRenamer, FileRenamer2
from comictaggerlib.imagefetcher import ImageFetcher
from comictaggerlib.settings import ComicTaggerSettings
@ -54,6 +57,82 @@ macRarHelp = """
</p>Once homebrew is installed, run: <b>brew install caskroom/cask/rar</b></body></html>
"""
old_template_tooltip = """
<html><head/><body><p>The template for the new filename. Accepts the following variables:</p><p>%series%<br/>%issue%<br/>%volume%<br/>%issuecount%<br/>%year%<br/>%month%<br/>%month_name%<br/>%publisher%<br/>%title%<br/>
%genre%<br/>
%language_code%<br/>
%criticalrating%<br/>
%alternateseries%<br/>
%alternatenumber%<br/>
%alternatecount%<br/>
%imprint%<br/>
%format%<br/>
%maturityrating%<br/>
%storyarc%<br/>
%seriesgroup%<br/>
%scaninfo%
</p><p>Examples:</p><p><span style=&quot; font-style:italic;&quot;>%series% %issue% (%year%)</span><br/><span style=&quot; font-style:italic;&quot;>%series% #%issue% - %title%</span></p></body></html>
"""
new_template_tooltip = """
<pre>The template for the new filename. Uses python format strings https://docs.python.org/3/library/string.html#format-string-syntax
Accepts the following variables:
{is_empty} (boolean)
{tag_origin} (string)
{series} (string)
{issue} (string)
{title} (string)
{publisher} (string)
{month} (integer)
{year} (integer)
{day} (integer)
{issue_count} (integer)
{volume} (integer)
{genre} (string)
{language} (string)
{comments} (string)
{volume_count} (integer)
{critical_rating} (string)
{country} (string)
{alternate_series} (string)
{alternate_number} (string)
{alternate_count} (integer)
{imprint} (string)
{notes} (string)
{web_link} (string)
{format} (string)
{manga} (string)
{black_and_white} (boolean)
{page_count} (integer)
{maturity_rating} (string)
{story_arc} (string)
{series_group} (string)
{scan_info} (string)
{characters} (string)
{teams} (string)
{locations} (string)
{credits} (list of dict({&apos;role&apos;: string, &apos;person&apos;: string, &apos;primary&apos;: boolean}))
{tags} (list of str)
{pages} (list of dict({&apos;Image&apos;: string(int), &apos;Type&apos;: string}))
CoMet-only items:
{price} (float)
{is_version_of} (string)
{rights} (string)
{identifier} (string)
{last_mark} (string)
{cover_image} (string)
Examples:
{series} {issue} ({year})
Spider-Geddon 1 (2018)
{series} #{issue} - {title}
Spider-Geddon #1 - New Players; Check In
</pre>
"""
class SettingsWindow(QtWidgets.QDialog):
def __init__(self, parent, settings):
@ -105,15 +184,61 @@ class SettingsWindow(QtWidgets.QDialog):
validator = QtGui.QIntValidator(0, 99, self)
self.leNameLengthDeltaThresh.setValidator(validator)
self.settings_to_form()
new_rename = self.settings.rename_new_renamer
self.cbxNewRenamer.setChecked(new_rename)
self.cbxMoveFiles.setEnabled(new_rename)
self.leDirectory.setEnabled(new_rename)
self.lblDirectory.setEnabled(new_rename)
if self.settings.rename_new_renamer:
self.leRenameTemplate.setToolTip(new_template_tooltip)
self.settings_to_form()
self.rename_error = None
self.rename_test()
self.cbxNewRenamer.clicked.connect(self.new_rename_toggle)
self.btnBrowseRar.clicked.connect(self.select_rar)
self.btnClearCache.clicked.connect(self.clear_cache)
self.btnResetSettings.clicked.connect(self.reset_settings)
self.btnTestKey.clicked.connect(self.test_api_key)
self.btnTemplateHelp.clicked.connect(self.show_template_help)
self.leRenameTemplate.textEdited.connect(self.rename__test)
self.cbxMoveFiles.clicked.connect(self.rename_test)
self.cbxRenameStrict.clicked.connect(self.rename_test)
self.leDirectory.textEdited.connect(self.rename_test)
def new_rename_toggle(self):
new_rename = self.cbxNewRenamer.isChecked()
if new_rename:
self.leRenameTemplate.setText(re.sub(r"%(\w+)%", r"{\1}", self.leRenameTemplate.text()))
self.leRenameTemplate.setToolTip(new_template_tooltip)
else:
self.leRenameTemplate.setText(re.sub(r"{(\w+)}", r"%\1%", self.leRenameTemplate.text()))
self.leRenameTemplate.setToolTip(old_template_tooltip)
self.cbxMoveFiles.setEnabled(new_rename)
self.leDirectory.setEnabled(new_rename)
self.lblDirectory.setEnabled(new_rename)
self.rename_test()
def rename_test(self):
self.rename__test(self.leRenameTemplate.text())
def rename__test(self, template):
fr = FileRenamer(md_test)
if self.cbxNewRenamer.isChecked():
fr = FileRenamer2(md_test, platform="universal" if self.cbxRenameStrict.isChecked() else "auto")
fr.move = self.cbxMoveFiles.isChecked()
fr.set_template(template)
fr.set_issue_zero_padding(int(self.leIssueNumPadding.text()))
fr.set_smart_cleanup(self.cbxSmartCleanup.isChecked())
try:
self.lblRenameTest.setText(fr.determine_name(".cbz"))
self.rename_error = None
except Exception as e:
self.rename_error = e
self.lblRenameTest.setText(str(e))
def settings_to_form(self):
# Copy values from settings to form
self.leRarExePath.setText(self.settings.rar_exe_path)
self.leNameLengthDeltaThresh.setText(str(self.settings.id_length_delta_thresh))
@ -166,8 +291,26 @@ class SettingsWindow(QtWidgets.QDialog):
self.cbxSmartCleanup.setCheckState(QtCore.Qt.CheckState.Checked)
if self.settings.rename_extension_based_on_archive:
self.cbxChangeExtension.setCheckState(QtCore.Qt.CheckState.Checked)
if self.settings.rename_move_dir:
self.cbxMoveFiles.setCheckState(QtCore.Qt.CheckState.Checked)
self.leDirectory.setText(self.settings.rename_dir)
if self.settings.rename_strict:
self.cbxRenameStrict.setCheckState(QtCore.Qt.CheckState.Checked)
def accept(self):
self.rename_test()
if self.rename_error is not None:
QtWidgets.QMessageBox.critical(
self,
"Invalid format string!",
"Your rename template is invalid!"
"<br/><br/>{}<br/><br/>"
"Please consult the template help in the "
"settings and the documentation on the format at "
"<a href='https://docs.python.org/3/library/string.html#format-string-syntax'>"
"https://docs.python.org/3/library/string.html#format-string-syntax</a>".format(self.rename_error),
)
return
# Copy values from form to settings and save
self.settings.rar_exe_path = str(self.leRarExePath.text())
@ -213,6 +356,11 @@ class SettingsWindow(QtWidgets.QDialog):
self.settings.rename_issue_number_padding = int(self.leIssueNumPadding.text())
self.settings.rename_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
self.settings.rename_extension_based_on_archive = self.cbxChangeExtension.isChecked()
self.settings.rename_move_dir = self.cbxMoveFiles.isChecked()
self.settings.rename_dir = self.leDirectory.text()
self.settings.rename_new_renamer = self.cbxNewRenamer.isChecked()
self.settings.rename_strict = self.cbxRenameStrict.isChecked()
self.settings.save()
QtWidgets.QDialog.accept(self)
@ -262,3 +410,17 @@ class SettingsWindow(QtWidgets.QDialog):
def show_rename_tab(self):
self.tabWidget.setCurrentIndex(5)
def show_template_help(self):
template_help_win = TemplateHelpWindow(self)
template_help_win.setModal(False)
if not self.cbxNewRenamer.isChecked():
template_help_win.textEdit.setHtml(old_template_tooltip)
template_help_win.show()
class TemplateHelpWindow(QtWidgets.QDialog):
def __init__(self, parent):
super(TemplateHelpWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.get_ui_file("TemplateHelp.ui"), self)

View File

@ -246,6 +246,15 @@ Have fun!
""",
)
self.settings.show_disclaimer = not checked
if not self.settings.hide_rename_message:
self.settings.hide_rename_message = OptionalMessageDialog.msg(
self,
"New rename template!",
"There is a new rename template format available. "
"Please use the settings window to enable and test if you use this feature.<br><br>"
"The old rename template format will be removed in the next release, "
"please reference the template help button in the settings or <a href='https://github.com/comictagger/comictagger/wiki/UserGuide#rename'>https://github.com/comictagger/comictagger/wiki/UserGuide#rename</a>",
)
if self.settings.check_for_new_version:
# self.checkLatestVersionOnline()
@ -360,6 +369,12 @@ Have fun!
self.actionApplyCBLTransform.setStatusTip("Modify tags specifically for CBL format")
self.actionApplyCBLTransform.triggered.connect(self.apply_cbl_transform)
self.actionReCalcPageDims.setShortcut("Ctrl+R")
self.actionReCalcPageDims.setStatusTip(
"Trigger re-calculating image size, height and width for all pages on the next save"
)
self.actionReCalcPageDims.triggered.connect(self.recalc_page_dimensions)
self.actionClearEntryForm.setShortcut("Ctrl+Shift+C")
self.actionClearEntryForm.setStatusTip("Clear all the data on the screen")
self.actionClearEntryForm.triggered.connect(self.clear_form)
@ -589,6 +604,7 @@ Please choose options below, and select OK.
self.actionAutoIdentify.setEnabled(False)
self.actionRename.setEnabled(False)
self.actionApplyCBLTransform.setEnabled(False)
self.actionReCalcPageDims.setEnabled(False)
# now, selectively re-enable
if self.comic_archive is not None:
@ -600,6 +616,7 @@ Please choose options below, and select OK.
self.actionAutoTag.setEnabled(True)
self.actionRename.setEnabled(True)
self.actionApplyCBLTransform.setEnabled(True)
self.actionReCalcPageDims.setEnabled(True)
self.actionRepackage.setEnabled(True)
self.actionRemoveAuto.setEnabled(True)
self.actionRemoveCRTags.setEnabled(True)
@ -1363,6 +1380,7 @@ Please choose options below, and select OK.
self.cbMaturityRating.addItem("PG", "")
self.cbMaturityRating.addItem("Kids to Adults", "")
self.cbMaturityRating.addItem("Teen", "")
self.cbMaturityRating.addItem("M", "")
self.cbMaturityRating.addItem("MA15+", "")
self.cbMaturityRating.addItem("Mature 17+", "")
self.cbMaturityRating.addItem("R18+", "")
@ -1928,6 +1946,18 @@ Please choose options below, and select OK to Auto-Tag.
self.metadata = CBLTransformer(self.metadata, self.settings).apply()
self.metadata_to_form()
def recalc_page_dimensions(self):
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
for p in self.metadata.pages:
if "ImageSize" in p:
del p["ImageSize"]
if "ImageHeight" in p:
del p["ImageHeight"]
if "ImageWidth" in p:
del p["ImageWidth"]
self.set_dirty_flag()
QtWidgets.QApplication.restoreOverrideCursor()
def rename_archive(self):
ca_list = self.fileSelectionList.get_selected_archive_list()

View File

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>702</width>
<height>452</height>
</rect>
</property>
<property name="windowTitle">
<string>Template Help</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<item>
<widget class="QTextBrowser" name="textEdit">
<property name="readOnly">
<bool>true</bool>
</property>
<property name="html">
<string>&lt;html&gt;
&lt;head&gt;
&lt;style&gt;
table {
font-family: arial, sans-serif;
border-collapse: collapse;
width: 100%;
}
td, th {
border: 1px solid #dddddd;
text-align: left;
padding: 8px;
}
tr:nth-child(even) {
background-color: #dddddd;
}
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1 style="text-align: center"&gt;Template help&lt;/h1&gt;
&lt;p&gt;The template uses Python format strings, in the simplest use it replaces the field (e.g. {issue}) with the value for that particular comic (e.g. 1) for advanced formatting please reference the
&lt;a href="https://docs.python.org/3/library/string.html#format-string-syntax"&gt;Python 3 documentation&lt;/a&gt;&lt;/p&gt;
Accepts the following variables:
&lt;table&gt;
&lt;tr&gt;
&lt;th&gt;Tag name&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{is_empty}&lt;/td&gt;&lt;td&gt;boolean&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{tag_origin}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{series}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{issue}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{title}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{publisher}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{month}&lt;/td&gt;&lt;td&gt;integer&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{year}&lt;/td&gt;&lt;td&gt;integer&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{day}&lt;/td&gt;&lt;td&gt;integer&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{issue_count}&lt;/td&gt;&lt;td&gt;integer&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{volume}&lt;/td&gt;&lt;td&gt;integer&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{genre}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{language}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{comments}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{volume_count}&lt;/td&gt;&lt;td&gt;integer&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{critical_rating}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{country}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{alternate_series}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{alternate_number}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{alternate_count}&lt;/td&gt;&lt;td&gt;integer&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{imprint}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{notes}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{web_link}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{format}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{manga}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{black_and_white}&lt;/td&gt;&lt;td&gt;boolean&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{page_count}&lt;/td&gt;&lt;td&gt;integer&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{maturity_rating}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{story_arc}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{series_group}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{scan_info}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{characters}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{teams}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{locations}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{credits}&lt;/td&gt;&lt;td&gt;list of dict({&apos;role&apos;: string, &apos;person&apos;: string, &apos;primary&apos;: boolean})&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{tags}&lt;/td&gt;&lt;td&gt;list of str&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{pages}&lt;/td&gt;&lt;td&gt;list of dict({&apos;Image&apos;: string(int), &apos;Type&apos;: string})&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{price}&lt;/td&gt;&lt;td&gt;float&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{is_version_of}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{rights}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{identifier}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{last_mark}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;{cover_image}&lt;/td&gt;&lt;td&gt;string&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;pre&gt;
Examples:
{series} {issue} ({year})
Spider-Geddon 1 (2018)
{series} #{issue} - {title}
Spider-Geddon #1 - New Players; Check In
&lt;/pre&gt;
&lt;/body&gt;
&lt;/html&gt;</string></property>
<property name="textInteractionFlags">
<set>Qt::TextBrowserInteraction</set>
</property>
<property name="openExternalLinks" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -87,25 +87,38 @@
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QFormLayout" name="formLayout">
<layout class="QGridLayout" name="gridLayout_1">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<widget class="QLabel" name="lblPageType">
<property name="text">
<string>Page Type:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight</set>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox"/>
<widget class="QComboBox" name="cbPageType"/>
</item>
<item row="0" column="2">
<widget class="QCheckBox" name="chkDoublePage">
<property name="text">
<string>&amp;Double Page?</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<widget class="QLabel" name="lblBookmark">
<property name="text">
<string>Bookmark:</string>
<string>Book&amp;mark:</string>
</property>
<property name="buddy">
<cstring>leBookmark</cstring>
</property>
</widget>
</item>

View File

@ -2,13 +2,14 @@
import io
import logging
import traceback
from comictaggerlib.settings import ComicTaggerSettings
logger = logging.getLogger(__name__)
try:
from PyQt5 import QtGui
from PyQt5 import QtGui, QtWidgets
qt_available = True
except ImportError:
@ -74,3 +75,10 @@ if qt_available:
if not success:
img.load(ComicTaggerSettings.get_graphic("nocover.png"))
return img
def qt_error(msg: str, e: Exception = None):
trace = ""
if e:
trace = "\n".join(traceback.format_exception(type(e), e, e.__traceback__))
QtWidgets.QMessageBox.critical(QtWidgets.QMainWindow(), "Error", msg + trace)

View File

@ -28,7 +28,7 @@
</sizepolicy>
</property>
<property name="currentIndex">
<number>1</number>
<number>0</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
@ -547,7 +547,7 @@
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="1" column="0">
<widget class="QLabel" name="label">
<widget class="QLabel" name="lblTemplate">
<property name="text">
<string>Template:</string>
</property>
@ -574,13 +574,33 @@
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<widget class="QPushButton" name="btnTemplateHelp">
<property name="text">
<string>Template Help</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="lblRenameTest">
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="lblPadding">
<property name="text">
<string>Issue # Zero Padding</string>
</property>
</widget>
</item>
<item row="2" column="1">
<item row="3" column="1">
<widget class="QLineEdit" name="leIssueNumPadding">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
@ -599,7 +619,7 @@
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="cbxSmartCleanup">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;&amp;quot;Smart Text Cleanup&amp;quot; &lt;/span&gt;will attempt to clean up the new filename if there are missing fields from the template. For example, removing empty braces, repeated spaces and dashes, and more. Experimental feature.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@ -609,13 +629,51 @@
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<item row="5" column="0" colspan="2">
<widget class="QCheckBox" name="cbxChangeExtension">
<property name="text">
<string>Change Extension Based On Archive Type</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="cbxNewRenamer">
<property name="text">
<string>Enable the new renamer</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QCheckBox" name="cbxMoveFiles">
<property name="toolTip">
<string>If checked moves files to specified folder</string>
</property>
<property name="text">
<string>Move files when renaming</string>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="lblDirectory">
<property name="text">
<string>Destination Directory:</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QLineEdit" name="leDirectory"/>
</item>
<item row="8" column="0">
<widget class="QCheckBox" name="cbxRenameStrict">
<property name="toolTip">
<string>If checked will ensure reserved characters and filenames are removed for all Operating Systems.
By default only removes restricted characters and filenames for the current Operating System.</string>
</property>
<property name="text">
<string>Strict renaming</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>

View File

@ -1198,6 +1198,7 @@
<addaction name="actionAutoIdentify"/>
<addaction name="separator"/>
<addaction name="actionApplyCBLTransform"/>
<addaction name="actionReCalcPageDims"/>
</widget>
<widget class="QMenu" name="menuWindow">
<property name="title">
@ -1372,6 +1373,11 @@
<string>Apply CBL Transform</string>
</property>
</action>
<action name="actionReCalcPageDims">
<property name="text">
<string>Re-Calculate Page Dimensions</string>
</property>
</action>
<action name="actionLoadFolder">
<property name="text">
<string>Open Folder</string>

View File

@ -130,6 +130,11 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
self.immediate_autoselect = autoselect
self.cover_index_list = cover_index_list
self.cv_search_results = None
self.ii = None
self.iddialog = None
self.id_thread = None
self.progdialog = None
self.search_thread = None
self.use_filter = self.settings.always_use_publisher_filter

View File

@ -23,9 +23,9 @@ def _lang_code_mac():
# - The macOS underlying API:
# https://developer.apple.com/documentation/foundation/nsuserdefaults.
LANG_DETECT_COMMAND = "defaults read -g AppleLocale"
lang_detect_command = "defaults read -g AppleLocale"
status, output = subprocess.getstatusoutput(LANG_DETECT_COMMAND)
status, output = subprocess.getstatusoutput(lang_detect_command)
if status == 0:
# Command was successful.
lang_code = output

View File

@ -2,5 +2,6 @@ beautifulsoup4 >= 4.1
natsort>=8.1.0
pillow>=4.3.0
requests==2.*
pathvalidate
pycountry
py7zr

View File

@ -4,4 +4,6 @@ setuptools_scm[toml]>=3.4
wheel
black>=22
flake8==4.*
flake8-encodings
isort>=5.10
pytest==7.*

View File

@ -24,7 +24,7 @@ def read(fname):
str
File contents.
"""
with open(os.path.join(os.path.dirname(__file__), fname)) as f:
with open(os.path.join(os.path.dirname(__file__), fname), encoding="utf-8") as f:
return f.read()

BIN
tests/data/fake_cbr.cbr Normal file

Binary file not shown.

595
tests/filenames.py Normal file
View File

@ -0,0 +1,595 @@
import pytest
fnames = [
(
"Monster_Island_v1_2__repaired__c2c.cbz",
"stuff",
{
"issue": "2",
"series": "Monster Island",
"title": "The Wrath of Foobar-Man, Part 1 of 2",
"volume": "1",
"year": "",
"remainder": "repaired c2c",
"issue_count": "",
},
),
(
"Monster Island v1 3 (1957) -- The Revenge Of King Klong (noads).cbz",
"stuff",
{
"issue": "3",
"series": "Monster Island",
"title": "The Wrath of Foobar-Man, Part 1 of 2",
"volume": "1",
"year": "1957",
"remainder": "The Revenge Of King Klong (noads)",
"issue_count": "",
},
),
pytest.param(
"Foobar-Man Annual 121 - The Wrath of Foobar-Man, Part 1 of 2.cbz",
"stuff",
{
"issue": "121",
"series": "Foobar-Man Annual",
"title": "The Wrath of Foobar-Man, Part 1 of 2",
"volume": "",
"year": "",
"remainder": "",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
(
"Plastic Man v1 002 (1942).cbz",
"stuff",
{
"issue": "2",
"series": "Plastic Man",
"title": "",
"volume": "1",
"year": "1942",
"remainder": "",
"issue_count": "",
},
),
(
"Blue Beetle 02.cbr",
"stuff",
{
"issue": "2",
"series": "Blue Beetle",
"title": "",
"volume": "",
"year": "",
"remainder": "",
"issue_count": "",
},
),
(
"Monster Island vol. 2 #2.cbz",
"stuff",
{
"issue": "2",
"series": "Monster Island",
"title": "",
"volume": "2",
"year": "",
"remainder": "",
"issue_count": "",
},
),
(
"Crazy Weird Comics 2 (of 2) (1969).rar",
"stuff",
{
"issue": "2",
"series": "Crazy Weird Comics",
"title": "",
"volume": "",
"year": "1969",
"remainder": "",
"issue_count": "2",
},
),
(
"Super Strange Yarns (1957) #92 (1969).cbz",
"stuff",
{
"issue": "92",
"series": "Super Strange Yarns",
"title": "",
"volume": "1957",
"year": "1969",
"remainder": "",
"issue_count": "",
},
),
(
"Action Spy Tales v1965 #3.cbr",
"stuff",
{
"issue": "3",
"series": "Action Spy Tales",
"title": "",
"volume": "1965",
"year": "",
"remainder": "",
"issue_count": "",
},
),
pytest.param(
" X-Men-V1-067.cbr",
"hyphen separated with hyphen in series",
{
"issue": "67",
"series": "X-Men",
"title": "",
"volume": "1",
"year": "",
"remainder": "",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
(
"Amazing Spider-Man 078.BEY (2022) (Digital) (Zone-Empire).cbr",
"number issue with extra",
{
"issue": "78.BEY",
"series": "Amazing Spider-Man",
"volume": "",
"year": "2022",
"remainder": "(Digital) (Zone-Empire)",
"issue_count": "",
},
),
pytest.param(
"Angel Wings 02 - Black Widow (2015) (Scanlation) (phillywilly).cbr",
"title after-issue",
{
"issue": "2",
"series": "Angel Wings",
"title": "Black Widow",
"volume": "",
"year": "2015",
"remainder": "(Scanlation) (phillywilly)",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
pytest.param(
"Angel Wings #02 - Black Widow (2015) (Scanlation) (phillywilly).cbr",
"title after-#issue",
{
"issue": "2",
"series": "Angel Wings",
"title": "Black Widow",
"volume": "",
"year": "2015",
"remainder": "(Scanlation) (phillywilly)",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
pytest.param(
"Aquaman - Green Arrow - Deep Target 01 (of 07) (2021) (digital) (Son of Ultron-Empire).cbr",
"issue count",
{
"issue": "1",
"series": "Aquaman - Green Arrow - Deep Target",
"volume": "",
"year": "2021",
"issue_count": "7",
"remainder": "(digital) (Son of Ultron-Empire)",
},
marks=pytest.mark.xfail,
),
(
"Aquaman 80th Anniversary 100-Page Super Spectacular (2021) 001 (2021) (Digital) (BlackManta-Empire).cbz",
"numbers in series",
{
"issue": "1",
"series": "Aquaman 80th Anniversary 100-Page Super Spectacular",
"volume": "2021",
"year": "2021",
"remainder": "(Digital) (BlackManta-Empire)",
"issue_count": "",
},
),
pytest.param(
"Avatar - The Last Airbender - The Legend of Korra (FCBD 2021) (Digital) (mv-DCP).cbr",
"FCBD date",
{
"issue": "",
"series": "Avatar - The Last Airbender - The Legend of Korra",
"volume": "",
"year": "2021",
"remainder": "(FCBD) (Digital) (mv-DCP)",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
pytest.param(
"Avengers By Brian Michael Bendis v03 (2013) (Digital) (F2) (Kileko-Empire).cbz",
"volume without issue",
{
"issue": "",
"series": "Avengers By Brian Michael Bendis",
"volume": "3",
"year": "2013",
"remainder": "(Digital) (F2) (Kileko-Empire)",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
(
"Batman '89 (2021) (Webrip) (The Last Kryptonian-DCP).cbr",
"year in title without issue",
{
"issue": "",
"series": "Batman '89",
"volume": "",
"year": "2021",
"remainder": "(Webrip) (The Last Kryptonian-DCP)",
"issue_count": "",
},
),
(
"Batman_-_Superman_020_(2021)_(digital)_(NeverAngel-Empire).cbr",
"underscores",
{
"issue": "20",
"series": "Batman - Superman",
"volume": "",
"year": "2021",
"remainder": "(digital) (NeverAngel-Empire)",
"issue_count": "",
},
),
(
"Black Widow 009 (2021) (Digital) (Zone-Empire).cbr",
"standard",
{
"issue": "9",
"series": "Black Widow",
"volume": "",
"year": "2021",
"remainder": "(Digital) (Zone-Empire)",
"issue_count": "",
},
),
(
"Blade Runner 2029 006 (2021) (3 covers) (digital) (Son of Ultron-Empire).cbr",
"year before issue",
{
"issue": "6",
"series": "Blade Runner 2029",
"volume": "",
"year": "2021",
"remainder": "(3 covers) (digital) (Son of Ultron-Empire)",
"issue_count": "",
},
),
pytest.param(
"Blade Runner Free Comic Book Day 2021 (2021) (digital-Empire).cbr",
"FCBD year and (year)",
{
"issue": "",
"series": "Blade Runner Free Comic Book Day 2021",
"volume": "",
"year": "2021",
"remainder": "(digital-Empire)",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
pytest.param(
"Bloodshot Book 03 (2020) (digital) (Son of Ultron-Empire).cbr",
"book",
{
"issue": "3",
"series": "Bloodshot",
"title": "Book 03",
"volume": "3",
"year": "2020",
"remainder": "(digital) (Son of Ultron-Empire)",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
pytest.param(
"Cyberpunk 2077 - You Have My Word 02 (2021) (digital) (Son of Ultron-Empire).cbr",
"title",
{
"issue": "2",
"series": "Cyberpunk 2077",
"title": "You Have My Word",
"volume": "",
"year": "2021",
"issue_count": "",
"remainder": "(digital) (Son of Ultron-Empire)",
},
marks=pytest.mark.xfail,
),
pytest.param(
"Elephantmen 2259 008 - Simple Truth 03 (of 06) (2021) (digital) (Son of Ultron-Empire).cbr",
"volume count",
{
"issue": "8",
"series": "Elephantmen 2259",
"title": "Simple Truth",
"volume": "3",
"year": "2021",
"volume_count": "6",
"remainder": "(digital) (Son of Ultron-Empire)",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
pytest.param(
"Elephantmen 2259 #008 - Simple Truth 03 (of 06) (2021) (digital) (Son of Ultron-Empire).cbr",
"volume count",
{
"issue": "8",
"series": "Elephantmen 2259",
"title": "Simple Truth",
"volume": "3",
"year": "2021",
"volume_count": "6",
"remainder": "(digital) (Son of Ultron-Empire)",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
pytest.param(
"Free Comic Book Day - Avengers.Hulk (2021) (2048px) (db).cbz",
"'.' in name",
{
"issue": "",
"series": "Free Comic Book Day - Avengers Hulk",
"volume": "",
"year": "2021",
"remainder": "(2048px) (db)",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
(
"Goblin (2021) (digital) (Son of Ultron-Empire).cbr",
"no-issue",
{
"issue": "",
"series": "Goblin",
"volume": "",
"year": "2021",
"remainder": "(digital) (Son of Ultron-Empire)",
"issue_count": "",
},
),
pytest.param(
"Marvel Previews 002 (January 2022) (Digital-Empire).cbr",
"(month year)",
{
"issue": "2",
"series": "Marvel Previews",
"volume": "",
"year": "2022",
"remainder": "(Digital-Empire)",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
pytest.param(
"Marvel Two In One V1 090 c2c (Comixbear-DCP).cbr",
"volume issue ctc",
{
"issue": "90",
"series": "Marvel Two In One",
"volume": "1",
"year": "",
"remainder": "c2c (Comixbear-DCP)",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
(
"Marvel Two In One V1 #090 c2c (Comixbear-DCP).cbr",
"volume then issue",
{
"issue": "90",
"series": "Marvel Two In One",
"volume": "1",
"year": "",
"remainder": "c2c (Comixbear-DCP)",
"issue_count": "",
},
),
pytest.param(
"Star Wars - War of the Bounty Hunters - IG-88 (2021) (Digital) (Kileko-Empire).cbz",
"number ends series, no-issue",
{
"issue": "",
"series": "Star Wars - War of the Bounty Hunters - IG-88",
"volume": "",
"year": "2021",
"remainder": "(Digital) (Kileko-Empire)",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
(
"Star Wars - War of the Bounty Hunters - IG-88 #1 (2021) (Digital) (Kileko-Empire).cbz",
"number ends series",
{
"issue": "1",
"series": "Star Wars - War of the Bounty Hunters - IG-88",
"volume": "",
"year": "2021",
"remainder": "(Digital) (Kileko-Empire)",
"issue_count": "",
},
),
(
"The Defenders v1 058 (1978) (digital).cbz",
"",
{
"issue": "58",
"series": "The Defenders",
"volume": "1",
"year": "1978",
"remainder": "(digital)",
"issue_count": "",
},
),
pytest.param(
"The Defenders v1 Annual 01 (1976) (Digital) (Minutemen-Slayer).cbr",
" v in series",
{
"issue": "1",
"series": "The Defenders Annual",
"volume": "1",
"year": "1976",
"remainder": "(Digital) (Minutemen-Slayer)",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
pytest.param(
"The Magic Order 2 06 (2022) (Digital) (Zone-Empire)[__913302__].cbz",
"ending id",
{
"issue": "6",
"series": "The Magic Order 2",
"volume": "",
"year": "2022",
"remainder": "(Digital) (Zone-Empire)[__913302__]",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
pytest.param(
"Wonder Woman 001 Wonder Woman Day Special Edition (2021) (digital-Empire).cbr",
"issue separates title",
{
"issue": "1",
"series": "Wonder Woman",
"title": "Wonder Woman Day Special Edition",
"volume": "",
"year": "2021",
"remainder": "(digital-Empire)",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
pytest.param(
"Wonder Woman #001 Wonder Woman Day Special Edition (2021) (digital-Empire).cbr",
"issue separates title",
{
"issue": "1",
"series": "Wonder Woman",
"title": "Wonder Woman Day Special Edition",
"volume": "",
"year": "2021",
"remainder": "(digital-Empire)",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
pytest.param(
"Wonder Woman 49 DC Sep-Oct 1951 digital [downsized, lightened, 4 missing story pages restored] (Shadowcat-Empire).cbz",
"date-range, no paren, braces",
{
"issue": "49",
"series": "Wonder Woman",
"volume": "",
"year": "1951",
"remainder": "(Shadowcat-Empire)",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
pytest.param(
"Wonder Woman #49 DC Sep-Oct 1951 digital [downsized, lightened, 4 missing story pages restored] (Shadowcat-Empire).cbz",
"date-range, no paren, braces",
{
"issue": "49",
"series": "Wonder Woman",
"volume": "",
"year": "1951",
"remainder": "(Shadowcat-Empire)",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
pytest.param(
"X-Men, 2021-08-04 (#02) (digital) (Glorith-HD).cbz",
"full-date, issue in parenthesis",
{
"issue": "2",
"series": "X-Men",
"volume": "",
"year": "2021",
"remainder": "(digital) (Glorith-HD)",
"issue_count": "",
},
marks=pytest.mark.xfail,
),
]
rnames = [
(
"{series} #{issue} - {title} ({year})",
False,
"universal",
"Cory Doctorow's Futuristic Tales of the Here and Now #001 - Anda's Game (2007).cbz",
),
(
"{series}: {title} #{issue} ({year})",
False,
"universal",
"Cory Doctorow's Futuristic Tales of the Here and Now - Anda's Game #001 (2007).cbz",
),
pytest.param(
"{series}: {title} #{issue} ({year})",
False,
"Linux",
"Cory Doctorow's Futuristic Tales of the Here and Now: Anda's Game #001 (2007).cbz",
marks=pytest.mark.xfail,
),
pytest.param(
"{publisher}/ {series} #{issue} - {title} ({year})",
True,
"universal",
"IDW Publishing/Cory Doctorow's Futuristic Tales of the Here and Now #001 - Anda's Game (2007).cbz",
marks=pytest.mark.xfail,
),
pytest.param(
"{publisher}/ {series} #{issue} - {title} ({year})",
False,
"universal",
"Cory Doctorow's Futuristic Tales of the Here and Now #001 - Anda's Game (2007).cbz",
marks=pytest.mark.xfail,
),
(
"{series} # {issue} - {title} ({year})",
False,
"universal",
"Cory Doctorow's Futuristic Tales of the Here and Now # 001 - Anda's Game (2007).cbz",
),
pytest.param(
"{series} # {issue} - {locations} ({year})",
False,
"universal",
"Cory Doctorow's Futuristic Tales of the Here and Now # 001 - lonely cottage (2007).cbz",
marks=pytest.mark.xfail,
),
pytest.param(
"{series} #{issue} - {title} - {WriteR}, {EDITOR} ({year})",
False,
"universal",
"Cory Doctorow's Futuristic Tales of the Here and Now #001 - Anda's Game - Dara Naraghi, Ted Adams (2007).cbz",
marks=pytest.mark.xfail,
),
]

View File

@ -0,0 +1,34 @@
import pytest
from filenames import fnames
import comicapi.filenameparser
# def test_filename_parser():
# p = comicapi.filenameparser.FileNameParser()
# p.parse_filename("Cory Doctorows Futuristic Tales of the Here and Now #1 andas game.rar")
# fp = p.__dict__
# assert fp["issue"] == "1"
# assert fp["series"] == "Cory Doctorows Futuristic Tales of the Here and Now"
# assert fp["remainder"] == "andas game"
# assert fp["volume"] == ""
# assert fp["year"] == ""
# assert fp["issue_count"] == ""
@pytest.mark.parametrize("filename,reason,expected", fnames)
def test_file_name_parser(filename, reason, expected):
p = comicapi.filenameparser.FileNameParser()
p.parse_filename(filename)
fp = p.__dict__
# del expected["remainder"]
# del expected["title"]
# del fp["archive"]
for s in ["title"]:
if s in expected:
# expected[s] = ""
del expected[s]
# if s not in fp:
# fp[s] = ""
assert fp == expected

View File

@ -0,0 +1,59 @@
import shutil
from os.path import abspath, dirname, join
import pytest
from comicapi.comicarchive import ComicArchive, rar_support
from comicapi.genericmetadata import GenericMetadata, PageType, md_test
thisdir = dirname(abspath(__file__))
@pytest.mark.xfail(not rar_support, reason="rar support")
def test_getPageNameList():
ComicArchive.logo_data = b""
c = ComicArchive(join(thisdir, "data", "fake_cbr.cbr"))
pageNameList = c.get_page_name_list()
assert pageNameList == [
"page0.jpg",
"Page1.jpeg",
"Page2.png",
"Page3.gif",
"page4.webp",
"page10.jpg",
]
def test_set_default_page_list(tmpdir):
md = GenericMetadata()
md.overlay(md_test)
md.pages = []
print(md_test.pages, md.pages)
md.set_default_page_list(len(md_test.pages))
assert isinstance(md.pages[0]["Image"], int)
def test_page_type():
c_path = join(thisdir, "data", "Cory Doctorow's Futuristic Tales of the Here and Now #001 - Anda's Game (2007).cbz")
c = ComicArchive(str(c_path))
md = c.read_cix()
assert isinstance(md.pages[0]["Type"], PageType)
def test_save_cix(tmpdir):
comic_path = tmpdir.mkdir("cbz").join(
"Cory Doctorow's Futuristic Tales of the Here and Now #001 - Anda's Game (2007).cbz"
)
c_path = join(thisdir, "data", "Cory Doctorow's Futuristic Tales of the Here and Now #001 - Anda's Game (2007).cbz")
shutil.copy(c_path, comic_path)
c = ComicArchive(str(comic_path))
md = c.read_cix()
md.set_default_page_list(c.get_number_of_pages())
assert c.write_cix(md)
md = c.read_cix()

24
tests/test_rename.py Normal file
View File

@ -0,0 +1,24 @@
import re
import pytest
from filenames import rnames
from comicapi.genericmetadata import md_test
from comictaggerlib.filerenamer import FileRenamer, FileRenamer2
@pytest.mark.parametrize("template, move, platform, expected", rnames)
def test_rename_old(template, platform, move, expected):
_ = platform
_ = move
fr = FileRenamer(md_test)
fr.set_template(re.sub(r"{(\w+)}", r"%\1%", template))
assert fr.determine_name(".cbz") == expected
@pytest.mark.parametrize("template, move, platform, expected", rnames)
def test_rename_new(template, platform, move, expected):
fr = FileRenamer2(md_test, platform=platform)
fr.move = move
fr.set_template(template)
assert fr.determine_name(".cbz") == expected