Compare commits
48 Commits
1.3.2-alph
...
1.4.2
Author | SHA1 | Date | |
---|---|---|---|
bfe005cb63 | |||
48c2e91f7e | |||
02f365b93f | |||
d78c3e3039 | |||
f18513fd0e | |||
caa94c4e28 | |||
7037877a77 | |||
6cccf22d54 | |||
ceb2b2861e | |||
298f50cb45 | |||
e616aa8373 | |||
0fe881df59 | |||
f3f48ea958 | |||
9a9d36dc65 | |||
028b728d82 | |||
23f323f52d | |||
49210e67c5 | |||
e519bf79be | |||
4f08610a28 | |||
544bdcb4e3 | |||
f3095144f5 | |||
75f31c7cb2 | |||
f7f4e41c95 | |||
6da177471b | |||
8a74e5b02b | |||
5658f261b0 | |||
6da3bf764e | |||
5e06d35057 | |||
82bcc876b3 | |||
d7a6882577 | |||
5e7e1b1513 | |||
cd9a02c255 | |||
b47f816dd5 | |||
d1a649c0ba | |||
b7759506fe | |||
97777d61d2 | |||
e622b56dae | |||
a24251e5b4 | |||
d4470a2015 | |||
d37022b71f | |||
5f38241bcb | |||
c9b5bd625f | |||
beb7c57a6b | |||
ce48730bd5 | |||
806b65db24 | |||
cdf9a40227 | |||
0adac47968 | |||
096a89eab4 |
43
Makefile
43
Makefile
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)"
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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=" font-style:italic;">%series% %issue% (%year%)</span><br/><span style=" font-style:italic;">%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({'role': string, 'person': string, 'primary': boolean}))
|
||||
{tags} (list of str)
|
||||
{pages} (list of dict({'Image': string(int), 'Type': 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)
|
||||
|
@ -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()
|
||||
|
||||
|
134
comictaggerlib/ui/TemplateHelp.ui
Normal file
134
comictaggerlib/ui/TemplateHelp.ui
Normal 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><html>
|
||||
<head>
|
||||
<style>
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 style="text-align: center">Template help</h1>
|
||||
<p>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
|
||||
|
||||
<a href="https://docs.python.org/3/library/string.html#format-string-syntax">Python 3 documentation</a></p>
|
||||
Accepts the following variables:
|
||||
<table>
|
||||
<tr>
|
||||
<th>Tag name</th>
|
||||
<th>Type</th>
|
||||
</tr>
|
||||
<tr><td>{is_empty}</td><td>boolean</td></tr>
|
||||
<tr><td>{tag_origin}</td><td>string</td></tr>
|
||||
<tr><td>{series}</td><td>string</td></tr>
|
||||
<tr><td>{issue}</td><td>string</td></tr>
|
||||
<tr><td>{title}</td><td>string</td></tr>
|
||||
<tr><td>{publisher}</td><td>string</td></tr>
|
||||
<tr><td>{month}</td><td>integer</td></tr>
|
||||
<tr><td>{year}</td><td>integer</td></tr>
|
||||
<tr><td>{day}</td><td>integer</td></tr>
|
||||
<tr><td>{issue_count}</td><td>integer</td></tr>
|
||||
<tr><td>{volume}</td><td>integer</td></tr>
|
||||
<tr><td>{genre}</td><td>string</td></tr>
|
||||
<tr><td>{language}</td><td>string</td></tr>
|
||||
<tr><td>{comments}</td><td>string</td></tr>
|
||||
<tr><td>{volume_count}</td><td>integer</td></tr>
|
||||
<tr><td>{critical_rating}</td><td>string</td></tr>
|
||||
<tr><td>{country}</td><td>string</td></tr>
|
||||
<tr><td>{alternate_series}</td><td>string</td></tr>
|
||||
<tr><td>{alternate_number}</td><td>string</td></tr>
|
||||
<tr><td>{alternate_count}</td><td>integer</td></tr>
|
||||
<tr><td>{imprint}</td><td>string</td></tr>
|
||||
<tr><td>{notes}</td><td>string</td></tr>
|
||||
<tr><td>{web_link}</td><td>string</td></tr>
|
||||
<tr><td>{format}</td><td>string</td></tr>
|
||||
<tr><td>{manga}</td><td>string</td></tr>
|
||||
<tr><td>{black_and_white}</td><td>boolean</td></tr>
|
||||
<tr><td>{page_count}</td><td>integer</td></tr>
|
||||
<tr><td>{maturity_rating}</td><td>string</td></tr>
|
||||
<tr><td>{story_arc}</td><td>string</td></tr>
|
||||
<tr><td>{series_group}</td><td>string</td></tr>
|
||||
<tr><td>{scan_info}</td><td>string</td></tr>
|
||||
<tr><td>{characters}</td><td>string</td></tr>
|
||||
<tr><td>{teams}</td><td>string</td></tr>
|
||||
<tr><td>{locations}</td><td>string</td></tr>
|
||||
<tr><td>{credits}</td><td>list of dict({'role': string, 'person': string, 'primary': boolean})</td></tr>
|
||||
<tr><td>{tags}</td><td>list of str</td></tr>
|
||||
<tr><td>{pages}</td><td>list of dict({'Image': string(int), 'Type': string})</td></tr>
|
||||
<tr><td>{price}</td><td>float</td></tr>
|
||||
<tr><td>{is_version_of}</td><td>string</td></tr>
|
||||
<tr><td>{rights}</td><td>string</td></tr>
|
||||
<tr><td>{identifier}</td><td>string</td></tr>
|
||||
<tr><td>{last_mark}</td><td>string</td></tr>
|
||||
<tr><td>{cover_image}</td><td>string</td></tr>
|
||||
</table>
|
||||
<pre>
|
||||
Examples:
|
||||
|
||||
{series} {issue} ({year})
|
||||
Spider-Geddon 1 (2018)
|
||||
|
||||
{series} #{issue} - {title}
|
||||
Spider-Geddon #1 - New Players; Check In
|
||||
|
||||
</pre>
|
||||
</body>
|
||||
</html></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>
|
@ -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>&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&mark:</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>leBookmark</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -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)
|
||||
|
@ -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><html><head/><body><p><span style=" font-weight:600;">&quot;Smart Text Cleanup&quot; </span>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.</p></body></html></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>
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -2,5 +2,6 @@ beautifulsoup4 >= 4.1
|
||||
natsort>=8.1.0
|
||||
pillow>=4.3.0
|
||||
requests==2.*
|
||||
pathvalidate
|
||||
pycountry
|
||||
py7zr
|
||||
|
@ -4,4 +4,6 @@ setuptools_scm[toml]>=3.4
|
||||
wheel
|
||||
black>=22
|
||||
flake8==4.*
|
||||
flake8-encodings
|
||||
isort>=5.10
|
||||
pytest==7.*
|
2
setup.py
2
setup.py
@ -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()
|
||||
|
||||
|
||||
|
Binary file not shown.
BIN
tests/data/fake_cbr.cbr
Normal file
BIN
tests/data/fake_cbr.cbr
Normal file
Binary file not shown.
595
tests/filenames.py
Normal file
595
tests/filenames.py
Normal 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,
|
||||
),
|
||||
]
|
34
tests/test_FilenameParser.py
Normal file
34
tests/test_FilenameParser.py
Normal 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
|
59
tests/test_comicarchive.py
Normal file
59
tests/test_comicarchive.py
Normal 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
24
tests/test_rename.py
Normal 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
|
Reference in New Issue
Block a user