Apply pre-commit configuration
This commit is contained in:
parent
c19ed49e05
commit
fd4c453854
2
.flake8
2
.flake8
@ -1,4 +1,4 @@
|
||||
[flake8]
|
||||
max-line-length = 120
|
||||
extend-ignore = E203, E501, E722
|
||||
extend-ignore = E203, E501, A003
|
||||
extend-exclude = venv, scripts, build, dist
|
||||
|
43
.pre-commit-config.yaml
Normal file
43
.pre-commit-config.yaml
Normal file
@ -0,0 +1,43 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.2.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: debug-statements
|
||||
- id: name-tests-test
|
||||
- id: requirements-txt-fixer
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v1.20.1
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.10.1
|
||||
hooks:
|
||||
- id: isort
|
||||
args: [--af,--add-import, 'from __future__ import annotations']
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.32.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py39-plus]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v1.4
|
||||
hooks:
|
||||
- id: autoflake
|
||||
args: [-i]
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 4.0.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [flake8-encodings, flake8-warnings, flake8-builtins, flake8-eradicate, flake8-length]
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.960
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies: [types-setuptools, types-requests]
|
@ -134,4 +134,4 @@ tests/test_comicarchive.py x... [ 73%]
|
||||
tests/test_rename.py ..xxx.xx..XXX.XX [100%]
|
||||
|
||||
================== 27 passed, 29 xfailed, 5 xpassed in 2.68s ===================
|
||||
```
|
||||
```
|
||||
|
@ -1 +1,3 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__author__ = "dromanin"
|
||||
|
@ -1,18 +1,19 @@
|
||||
"""A class to encapsulate CoMet data"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
@ -208,7 +209,7 @@ class CoMet:
|
||||
root = tree.getroot()
|
||||
if root.tag != "comet":
|
||||
raise Exception
|
||||
except:
|
||||
except ET.ParseError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
@ -1,18 +1,18 @@
|
||||
"""A class to represent a single comic, be it file or folder of images"""
|
||||
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import logging
|
||||
@ -21,20 +21,26 @@ import pathlib
|
||||
import platform
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import zipfile
|
||||
from typing import cast
|
||||
|
||||
import natsort
|
||||
import py7zr
|
||||
import wordninja
|
||||
|
||||
from comicapi import filenamelexer, filenameparser, utils
|
||||
from comicapi.comet import CoMet
|
||||
from comicapi.comicbookinfo import ComicBookInfo
|
||||
from comicapi.comicinfoxml import ComicInfoXml
|
||||
from comicapi.genericmetadata import GenericMetadata, PageType
|
||||
|
||||
try:
|
||||
from unrar.cffi import rarfile
|
||||
|
||||
rar_support = True
|
||||
except:
|
||||
except ImportError:
|
||||
rar_support = False
|
||||
|
||||
try:
|
||||
@ -44,20 +50,11 @@ try:
|
||||
except ImportError:
|
||||
pil_available = False
|
||||
|
||||
from typing import List, Optional, Union, cast
|
||||
|
||||
from comicapi import filenamelexer, filenameparser, utils
|
||||
from comicapi.comet import CoMet
|
||||
from comicapi.comicbookinfo import ComicBookInfo
|
||||
from comicapi.comicinfoxml import ComicInfoXml
|
||||
from comicapi.genericmetadata import GenericMetadata, PageType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
if not pil_available:
|
||||
logger.exception("PIL unavalable")
|
||||
|
||||
sys.path.insert(0, os.path.abspath("."))
|
||||
|
||||
|
||||
class MetaDataStyle:
|
||||
CBI = 0
|
||||
@ -71,7 +68,7 @@ class UnknownArchiver:
|
||||
|
||||
"""Unknown implementation"""
|
||||
|
||||
def __init__(self, path: Union[pathlib.Path, str]) -> None:
|
||||
def __init__(self, path: pathlib.Path | str) -> None:
|
||||
self.path = path
|
||||
|
||||
def get_comment(self) -> str:
|
||||
@ -80,7 +77,7 @@ class UnknownArchiver:
|
||||
def set_comment(self, comment: str) -> bool:
|
||||
return False
|
||||
|
||||
def read_file(self, archive_file: str) -> Optional[bytes]:
|
||||
def read_file(self, archive_file: str) -> bytes | None:
|
||||
return None
|
||||
|
||||
def write_file(self, archive_file: str, data: bytes) -> bool:
|
||||
@ -97,7 +94,7 @@ class SevenZipArchiver(UnknownArchiver):
|
||||
|
||||
"""7Z implementation"""
|
||||
|
||||
def __init__(self, path: Union[pathlib.Path, str]) -> None:
|
||||
def __init__(self, path: pathlib.Path | str) -> None:
|
||||
self.path = pathlib.Path(path)
|
||||
|
||||
# @todo: Implement Comment?
|
||||
@ -114,17 +111,17 @@ class SevenZipArchiver(UnknownArchiver):
|
||||
data = zf.read(archive_file)[archive_file].read()
|
||||
except py7zr.Bad7zFile as e:
|
||||
logger.error("bad 7zip file [%s]: %s :: %s", e, self.path, archive_file)
|
||||
raise IOError from e
|
||||
except Exception as e:
|
||||
raise OSError from e
|
||||
except OSError as e:
|
||||
logger.error("bad 7zip file [%s]: %s :: %s", e, self.path, archive_file)
|
||||
raise IOError from e
|
||||
raise OSError from e
|
||||
|
||||
return data
|
||||
|
||||
def remove_file(self, archive_file: str) -> bool:
|
||||
try:
|
||||
self.rebuild_zip_file([archive_file])
|
||||
except:
|
||||
except (py7zr.Bad7zFile, OSError):
|
||||
logger.exception("Failed to remove %s from 7zip archive", archive_file)
|
||||
return False
|
||||
else:
|
||||
@ -143,7 +140,7 @@ class SevenZipArchiver(UnknownArchiver):
|
||||
with py7zr.SevenZipFile(self.path, "a") as zf:
|
||||
zf.writestr(data, archive_file)
|
||||
return True
|
||||
except:
|
||||
except (py7zr.Bad7zFile, OSError):
|
||||
logger.exception("Writing zip file failed")
|
||||
return False
|
||||
|
||||
@ -153,7 +150,7 @@ class SevenZipArchiver(UnknownArchiver):
|
||||
namelist: list[str] = zf.getnames()
|
||||
|
||||
return namelist
|
||||
except Exception as e:
|
||||
except (py7zr.Bad7zFile, OSError) as e:
|
||||
logger.error("Unable to get 7zip file list [%s]: %s", e, self.path)
|
||||
return []
|
||||
|
||||
@ -172,7 +169,7 @@ class SevenZipArchiver(UnknownArchiver):
|
||||
with py7zr.SevenZipFile(tmp_name, "w") as zout:
|
||||
for fname, bio in zin.read(targets).items():
|
||||
zout.writef(bio, fname)
|
||||
except Exception:
|
||||
except (py7zr.Bad7zFile, OSError):
|
||||
logger.exception("Error rebuilding 7zip file: %s", self.path)
|
||||
|
||||
# replace with the new file
|
||||
@ -198,7 +195,7 @@ class ZipArchiver(UnknownArchiver):
|
||||
|
||||
"""ZIP implementation"""
|
||||
|
||||
def __init__(self, path: Union[pathlib.Path, str]) -> None:
|
||||
def __init__(self, path: pathlib.Path | str) -> None:
|
||||
self.path = pathlib.Path(path)
|
||||
|
||||
def get_comment(self) -> str:
|
||||
@ -217,16 +214,16 @@ class ZipArchiver(UnknownArchiver):
|
||||
data = zf.read(archive_file)
|
||||
except zipfile.BadZipfile as e:
|
||||
logger.error("bad zipfile [%s]: %s :: %s", e, self.path, archive_file)
|
||||
raise IOError from e
|
||||
except Exception as e:
|
||||
raise OSError from e
|
||||
except OSError as e:
|
||||
logger.error("bad zipfile [%s]: %s :: %s", e, self.path, archive_file)
|
||||
raise IOError from e
|
||||
raise OSError from e
|
||||
return data
|
||||
|
||||
def remove_file(self, archive_file: str) -> bool:
|
||||
try:
|
||||
self.rebuild_zip_file([archive_file])
|
||||
except:
|
||||
except (zipfile.BadZipfile, OSError):
|
||||
logger.exception("Failed to remove %s from zip archive", archive_file)
|
||||
return False
|
||||
else:
|
||||
@ -245,20 +242,20 @@ class ZipArchiver(UnknownArchiver):
|
||||
with zipfile.ZipFile(self.path, mode="a", allowZip64=True, compression=zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.writestr(archive_file, data)
|
||||
return True
|
||||
except Exception as e:
|
||||
except (zipfile.BadZipfile, OSError) as e:
|
||||
logger.error("writing zip file failed [%s]: %s", e, self.path)
|
||||
return False
|
||||
|
||||
def get_filename_list(self) -> List[str]:
|
||||
def get_filename_list(self) -> list[str]:
|
||||
try:
|
||||
with zipfile.ZipFile(self.path, "r") as zf:
|
||||
namelist = zf.namelist()
|
||||
return namelist
|
||||
except Exception as e:
|
||||
except (zipfile.BadZipfile, OSError) as e:
|
||||
logger.error("Unable to get zipfile list [%s]: %s", e, self.path)
|
||||
return []
|
||||
|
||||
def rebuild_zip_file(self, exclude_list: List[str]) -> None:
|
||||
def rebuild_zip_file(self, exclude_list: list[str]) -> None:
|
||||
"""Zip helper func
|
||||
|
||||
This recompresses the zip archive, without the files in the exclude_list
|
||||
@ -276,14 +273,14 @@ class ZipArchiver(UnknownArchiver):
|
||||
|
||||
# preserve the old comment
|
||||
zout.comment = zin.comment
|
||||
except Exception:
|
||||
except (zipfile.BadZipfile, OSError):
|
||||
logger.exception("Error rebuilding zip file: %s", self.path)
|
||||
|
||||
# replace with the new file
|
||||
os.remove(self.path)
|
||||
os.rename(tmp_name, self.path)
|
||||
|
||||
def write_zip_comment(self, filename: Union[pathlib.Path, str], comment: str) -> bool:
|
||||
def write_zip_comment(self, filename: pathlib.Path | str, comment: str) -> bool:
|
||||
"""
|
||||
This is a custom function for writing a comment to a zip file,
|
||||
since the built-in one doesn't seem to work on Windows and Mac OS/X
|
||||
@ -370,7 +367,7 @@ class RarArchiver(UnknownArchiver):
|
||||
|
||||
devnull = None
|
||||
|
||||
def __init__(self, path: Union[pathlib.Path, str], rar_exe_path: str) -> None:
|
||||
def __init__(self, path: pathlib.Path | str, rar_exe_path: str) -> None:
|
||||
self.path = pathlib.Path(path)
|
||||
self.rar_exe_path = rar_exe_path
|
||||
|
||||
@ -411,7 +408,7 @@ class RarArchiver(UnknownArchiver):
|
||||
if platform.system() == "Darwin":
|
||||
time.sleep(1)
|
||||
os.remove(tmp_name)
|
||||
except Exception:
|
||||
except (subprocess.CalledProcessError, OSError):
|
||||
logger.exception("Failed to set a comment")
|
||||
return False
|
||||
else:
|
||||
@ -442,7 +439,7 @@ class RarArchiver(UnknownArchiver):
|
||||
tries,
|
||||
)
|
||||
continue
|
||||
except (OSError, IOError) as e:
|
||||
except OSError as e:
|
||||
logger.error("read_file(): [%s] %s:%s attempt #%d", e, self.path, archive_file, tries)
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
@ -458,9 +455,9 @@ class RarArchiver(UnknownArchiver):
|
||||
if len(entries) == 1:
|
||||
return entries[0][1]
|
||||
|
||||
raise IOError
|
||||
raise OSError
|
||||
|
||||
raise IOError
|
||||
raise OSError
|
||||
|
||||
def write_file(self, archive_file: str, data: bytes) -> bool:
|
||||
|
||||
@ -490,7 +487,7 @@ class RarArchiver(UnknownArchiver):
|
||||
time.sleep(1)
|
||||
os.remove(tmp_file)
|
||||
os.rmdir(tmp_folder)
|
||||
except Exception as e:
|
||||
except (subprocess.CalledProcessError, OSError) as e:
|
||||
logger.info(str(e))
|
||||
logger.exception("Failed write %s to rar archive", archive_file)
|
||||
return False
|
||||
@ -513,7 +510,7 @@ class RarArchiver(UnknownArchiver):
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
time.sleep(1)
|
||||
except:
|
||||
except (subprocess.CalledProcessError, OSError):
|
||||
logger.exception("Failed to remove %s from rar archive", archive_file)
|
||||
return False
|
||||
else:
|
||||
@ -532,16 +529,16 @@ class RarArchiver(UnknownArchiver):
|
||||
if item.file_size != 0:
|
||||
namelist.append(item.filename)
|
||||
|
||||
except (OSError, IOError) as e:
|
||||
except OSError as e:
|
||||
logger.error(f"get_filename_list(): [{e}] {self.path} attempt #{tries}".format(str(e), self.path, tries))
|
||||
time.sleep(1)
|
||||
|
||||
return namelist
|
||||
|
||||
def get_rar_obj(self) -> "Optional[rarfile.RarFile]":
|
||||
def get_rar_obj(self) -> rarfile.RarFile | None:
|
||||
try:
|
||||
rarc = rarfile.RarFile(str(self.path))
|
||||
except (OSError, IOError) as e:
|
||||
except OSError as e:
|
||||
logger.error("getRARObj(): [%s] %s", e, self.path)
|
||||
else:
|
||||
return rarc
|
||||
@ -553,7 +550,7 @@ class FolderArchiver(UnknownArchiver):
|
||||
|
||||
"""Folder implementation"""
|
||||
|
||||
def __init__(self, path: Union[pathlib.Path, str]) -> None:
|
||||
def __init__(self, path: pathlib.Path | str) -> None:
|
||||
self.path = pathlib.Path(path)
|
||||
self.comment_file_name = "ComicTaggerFolderComment.txt"
|
||||
|
||||
@ -570,7 +567,7 @@ class FolderArchiver(UnknownArchiver):
|
||||
try:
|
||||
with open(fname, "rb") as f:
|
||||
data = f.read()
|
||||
except IOError:
|
||||
except OSError:
|
||||
logger.exception("Failed to read: %s", fname)
|
||||
|
||||
return data
|
||||
@ -581,7 +578,7 @@ class FolderArchiver(UnknownArchiver):
|
||||
try:
|
||||
with open(fname, "wb") as f:
|
||||
f.write(data)
|
||||
except:
|
||||
except OSError:
|
||||
logger.exception("Failed to write: %s", fname)
|
||||
return False
|
||||
else:
|
||||
@ -592,7 +589,7 @@ class FolderArchiver(UnknownArchiver):
|
||||
fname = os.path.join(self.path, archive_file)
|
||||
try:
|
||||
os.remove(fname)
|
||||
except:
|
||||
except OSError:
|
||||
logger.exception("Failed to remove: %s", fname)
|
||||
return False
|
||||
else:
|
||||
@ -601,7 +598,7 @@ class FolderArchiver(UnknownArchiver):
|
||||
def get_filename_list(self) -> list[str]:
|
||||
return self.list_files(self.path)
|
||||
|
||||
def list_files(self, folder: Union[pathlib.Path, str]) -> list[str]:
|
||||
def list_files(self, folder: pathlib.Path | str) -> list[str]:
|
||||
|
||||
itemlist = []
|
||||
|
||||
@ -621,19 +618,19 @@ class ComicArchive:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path: Union[pathlib.Path, str],
|
||||
path: pathlib.Path | str,
|
||||
rar_exe_path: str = "",
|
||||
default_image_path: Union[pathlib.Path, str, None] = None,
|
||||
default_image_path: pathlib.Path | str | None = None,
|
||||
) -> None:
|
||||
self.cbi_md: Optional[GenericMetadata] = None
|
||||
self.cix_md: Optional[GenericMetadata] = None
|
||||
self.comet_filename: Optional[str] = None
|
||||
self.comet_md: Optional[GenericMetadata] = None
|
||||
self.has__cbi: Optional[bool] = None
|
||||
self.has__cix: Optional[bool] = None
|
||||
self.has__comet: Optional[bool] = None
|
||||
self.cbi_md: GenericMetadata | None = None
|
||||
self.cix_md: GenericMetadata | None = None
|
||||
self.comet_filename: str | None = None
|
||||
self.comet_md: GenericMetadata | None = None
|
||||
self.has__cbi: bool | None = None
|
||||
self.has__cix: bool | None = None
|
||||
self.has__comet: bool | None = None
|
||||
self.path = pathlib.Path(path)
|
||||
self.page_count: Optional[int] = None
|
||||
self.page_count: int | None = None
|
||||
self.page_list: list[str] = []
|
||||
|
||||
self.rar_exe_path = rar_exe_path
|
||||
@ -688,11 +685,11 @@ class ComicArchive:
|
||||
self.cbi_md = None
|
||||
self.comet_md = None
|
||||
|
||||
def load_cache(self, style_list: List[int]) -> None:
|
||||
def load_cache(self, style_list: list[int]) -> None:
|
||||
for style in style_list:
|
||||
self.read_metadata(style)
|
||||
|
||||
def rename(self, path: Union[pathlib.Path, str]) -> None:
|
||||
def rename(self, path: pathlib.Path | str) -> None:
|
||||
self.path = pathlib.Path(path)
|
||||
self.archiver.path = pathlib.Path(path)
|
||||
|
||||
@ -703,10 +700,9 @@ class ComicArchive:
|
||||
return zipfile.is_zipfile(self.path)
|
||||
|
||||
def rar_test(self) -> bool:
|
||||
try:
|
||||
return bool(rarfile.is_rarfile(str(self.path)))
|
||||
except:
|
||||
return False
|
||||
if rar_support:
|
||||
return rarfile.is_rarfile(str(self.path))
|
||||
return False
|
||||
|
||||
def is_sevenzip(self) -> bool:
|
||||
return self.archive_type == self.ArchiveType.SevenZip
|
||||
@ -798,7 +794,7 @@ class ComicArchive:
|
||||
if filename:
|
||||
try:
|
||||
image_data = self.archiver.read_file(filename) or bytes()
|
||||
except IOError:
|
||||
except OSError:
|
||||
logger.exception("Error reading in page. Substituting logo page.")
|
||||
image_data = ComicArchive.logo_data
|
||||
|
||||
@ -816,7 +812,7 @@ class ComicArchive:
|
||||
|
||||
return page_list[index]
|
||||
|
||||
def get_scanner_page_index(self) -> Optional[int]:
|
||||
def get_scanner_page_index(self) -> int | None:
|
||||
scanner_page_index = None
|
||||
|
||||
# make a guess at the scanner page
|
||||
@ -864,7 +860,7 @@ class ComicArchive:
|
||||
|
||||
return scanner_page_index
|
||||
|
||||
def get_page_name_list(self, sort_list: bool = True) -> List[str]:
|
||||
def get_page_name_list(self, sort_list: bool = True) -> list[str]:
|
||||
if not self.page_list:
|
||||
# get the list file names in the archive, and sort
|
||||
files: list[str] = self.archiver.get_filename_list()
|
||||
@ -966,7 +962,7 @@ class ComicArchive:
|
||||
return b""
|
||||
try:
|
||||
raw_cix = self.archiver.read_file(self.ci_xml_filename) or b""
|
||||
except IOError as e:
|
||||
except OSError as e:
|
||||
logger.error("Error reading in raw CIX!: %s", e)
|
||||
raw_cix = bytes()
|
||||
return raw_cix
|
||||
@ -1035,13 +1031,13 @@ class ComicArchive:
|
||||
if not self.has_comet():
|
||||
logger.info("%s doesn't have CoMet data!", self.path)
|
||||
raw_comet = ""
|
||||
|
||||
try:
|
||||
raw_bytes = self.archiver.read_file(cast(str, self.comet_filename))
|
||||
if raw_bytes:
|
||||
raw_comet = raw_bytes.decode("utf-8")
|
||||
except:
|
||||
logger.exception("Error reading in raw CoMet!")
|
||||
else:
|
||||
try:
|
||||
raw_bytes = self.archiver.read_file(cast(str, self.comet_filename))
|
||||
if raw_bytes:
|
||||
raw_comet = raw_bytes.decode("utf-8")
|
||||
except OSError as e:
|
||||
logger.exception("Error reading in raw CoMet!: %s", e)
|
||||
return raw_comet
|
||||
|
||||
def write_comet(self, metadata: GenericMetadata) -> bool:
|
||||
|
@ -1,24 +1,24 @@
|
||||
"""A class to encapsulate the ComicBookInfo data"""
|
||||
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal, TypedDict, Union
|
||||
from typing import Any, Literal, TypedDict
|
||||
|
||||
from comicapi import utils
|
||||
from comicapi.genericmetadata import GenericMetadata
|
||||
@ -119,12 +119,12 @@ class ComicBookInfo:
|
||||
cbi_container = self.create_json_dictionary(metadata)
|
||||
return json.dumps(cbi_container)
|
||||
|
||||
def validate_string(self, string: Union[bytes, str]) -> bool:
|
||||
def validate_string(self, string: bytes | str) -> bool:
|
||||
"""Verify that the string actually contains CBI data in JSON format"""
|
||||
|
||||
try:
|
||||
cbi_container = json.loads(string)
|
||||
except:
|
||||
except json.JSONDecodeError:
|
||||
return False
|
||||
|
||||
return "ComicBookInfo/1.0" in cbi_container
|
||||
|
@ -1,23 +1,23 @@
|
||||
"""A class to encapsulate ComicRack's ComicInfo.xml data"""
|
||||
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections import OrderedDict
|
||||
from typing import Any, List, Optional, cast
|
||||
from typing import Any, cast
|
||||
from xml.etree.ElementTree import ElementTree
|
||||
|
||||
from comicapi import utils
|
||||
@ -37,7 +37,7 @@ class ComicInfoXml:
|
||||
cover_synonyms = ["cover", "covers", "coverartist", "cover artist"]
|
||||
editor_synonyms = ["editor"]
|
||||
|
||||
def get_parseable_credits(self) -> List[str]:
|
||||
def get_parseable_credits(self) -> list[str]:
|
||||
parsable_credits = []
|
||||
parsable_credits.extend(self.writer_synonyms)
|
||||
parsable_credits.extend(self.penciller_synonyms)
|
||||
@ -59,7 +59,7 @@ class ComicInfoXml:
|
||||
return str(tree_str)
|
||||
|
||||
def convert_metadata_to_xml(
|
||||
self, filename: "ComicInfoXml", metadata: GenericMetadata, xml: bytes = b""
|
||||
self, filename: ComicInfoXml, metadata: GenericMetadata, xml: bytes = b""
|
||||
) -> ElementTree:
|
||||
|
||||
# shorthand for the metadata
|
||||
@ -192,7 +192,7 @@ class ComicInfoXml:
|
||||
if root.tag != "ComicInfo":
|
||||
raise Exception("Not a ComicInfo file")
|
||||
|
||||
def get(name: str) -> Optional[str]:
|
||||
def get(name: str) -> str | None:
|
||||
tag = root.find(name)
|
||||
if tag is None:
|
||||
return None
|
||||
|
@ -127,4 +127,4 @@
|
||||
"red circle Comics": "Dark Circle Comics",
|
||||
"red circle": "Dark Circle Comics"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
import os
|
||||
import unicodedata
|
||||
from enum import Enum, auto
|
||||
from typing import Any, Callable, Optional, Set
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
class ItemType(Enum):
|
||||
@ -86,7 +88,7 @@ class Item:
|
||||
class Lexer:
|
||||
def __init__(self, string: str) -> None:
|
||||
self.input: str = string # The string being scanned
|
||||
self.state: Optional[Callable[[Lexer], Optional[Callable]]] = None # The next lexing function to enter
|
||||
self.state: Callable[[Lexer], Callable | None] | None = None # The next lexing function to enter
|
||||
self.pos: int = -1 # Current position in the input
|
||||
self.start: int = 0 # Start position of this item
|
||||
self.lastPos: int = 0 # Position of most recent item returned by nextItem
|
||||
@ -168,13 +170,13 @@ class Lexer:
|
||||
|
||||
# Errorf returns an error token and terminates the scan by passing
|
||||
# Back a nil pointer that will be the next state, terminating self.nextItem.
|
||||
def errorf(lex: Lexer, message: str) -> Optional[Callable[[Lexer], Optional[Callable]]]:
|
||||
def errorf(lex: Lexer, message: str) -> Callable[[Lexer], Callable | None] | None:
|
||||
lex.items.append(Item(ItemType.Error, lex.start, message))
|
||||
return None
|
||||
|
||||
|
||||
# Scans the elements inside action delimiters.
|
||||
def lex_filename(lex: Lexer) -> Optional[Callable[[Lexer], Optional[Callable]]]:
|
||||
def lex_filename(lex: Lexer) -> Callable[[Lexer], Callable | None] | None:
|
||||
r = lex.get()
|
||||
if r == eof:
|
||||
if lex.paren_depth != 0:
|
||||
@ -301,7 +303,7 @@ def lex_text(lex: Lexer) -> Callable:
|
||||
return lex_filename
|
||||
|
||||
|
||||
def cal(value: str) -> Set[Any]:
|
||||
def cal(value: str) -> set[Any]:
|
||||
month_abbr = [i for i, x in enumerate(calendar.month_abbr) if x == value.title()]
|
||||
month_name = [i for i, x in enumerate(calendar.month_name) if x == value.title()]
|
||||
day_abbr = [i for i, x in enumerate(calendar.day_abbr) if x == value.title()]
|
||||
@ -309,7 +311,7 @@ def cal(value: str) -> Set[Any]:
|
||||
return set(month_abbr + month_name + day_abbr + day_name)
|
||||
|
||||
|
||||
def lex_number(lex: Lexer) -> Optional[Callable[[Lexer], Optional[Callable]]]:
|
||||
def lex_number(lex: Lexer) -> Callable[[Lexer], Callable | None] | None:
|
||||
if not lex.scan_number():
|
||||
return errorf(lex, "bad number syntax: " + lex.input[lex.start : lex.pos])
|
||||
# Complex number logic removed. Messes with math operations without space
|
||||
|
@ -2,29 +2,29 @@
|
||||
|
||||
This should probably be re-written, but, well, it mostly works!
|
||||
"""
|
||||
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
#
|
||||
# Some portions of this code were modified from pyComicMetaThis project
|
||||
# http://code.google.com/p/pycomicmetathis/
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from operator import itemgetter
|
||||
from typing import Callable, Match, Optional, TypedDict
|
||||
from typing import Callable, Match, TypedDict
|
||||
from urllib.parse import unquote
|
||||
|
||||
from text2digits import text2digits
|
||||
@ -58,7 +58,7 @@ class FileNameParser:
|
||||
placeholders = [r"[_]", r" +"]
|
||||
for ph in placeholders:
|
||||
string = re.sub(ph, self.repl, string)
|
||||
return string # .strip()
|
||||
return string
|
||||
|
||||
def get_issue_count(self, filename: str, issue_end: int) -> str:
|
||||
|
||||
@ -176,13 +176,11 @@ class FileNameParser:
|
||||
|
||||
# in case there is no issue number, remove some obvious stuff
|
||||
if "--" in filename:
|
||||
# the pattern seems to be that anything to left of the first "--"
|
||||
# is the series name followed by issue
|
||||
# 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)
|
||||
|
||||
elif "__" in filename:
|
||||
# the pattern seems to be that anything to left of the first "__"
|
||||
# is the series name followed by issue
|
||||
# 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)
|
||||
|
||||
filename = filename.replace("+", " ")
|
||||
@ -192,9 +190,10 @@ class FileNameParser:
|
||||
volume = ""
|
||||
|
||||
# save the last word
|
||||
try:
|
||||
last_word = series.split()[-1]
|
||||
except:
|
||||
split = series.split()
|
||||
if split:
|
||||
last_word = split[-1]
|
||||
else:
|
||||
last_word = ""
|
||||
|
||||
# remove any parenthetical phrases
|
||||
@ -223,12 +222,11 @@ class FileNameParser:
|
||||
# be removed to help search online
|
||||
if issue_start == 0:
|
||||
one_shot_words = ["tpb", "os", "one-shot", "ogn", "gn"]
|
||||
try:
|
||||
last_word = series.split()[-1]
|
||||
if last_word.lower() in one_shot_words:
|
||||
series = series.rsplit(" ", 1)[0]
|
||||
except:
|
||||
pass
|
||||
split = series.split()
|
||||
if split:
|
||||
last_word = split[-1]
|
||||
if last_word.casefold() in one_shot_words:
|
||||
series, _, _ = series.rpartition(" ")
|
||||
|
||||
if volume:
|
||||
series = re.sub(r"\s+v(|ol|olume)$", "", series)
|
||||
@ -343,7 +341,7 @@ class Parser:
|
||||
remove_fcbd: bool = False,
|
||||
remove_publisher: bool = False,
|
||||
) -> None:
|
||||
self.state: Optional[Callable[[Parser], Optional[Callable]]] = None
|
||||
self.state: Callable[[Parser], Callable | None] | None = None
|
||||
self.pos = -1
|
||||
|
||||
self.firstItem = True
|
||||
@ -406,7 +404,7 @@ class Parser:
|
||||
self.state = self.state(self)
|
||||
|
||||
|
||||
def parse(p: Parser) -> Optional[Callable[[Parser], Optional[Callable]]]:
|
||||
def parse(p: Parser) -> Callable[[Parser], Callable | None] | None:
|
||||
item: filenamelexer.Item = p.get()
|
||||
|
||||
# We're done, time to do final processing
|
||||
@ -430,7 +428,8 @@ def parse(p: Parser) -> Optional[Callable[[Parser], Optional[Callable]]]:
|
||||
if len(item.val.lstrip("0")) < 4:
|
||||
# An issue number starting with # Was not found and no previous number was found
|
||||
if p.issue_number_at is None:
|
||||
# Series has already been started/parsed, filters out leading alternate numbers leading alternate number
|
||||
# Series has already been started/parsed,
|
||||
# filters out leading alternate numbers leading alternate number
|
||||
if len(p.series_parts) > 0:
|
||||
# Unset first item
|
||||
if p.firstItem:
|
||||
@ -526,7 +525,8 @@ def parse(p: Parser) -> Optional[Callable[[Parser], Optional[Callable]]]:
|
||||
if p.peek_back().typ == filenamelexer.ItemType.Dot:
|
||||
p.used_items.append(p.peek_back())
|
||||
|
||||
# Allows removing DC from 'Wonder Woman 49 DC Sep-Oct 1951' dependent on publisher being in a static list in the lexer
|
||||
# Allows removing DC from 'Wonder Woman 49 DC Sep-Oct 1951'
|
||||
# dependent on publisher being in a static list in the lexer
|
||||
elif item.typ == filenamelexer.ItemType.Publisher:
|
||||
p.filename_info["publisher"] = item.val
|
||||
p.used_items.append(item)
|
||||
@ -656,7 +656,7 @@ def parse(p: Parser) -> Optional[Callable[[Parser], Optional[Callable]]]:
|
||||
|
||||
|
||||
# TODO: What about more esoteric numbers???
|
||||
def parse_issue_number(p: Parser) -> Optional[Callable[[Parser], Optional[Callable]]]:
|
||||
def parse_issue_number(p: Parser) -> Callable[[Parser], Callable | None] | None:
|
||||
item = p.input[p.pos]
|
||||
|
||||
if "issue" in p.filename_info:
|
||||
@ -689,7 +689,7 @@ def parse_issue_number(p: Parser) -> Optional[Callable[[Parser], Optional[Callab
|
||||
return parse
|
||||
|
||||
|
||||
def parse_series(p: Parser) -> Optional[Callable[[Parser], Optional[Callable]]]:
|
||||
def parse_series(p: Parser) -> Callable[[Parser], Callable | None] | None:
|
||||
item = p.input[p.pos]
|
||||
|
||||
series: list[list[filenamelexer.Item]] = [[]]
|
||||
@ -854,7 +854,7 @@ def resolve_year(p: Parser) -> None:
|
||||
p.title_parts.remove(selected_year)
|
||||
|
||||
|
||||
def parse_finish(p: Parser) -> Optional[Callable[[Parser], Optional[Callable]]]:
|
||||
def parse_finish(p: Parser) -> Callable[[Parser], Callable | None] | None:
|
||||
resolve_year(p)
|
||||
|
||||
# If we don't have an issue try to find it in the series
|
||||
@ -865,14 +865,16 @@ def parse_finish(p: Parser) -> Optional[Callable[[Parser], Optional[Callable]]]:
|
||||
if issue_num in [x[1] for x in p.year_candidates]:
|
||||
p.series_parts.append(issue_num)
|
||||
else:
|
||||
# If this number was rejected because of an operator and the operator is still there add it back e.g. 'IG-88'
|
||||
# If this number was rejected because of an operator and the operator is still there add it back
|
||||
# e.g. 'IG-88'
|
||||
if (
|
||||
issue_num in p.operator_rejected
|
||||
and p.series_parts
|
||||
and p.series_parts[-1].typ == filenamelexer.ItemType.Operator
|
||||
):
|
||||
p.series_parts.append(issue_num)
|
||||
# We have no reason to not use this number as the issue number. Specifically happens when parsing 'X-Men-V1-067.cbr'
|
||||
# We have no reason to not use this number as the issue number.
|
||||
# Specifically happens when parsing 'X-Men-V1-067.cbr'
|
||||
else:
|
||||
p.filename_info["issue"] = issue_num.val
|
||||
p.used_items.append(issue_num)
|
||||
@ -998,7 +1000,7 @@ def get_remainder(p: Parser) -> str:
|
||||
return remainder.strip()
|
||||
|
||||
|
||||
def parse_info_specifier(p: Parser) -> Optional[Callable[[Parser], Optional[Callable]]]:
|
||||
def parse_info_specifier(p: Parser) -> Callable[[Parser], Callable | None] | None:
|
||||
item = p.input[p.pos]
|
||||
index = p.pos
|
||||
|
||||
@ -1033,7 +1035,8 @@ def parse_info_specifier(p: Parser) -> Optional[Callable[[Parser], Optional[Call
|
||||
p.used_items.append(item)
|
||||
p.used_items.append(number)
|
||||
|
||||
# This is not for the issue number it is not in either the issue or the title, assume it is the volume number and count
|
||||
# This is not for the issue number it is not in either the issue or the title,
|
||||
# assume it is the volume number and count
|
||||
elif p.issue_number_at != i.pos and i not in p.series_parts and i not in p.title_parts:
|
||||
p.filename_info["volume"] = i.val
|
||||
p.filename_info["volume_count"] = str(int(t2do.convert(number.val)))
|
||||
@ -1053,12 +1056,13 @@ def parse_info_specifier(p: Parser) -> Optional[Callable[[Parser], Optional[Call
|
||||
|
||||
|
||||
# Gets 03 in '03 of 6'
|
||||
def get_number(p: Parser, index: int) -> Optional[filenamelexer.Item]:
|
||||
def get_number(p: Parser, index: int) -> filenamelexer.Item | None:
|
||||
# Go backward through the filename to see if we can find what this is of eg '03 (of 6)' or '008 title 03 (of 6)'
|
||||
rev = p.input[:index]
|
||||
rev.reverse()
|
||||
for i in rev:
|
||||
# We don't care about these types, we are looking to see if there is a number that is possibly different from the issue number for this count
|
||||
# We don't care about these types, we are looking to see if there is a number that is possibly different from
|
||||
# the issue number for this count
|
||||
if i.typ in [
|
||||
filenamelexer.ItemType.LeftParen,
|
||||
filenamelexer.ItemType.LeftBrace,
|
||||
|
@ -5,23 +5,23 @@ tagging schemes and databases, such as ComicVine or GCD. This makes conversion
|
||||
possible, however lossy it might be
|
||||
|
||||
"""
|
||||
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, List, Optional, TypedDict
|
||||
from typing import Any, TypedDict
|
||||
|
||||
from comicapi import utils
|
||||
|
||||
@ -76,59 +76,59 @@ class GenericMetadata:
|
||||
def __init__(self) -> None:
|
||||
|
||||
self.is_empty: bool = True
|
||||
self.tag_origin: Optional[str] = None
|
||||
self.tag_origin: str | None = None
|
||||
|
||||
self.series: Optional[str] = None
|
||||
self.issue: Optional[str] = None
|
||||
self.title: Optional[str] = None
|
||||
self.publisher: Optional[str] = None
|
||||
self.month: Optional[int] = None
|
||||
self.year: Optional[int] = None
|
||||
self.day: Optional[int] = None
|
||||
self.issue_count: Optional[int] = None
|
||||
self.volume: Optional[int] = None
|
||||
self.genre: Optional[str] = None
|
||||
self.language: Optional[str] = None # 2 letter iso code
|
||||
self.comments: Optional[str] = None # use same way as Summary in CIX
|
||||
self.series: str | None = None
|
||||
self.issue: str | None = None
|
||||
self.title: str | None = None
|
||||
self.publisher: str | None = None
|
||||
self.month: int | None = None
|
||||
self.year: int | None = None
|
||||
self.day: int | None = None
|
||||
self.issue_count: int | None = None
|
||||
self.volume: int | None = None
|
||||
self.genre: str | None = None
|
||||
self.language: str | None = None # 2 letter iso code
|
||||
self.comments: str | None = None # use same way as Summary in CIX
|
||||
|
||||
self.volume_count: Optional[int] = None
|
||||
self.critical_rating: Optional[str] = None
|
||||
self.country: Optional[str] = None
|
||||
self.volume_count: int | None = None
|
||||
self.critical_rating: str | None = None
|
||||
self.country: str | None = None
|
||||
|
||||
self.alternate_series: Optional[str] = None
|
||||
self.alternate_number: Optional[str] = None
|
||||
self.alternate_count: Optional[int] = None
|
||||
self.imprint: Optional[str] = None
|
||||
self.notes: Optional[str] = None
|
||||
self.web_link: Optional[str] = None
|
||||
self.format: Optional[str] = None
|
||||
self.manga: Optional[str] = None
|
||||
self.black_and_white: Optional[bool] = None
|
||||
self.page_count: Optional[int] = None
|
||||
self.maturity_rating: Optional[str] = None
|
||||
self.community_rating: Optional[str] = None
|
||||
self.alternate_series: str | None = None
|
||||
self.alternate_number: str | None = None
|
||||
self.alternate_count: int | None = None
|
||||
self.imprint: str | None = None
|
||||
self.notes: str | None = None
|
||||
self.web_link: str | None = None
|
||||
self.format: str | None = None
|
||||
self.manga: str | None = None
|
||||
self.black_and_white: bool | None = None
|
||||
self.page_count: int | None = None
|
||||
self.maturity_rating: str | None = None
|
||||
self.community_rating: str | None = None
|
||||
|
||||
self.story_arc: Optional[str] = None
|
||||
self.series_group: Optional[str] = None
|
||||
self.scan_info: Optional[str] = None
|
||||
self.story_arc: str | None = None
|
||||
self.series_group: str | None = None
|
||||
self.scan_info: str | None = None
|
||||
|
||||
self.characters: Optional[str] = None
|
||||
self.teams: Optional[str] = None
|
||||
self.locations: Optional[str] = None
|
||||
self.characters: str | None = None
|
||||
self.teams: str | None = None
|
||||
self.locations: str | None = None
|
||||
|
||||
self.credits: List[CreditMetadata] = []
|
||||
self.tags: List[str] = []
|
||||
self.pages: List[ImageMetadata] = []
|
||||
self.credits: list[CreditMetadata] = []
|
||||
self.tags: list[str] = []
|
||||
self.pages: list[ImageMetadata] = []
|
||||
|
||||
# Some CoMet-only items
|
||||
self.price: Optional[str] = None
|
||||
self.is_version_of: Optional[str] = None
|
||||
self.rights: Optional[str] = None
|
||||
self.identifier: Optional[str] = None
|
||||
self.last_mark: Optional[str] = None
|
||||
self.cover_image: Optional[str] = None
|
||||
self.price: str | None = None
|
||||
self.is_version_of: str | None = None
|
||||
self.rights: str | None = None
|
||||
self.identifier: str | None = None
|
||||
self.last_mark: str | None = None
|
||||
self.cover_image: str | None = None
|
||||
|
||||
def overlay(self, new_md: "GenericMetadata") -> None:
|
||||
def overlay(self, new_md: GenericMetadata) -> None:
|
||||
"""Overlay a metadata object on this one
|
||||
|
||||
That is, when the new object has non-None values, over-write them
|
||||
@ -198,7 +198,7 @@ class GenericMetadata:
|
||||
if len(new_md.pages) > 0:
|
||||
assign("pages", new_md.pages)
|
||||
|
||||
def overlay_credits(self, new_credits: List[CreditMetadata]) -> None:
|
||||
def overlay_credits(self, new_credits: list[CreditMetadata]) -> None:
|
||||
for c in new_credits:
|
||||
primary = bool("primary" in c and c["primary"])
|
||||
|
||||
@ -220,8 +220,7 @@ class GenericMetadata:
|
||||
self.pages.append(page_dict)
|
||||
|
||||
def get_archive_page_index(self, pagenum: int) -> int:
|
||||
# convert the displayed page number to the page index of the file in
|
||||
# the archive
|
||||
# convert the displayed page number to the page index of the file in the archive
|
||||
if pagenum < len(self.pages):
|
||||
return int(self.pages[pagenum]["Image"])
|
||||
|
||||
@ -374,9 +373,9 @@ 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."
|
||||
"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
|
||||
|
@ -4,31 +4,29 @@ Class for handling the odd permutations of an 'issue number' that the
|
||||
comics industry throws at us.
|
||||
e.g.: "12", "12.1", "0", "-1", "5AU", "100-2"
|
||||
"""
|
||||
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import unicodedata
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IssueString:
|
||||
def __init__(self, text: Optional[str]) -> None:
|
||||
def __init__(self, text: str | None) -> None:
|
||||
|
||||
# break up the issue number string into 2 parts: the numeric and suffix string.
|
||||
# (assumes that the numeric portion is always first)
|
||||
@ -52,8 +50,7 @@ class IssueString:
|
||||
|
||||
# if it's still not numeric at start skip it
|
||||
if text[start].isdigit() or text[start] == ".":
|
||||
# walk through the string, look for split point (the first
|
||||
# non-numeric)
|
||||
# walk through the string, look for split point (the first non-numeric)
|
||||
decimal_count = 0
|
||||
for idx in range(start, len(text)):
|
||||
if text[idx] not in "0123456789.":
|
||||
@ -71,8 +68,7 @@ class IssueString:
|
||||
if text[idx - 1] == "." and len(text) != idx:
|
||||
idx = idx - 1
|
||||
|
||||
# if there is no numeric after the minus, make the minus part of
|
||||
# the suffix
|
||||
# if there is no numeric after the minus, make the minus part of the suffix
|
||||
if idx == 1 and start == 1:
|
||||
idx = 0
|
||||
|
||||
@ -113,7 +109,7 @@ class IssueString:
|
||||
|
||||
return num_s
|
||||
|
||||
def as_float(self) -> Optional[float]:
|
||||
def as_float(self) -> float | None:
|
||||
# return the float, with no suffix
|
||||
if len(self.suffix) == 1 and self.suffix.isnumeric():
|
||||
return (self.num or 0) + unicodedata.numeric(self.suffix)
|
||||
|
@ -1,18 +1,18 @@
|
||||
"""Some generic utilities"""
|
||||
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
@ -21,7 +21,7 @@ import pathlib
|
||||
import re
|
||||
import unicodedata
|
||||
from collections import defaultdict
|
||||
from typing import Any, Iterable, List, Optional, Union
|
||||
from typing import Any, Mapping
|
||||
|
||||
import pycountry
|
||||
|
||||
@ -32,7 +32,7 @@ class UtilsVars:
|
||||
already_fixed_encoding = False
|
||||
|
||||
|
||||
def get_recursive_filelist(pathlist: List[str]) -> List[str]:
|
||||
def get_recursive_filelist(pathlist: list[str]) -> list[str]:
|
||||
"""Get a recursive list of of all files under all path items in the list"""
|
||||
|
||||
filelist = []
|
||||
@ -55,7 +55,7 @@ def get_recursive_filelist(pathlist: List[str]) -> List[str]:
|
||||
return filelist
|
||||
|
||||
|
||||
def list_to_string(lst: List[Union[str, Any]]) -> str:
|
||||
def list_to_string(lst: list[str | Any]) -> str:
|
||||
string = ""
|
||||
if lst is not None:
|
||||
for item in lst:
|
||||
@ -77,7 +77,7 @@ def add_to_path(dirname: str) -> None:
|
||||
os.environ["PATH"] = dirname + os.pathsep + os.environ["PATH"]
|
||||
|
||||
|
||||
def which(program: str) -> Optional[str]:
|
||||
def which(program: str) -> str | None:
|
||||
"""Returns path of the executable, if it exists"""
|
||||
|
||||
def is_exe(fpath: str) -> bool:
|
||||
@ -173,9 +173,9 @@ def unique_file(file_name: str) -> str:
|
||||
counter += 1
|
||||
|
||||
|
||||
languages: dict[Optional[str], Optional[str]] = defaultdict(lambda: None)
|
||||
languages: dict[str | None, str | None] = defaultdict(lambda: None)
|
||||
|
||||
countries: dict[Optional[str], Optional[str]] = defaultdict(lambda: None)
|
||||
countries: dict[str | None, str | None] = defaultdict(lambda: None)
|
||||
|
||||
for c in pycountry.countries:
|
||||
if "alpha_2" in c._fields:
|
||||
@ -186,11 +186,11 @@ for lng in pycountry.languages:
|
||||
languages[lng.alpha_2] = lng.name
|
||||
|
||||
|
||||
def get_language_from_iso(iso: Optional[str]) -> Optional[str]:
|
||||
def get_language_from_iso(iso: str | None) -> str | None:
|
||||
return languages[iso]
|
||||
|
||||
|
||||
def get_language(string: Optional[str]) -> Optional[str]:
|
||||
def get_language(string: str | None) -> str | None:
|
||||
if string is None:
|
||||
return None
|
||||
|
||||
@ -199,7 +199,7 @@ def get_language(string: Optional[str]) -> Optional[str]:
|
||||
if lang is None:
|
||||
try:
|
||||
return str(pycountry.languages.lookup(string).name)
|
||||
except:
|
||||
except LookupError:
|
||||
return None
|
||||
return lang
|
||||
|
||||
@ -217,7 +217,7 @@ def get_publisher(publisher: str) -> tuple[str, str]:
|
||||
return (imprint, publisher)
|
||||
|
||||
|
||||
def update_publishers(new_publishers: dict[str, dict[str, str]]) -> None:
|
||||
def update_publishers(new_publishers: Mapping[str, Mapping[str, str]]) -> None:
|
||||
for publisher in new_publishers:
|
||||
if publisher in publishers:
|
||||
publishers[publisher].update(new_publishers[publisher])
|
||||
@ -233,7 +233,7 @@ class ImprintDict(dict):
|
||||
if the key does not exist the key is returned as the publisher unchanged
|
||||
"""
|
||||
|
||||
def __init__(self, publisher: str, mapping: Iterable = (), **kwargs: Any):
|
||||
def __init__(self, publisher: str, mapping=(), **kwargs) -> None:
|
||||
super().__init__(mapping, **kwargs)
|
||||
self.publisher = publisher
|
||||
|
||||
@ -249,7 +249,7 @@ class ImprintDict(dict):
|
||||
else:
|
||||
return (item, self.publisher, True)
|
||||
|
||||
def copy(self) -> "ImprintDict":
|
||||
def copy(self) -> ImprintDict:
|
||||
return ImprintDict(self.publisher, super().copy())
|
||||
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import localefix
|
||||
from comictaggerlib.main import ctmain
|
||||
|
||||
|
@ -0,0 +1 @@
|
||||
from __future__ import annotations
|
@ -1,22 +1,23 @@
|
||||
"""A PyQT4 dialog to select from automated issue matches"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Callable, List
|
||||
from typing import Callable
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
|
||||
@ -36,7 +37,7 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
|
||||
def __init__(
|
||||
self,
|
||||
parent: QtWidgets.QWidget,
|
||||
match_set_list: List[MultipleMatch],
|
||||
match_set_list: list[MultipleMatch],
|
||||
style: int,
|
||||
fetch_func: Callable[[IssueResult], GenericMetadata],
|
||||
settings: ComicTaggerSettings,
|
||||
|
@ -1,19 +1,19 @@
|
||||
"""A PyQT4 dialog to show ID log and progress"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
|
@ -1,19 +1,19 @@
|
||||
"""A PyQT4 dialog to confirm and set options for auto-tag"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
|
@ -1,21 +1,21 @@
|
||||
"""A class to manage modifying metadata specifically for CBL/CBI"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from comicapi.genericmetadata import CreditMetadata, GenericMetadata
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
@ -34,7 +34,7 @@ class CBLTransformer:
|
||||
if item.lower() not in (tag.lower() for tag in self.metadata.tags):
|
||||
self.metadata.tags.append(item)
|
||||
|
||||
def add_string_list_to_tags(str_list: Optional[str]) -> None:
|
||||
def add_string_list_to_tags(str_list: str | None) -> None:
|
||||
if str_list:
|
||||
items = [s.strip() for s in str_list.split(",")]
|
||||
for item in items:
|
||||
@ -43,8 +43,8 @@ class CBLTransformer:
|
||||
if self.settings.assume_lone_credit_is_primary:
|
||||
|
||||
# helper
|
||||
def set_lone_primary(role_list: list[str]) -> tuple[Optional[CreditMetadata], int]:
|
||||
lone_credit: Optional[CreditMetadata] = None
|
||||
def set_lone_primary(role_list: list[str]) -> tuple[CreditMetadata | None, int]:
|
||||
lone_credit: CreditMetadata | None = None
|
||||
count = 0
|
||||
for c in self.metadata.credits:
|
||||
if c["role"].lower() in role_list:
|
||||
|
@ -1,20 +1,20 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
"""ComicTagger CLI functions"""
|
||||
|
||||
#
|
||||
# Copyright 2013 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
@ -539,7 +539,7 @@ def process_file_cli(
|
||||
if opts.delete_after_zip_export:
|
||||
try:
|
||||
os.unlink(rar_file)
|
||||
except:
|
||||
except OSError:
|
||||
logger.exception(msg_hdr + "Error deleting original RAR after export")
|
||||
delete_success = False
|
||||
else:
|
||||
|
@ -1,24 +1,25 @@
|
||||
"""A python class to manage caching of data from Comic Vine"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import sqlite3 as lite
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from comicapi import utils
|
||||
from comictaggerlib import ctversion
|
||||
@ -40,7 +41,7 @@ class ComicVineCacher:
|
||||
with open(self.version_file, "rb") as f:
|
||||
data = f.read().decode("utf-8")
|
||||
f.close()
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
if data != ctversion.version:
|
||||
self.clear_cache()
|
||||
@ -51,11 +52,11 @@ class ComicVineCacher:
|
||||
def clear_cache(self) -> None:
|
||||
try:
|
||||
os.unlink(self.db_file)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
os.unlink(self.version_file)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def create_cache_db(self) -> None:
|
||||
@ -283,9 +284,9 @@ class ComicVineCacher:
|
||||
}
|
||||
self.upsert(cur, "issues", "id", issue["id"], data)
|
||||
|
||||
def get_volume_info(self, volume_id: int) -> Optional[CVVolumeResults]:
|
||||
def get_volume_info(self, volume_id: int) -> CVVolumeResults | None:
|
||||
|
||||
result: Optional[CVVolumeResults] = None
|
||||
result: CVVolumeResults | None = None
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
with con:
|
||||
@ -333,7 +334,10 @@ class ComicVineCacher:
|
||||
results: list[CVIssuesResults] = []
|
||||
|
||||
cur.execute(
|
||||
"SELECT id,name,issue_number,site_detail_url,cover_date,super_url,thumb_url,description FROM Issues WHERE volume_id = ?",
|
||||
(
|
||||
"SELECT id,name,issue_number,site_detail_url,cover_date,super_url,thumb_url,description"
|
||||
" FROM Issues WHERE volume_id = ?"
|
||||
),
|
||||
[volume_id],
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
@ -1,25 +1,26 @@
|
||||
"""A python class to manage communication with Comic Vine's REST API"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any, Callable, Optional, Union, cast
|
||||
from typing import Any, Callable, cast
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
@ -71,7 +72,7 @@ def list_fetch_complete(url_list: list[str]) -> None:
|
||||
...
|
||||
|
||||
|
||||
def url_fetch_complete(image_url: str, thumb_url: Optional[str]) -> None:
|
||||
def url_fetch_complete(image_url: str, thumb_url: str | None) -> None:
|
||||
...
|
||||
|
||||
|
||||
@ -97,14 +98,14 @@ class ComicVineTalker:
|
||||
# key that is registered to comictagger
|
||||
default_api_key = "27431e6787042105bd3e47e169a624521f89f3a4"
|
||||
|
||||
self.issue_id: Optional[int] = None
|
||||
self.issue_id: int | None = None
|
||||
|
||||
if ComicVineTalker.api_key == "":
|
||||
self.api_key = default_api_key
|
||||
else:
|
||||
self.api_key = ComicVineTalker.api_key
|
||||
|
||||
self.log_func: Optional[Callable[[str], None]] = None
|
||||
self.log_func: Callable[[str], None] | None = None
|
||||
|
||||
if qt_available:
|
||||
self.nam = QtNetwork.QNetworkAccessManager()
|
||||
@ -118,7 +119,7 @@ class ComicVineTalker:
|
||||
else:
|
||||
self.log_func(text)
|
||||
|
||||
def parse_date_str(self, date_str: str) -> tuple[Optional[int], Optional[int], Optional[int]]:
|
||||
def parse_date_str(self, date_str: str) -> tuple[int | None, int | None, int | None]:
|
||||
day = None
|
||||
month = None
|
||||
year = None
|
||||
@ -142,7 +143,7 @@ class ComicVineTalker:
|
||||
|
||||
# Bogus request, but if the key is wrong, you get error 100: "Invalid API Key"
|
||||
return cv_response["status_code"] != 100
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_cv_content(self, url: str, params: dict[str, Any]) -> CVResult:
|
||||
@ -199,7 +200,7 @@ class ComicVineTalker:
|
||||
raise ComicVineTalkerException(ComicVineTalkerException.Unknown, "Error on Comic Vine server")
|
||||
|
||||
def search_for_series(
|
||||
self, series_name: str, callback: Optional[Callable[[int, int], None]] = None, refresh_cache: bool = False
|
||||
self, series_name: str, callback: Callable[[int, int], None] | None = None, refresh_cache: bool = False
|
||||
) -> list[CVVolumeResults]:
|
||||
|
||||
# Sanitize the series name for comicvine searching, comicvine search ignore symbols
|
||||
@ -372,7 +373,7 @@ class ComicVineTalker:
|
||||
return volume_issues_result
|
||||
|
||||
def fetch_issues_by_volume_issue_num_and_year(
|
||||
self, volume_id_list: list[int], issue_number: str, year: Union[str, int, None]
|
||||
self, volume_id_list: list[int], issue_number: str, year: str | int | None
|
||||
) -> list[CVIssuesResults]:
|
||||
volume_filter = ""
|
||||
for vid in volume_id_list:
|
||||
@ -383,7 +384,7 @@ class ComicVineTalker:
|
||||
if int_year is not None:
|
||||
flt += f",cover_date:{int_year}-1-1|{int_year+1}-1-1"
|
||||
|
||||
params: dict[str, Union[str, int]] = {
|
||||
params: dict[str, str | int] = {
|
||||
"api_key": self.api_key,
|
||||
"format": "json",
|
||||
"field_list": "id,volume,issue_number,name,image,cover_date,site_detail_url,description",
|
||||
@ -473,7 +474,10 @@ class ComicVineTalker:
|
||||
if settings.use_series_start_as_volume:
|
||||
metadata.volume = int(volume_results["start_year"])
|
||||
|
||||
metadata.notes = f"Tagged with ComicTagger {ctversion.version} using info from Comic Vine on {datetime.now():%Y-%m-%d %H:%M:%S}. [Issue ID {issue_results['id']}]"
|
||||
metadata.notes = (
|
||||
f"Tagged with ComicTagger {ctversion.version} using info from Comic Vine on"
|
||||
f" {datetime.now():%Y-%m-%d %H:%M:%S}. [Issue ID {issue_results['id']}]"
|
||||
)
|
||||
metadata.web_link = issue_results["site_detail_url"]
|
||||
|
||||
person_credits = issue_results["person_credits"]
|
||||
@ -584,7 +588,7 @@ class ComicVineTalker:
|
||||
# now we have the data, make it into text
|
||||
fmtstr = ""
|
||||
for w in col_widths:
|
||||
fmtstr += " {{:{}}}|".format(w + 1)
|
||||
fmtstr += f" {{:{w + 1}}}|"
|
||||
width = sum(col_widths) + len(col_widths) * 2
|
||||
table_text = ""
|
||||
counter = 0
|
||||
@ -597,7 +601,7 @@ class ComicVineTalker:
|
||||
table_strings.append(table_text)
|
||||
|
||||
newstring = newstring.format(*table_strings)
|
||||
except:
|
||||
except Exception:
|
||||
# we caught an error rebuilding the table.
|
||||
# just bail and remove the formatting
|
||||
logger.exception("table parse error")
|
||||
@ -605,16 +609,16 @@ class ComicVineTalker:
|
||||
|
||||
return newstring
|
||||
|
||||
def fetch_issue_date(self, issue_id: int) -> tuple[Optional[int], Optional[int]]:
|
||||
def fetch_issue_date(self, issue_id: int) -> tuple[int | None, int | None]:
|
||||
details = self.fetch_issue_select_details(issue_id)
|
||||
_, month, year = self.parse_date_str(details["cover_date"] or "")
|
||||
return month, year
|
||||
|
||||
def fetch_issue_cover_urls(self, issue_id: int) -> tuple[Optional[str], Optional[str]]:
|
||||
def fetch_issue_cover_urls(self, issue_id: int) -> tuple[str | None, str | None]:
|
||||
details = self.fetch_issue_select_details(issue_id)
|
||||
return details["image_url"], details["thumb_image_url"]
|
||||
|
||||
def fetch_issue_page_url(self, issue_id: int) -> Optional[str]:
|
||||
def fetch_issue_page_url(self, issue_id: int) -> str | None:
|
||||
details = self.fetch_issue_select_details(issue_id)
|
||||
return details["site_detail_url"]
|
||||
|
||||
@ -736,7 +740,7 @@ class ComicVineTalker:
|
||||
self.nam.finished.connect(self.async_fetch_issue_cover_url_complete)
|
||||
self.nam.get(QtNetwork.QNetworkRequest(QtCore.QUrl(issue_url)))
|
||||
|
||||
def async_fetch_issue_cover_url_complete(self, reply: "QtNetwork.QNetworkReply") -> None:
|
||||
def async_fetch_issue_cover_url_complete(self, reply: QtNetwork.QNetworkReply) -> None:
|
||||
# read in the response
|
||||
data = reply.readAll()
|
||||
|
||||
@ -772,7 +776,7 @@ class ComicVineTalker:
|
||||
self.nam.finished.connect(self.async_fetch_alternate_cover_urls_complete)
|
||||
self.nam.get(QtNetwork.QNetworkRequest(QtCore.QUrl(str(issue_page_url))))
|
||||
|
||||
def async_fetch_alternate_cover_urls_complete(self, reply: "QtNetwork.QNetworkReply") -> None:
|
||||
def async_fetch_alternate_cover_urls_complete(self, reply: QtNetwork.QNetworkReply) -> None:
|
||||
# read in the response
|
||||
html = str(reply.readAll())
|
||||
alt_cover_url_list = self.parse_out_alt_cover_urls(html)
|
||||
@ -783,7 +787,7 @@ class ComicVineTalker:
|
||||
ComicVineTalker.alt_url_list_fetch_complete(alt_cover_url_list)
|
||||
|
||||
def repair_urls(
|
||||
self, issue_list: Union[list[CVIssuesResults], list[CVVolumeResults], list[CVIssueDetailResults]]
|
||||
self, issue_list: list[CVIssuesResults] | list[CVVolumeResults] | list[CVIssueDetailResults]
|
||||
) -> None:
|
||||
# make sure there are URLs for the image fields
|
||||
for issue in issue_list:
|
||||
|
@ -3,24 +3,24 @@
|
||||
Display cover images from either a local archive, or from Comic Vine.
|
||||
TODO: This should be re-factored using subclasses!
|
||||
"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Callable, Optional, Union, cast
|
||||
from typing import Callable, cast
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
|
||||
@ -74,10 +74,10 @@ class Signal(QtCore.QObject):
|
||||
def emit_list(self, url_list: list[str]) -> None:
|
||||
self.alt_url_list_fetch_complete.emit(url_list)
|
||||
|
||||
def emit_url(self, image_url: str, thumb_url: Optional[str]) -> None:
|
||||
def emit_url(self, image_url: str, thumb_url: str | None) -> None:
|
||||
self.url_fetch_complete.emit(image_url, thumb_url)
|
||||
|
||||
def emit_image(self, image_data: Union[bytes, QtCore.QByteArray]) -> None:
|
||||
def emit_image(self, image_data: bytes | QtCore.QByteArray) -> None:
|
||||
self.image_fetch_complete.emit(image_data)
|
||||
|
||||
|
||||
@ -99,13 +99,13 @@ class CoverImageWidget(QtWidgets.QWidget):
|
||||
)
|
||||
|
||||
self.mode: int = mode
|
||||
self.page_loader: Optional[PageLoader] = None
|
||||
self.page_loader: PageLoader | None = None
|
||||
self.showControls = True
|
||||
|
||||
self.current_pixmap = QtGui.QPixmap()
|
||||
|
||||
self.comic_archive: Optional[ComicArchive] = None
|
||||
self.issue_id: Optional[int] = None
|
||||
self.comic_archive: ComicArchive | None = None
|
||||
self.issue_id: int | None = None
|
||||
self.url_list: list[str] = []
|
||||
if self.page_loader is not None:
|
||||
self.page_loader.abandoned = True
|
||||
@ -193,7 +193,7 @@ class CoverImageWidget(QtWidgets.QWidget):
|
||||
|
||||
self.update_content()
|
||||
|
||||
def primary_url_fetch_complete(self, primary_url: str, thumb_url: Optional[str]) -> None:
|
||||
def primary_url_fetch_complete(self, primary_url: str, thumb_url: str | None) -> None:
|
||||
self.url_list.append(str(primary_url))
|
||||
self.imageIndex = 0
|
||||
self.imageCount = len(self.url_list)
|
||||
|
@ -1,19 +1,19 @@
|
||||
"""A PyQT4 dialog to edit credits"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
@ -1,19 +1,19 @@
|
||||
"""A PyQT4 dialog to confirm and set options for export to zip"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
|
@ -1,18 +1,19 @@
|
||||
"""Functions for renaming files based on metadata"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
import logging
|
||||
@ -20,7 +21,7 @@ import os
|
||||
import pathlib
|
||||
import string
|
||||
import sys
|
||||
from typing import Any, Optional, cast
|
||||
from typing import Any, cast
|
||||
|
||||
from pathvalidate import sanitize_filename
|
||||
|
||||
@ -119,7 +120,7 @@ class MetadataFormatter(string.Formatter):
|
||||
|
||||
|
||||
class FileRenamer:
|
||||
def __init__(self, metadata: Optional[GenericMetadata], platform: str = "auto") -> None:
|
||||
def __init__(self, metadata: GenericMetadata | None, platform: str = "auto") -> None:
|
||||
self.template = "{publisher}/{series}/{series} v{volume} #{issue} (of {issue_count}) ({year})"
|
||||
self.smart_cleanup = True
|
||||
self.issue_zero_padding = 3
|
||||
|
@ -1,22 +1,23 @@
|
||||
"""A PyQt5 widget for managing list of comic archive files"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Callable, List, Optional, cast
|
||||
from typing import Callable, cast
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets, uic
|
||||
|
||||
@ -130,13 +131,13 @@ class FileSelectionList(QtWidgets.QWidget):
|
||||
elif self.twList.rowCount() <= 0:
|
||||
self.listCleared.emit()
|
||||
|
||||
def get_archive_by_row(self, row: int) -> Optional[ComicArchive]:
|
||||
def get_archive_by_row(self, row: int) -> ComicArchive | None:
|
||||
if row >= 0:
|
||||
fi: FileInfo = self.twList.item(row, FileSelectionList.dataColNum).data(QtCore.Qt.ItemDataRole.UserRole)
|
||||
return fi.ca
|
||||
return None
|
||||
|
||||
def get_current_archive(self) -> Optional[ComicArchive]:
|
||||
def get_current_archive(self) -> ComicArchive | None:
|
||||
return self.get_archive_by_row(self.twList.currentRow())
|
||||
|
||||
def remove_selection(self) -> None:
|
||||
@ -345,8 +346,8 @@ class FileSelectionList(QtWidgets.QWidget):
|
||||
fi.ca.read_cix()
|
||||
fi.ca.has_cbi()
|
||||
|
||||
def get_selected_archive_list(self) -> List[ComicArchive]:
|
||||
ca_list: List[ComicArchive] = []
|
||||
def get_selected_archive_list(self) -> list[ComicArchive]:
|
||||
ca_list: list[ComicArchive] = []
|
||||
for r in range(self.twList.rowCount()):
|
||||
item = self.twList.item(r, FileSelectionList.dataColNum)
|
||||
if item.isSelected():
|
||||
@ -366,7 +367,7 @@ class FileSelectionList(QtWidgets.QWidget):
|
||||
self.update_row(r)
|
||||
self.twList.setSortingEnabled(True)
|
||||
|
||||
def current_item_changed_cb(self, curr: Optional[QtCore.QModelIndex], prev: Optional[QtCore.QModelIndex]) -> None:
|
||||
def current_item_changed_cb(self, curr: QtCore.QModelIndex | None, prev: QtCore.QModelIndex | None) -> None:
|
||||
if curr is not None:
|
||||
new_idx = curr.row()
|
||||
old_idx = -1
|
||||
|
@ -1,18 +1,19 @@
|
||||
"""A class to manage fetching and caching of images by URL"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
@ -20,7 +21,6 @@ import os
|
||||
import shutil
|
||||
import sqlite3 as lite
|
||||
import tempfile
|
||||
from typing import Union
|
||||
|
||||
import requests
|
||||
|
||||
@ -41,7 +41,7 @@ class ImageFetcherException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def fetch_complete(image_data: "Union[bytes, QtCore.QByteArray]") -> None:
|
||||
def fetch_complete(image_data: bytes | QtCore.QByteArray) -> None:
|
||||
...
|
||||
|
||||
|
||||
@ -106,7 +106,7 @@ class ImageFetcher:
|
||||
# we'll get called back when done...
|
||||
return bytes()
|
||||
|
||||
def finish_request(self, reply: "QtNetwork.QNetworkReply") -> None:
|
||||
def finish_request(self, reply: QtNetwork.QNetworkReply) -> None:
|
||||
# read in the image data
|
||||
logger.debug("request finished")
|
||||
image_data = reply.readAll()
|
||||
@ -134,7 +134,7 @@ class ImageFetcher:
|
||||
|
||||
cur.execute("CREATE TABLE Images(url TEXT,filename TEXT,timestamp TEXT,PRIMARY KEY (url))")
|
||||
|
||||
def add_image_to_cache(self, url: str, image_data: "Union[bytes, QtCore.QByteArray]") -> None:
|
||||
def add_image_to_cache(self, url: str, image_data: bytes | QtCore.QByteArray) -> None:
|
||||
|
||||
con = lite.connect(self.db_file)
|
||||
|
||||
@ -168,7 +168,7 @@ class ImageFetcher:
|
||||
with open(filename, "rb") as f:
|
||||
image_data = f.read()
|
||||
f.close()
|
||||
except IOError:
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return image_data
|
||||
|
@ -1,23 +1,24 @@
|
||||
"""A class to manage creating image content hashes, and calculate hamming distances"""
|
||||
|
||||
#
|
||||
# Copyright 2013 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import logging
|
||||
from functools import reduce
|
||||
from typing import Optional, TypeVar
|
||||
from typing import TypeVar
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
@ -29,12 +30,12 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImageHasher:
|
||||
def __init__(self, path: Optional[str] = None, data: bytes = bytes(), width: int = 8, height: int = 8) -> None:
|
||||
def __init__(self, path: str | None = None, data: bytes = bytes(), width: int = 8, height: int = 8) -> None:
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
if path is None and not data:
|
||||
raise IOError
|
||||
raise OSError
|
||||
|
||||
try:
|
||||
if path is not None:
|
||||
@ -86,7 +87,6 @@ class ImageHasher:
|
||||
result = reduce(lambda x, (y, z): x | (z << y),
|
||||
enumerate(map(lambda i: 0 if i < 0 else 1, filt_data)),
|
||||
0)
|
||||
#print("{0:016x}".format(result))
|
||||
return result
|
||||
"""
|
||||
|
||||
@ -164,7 +164,6 @@ class ImageHasher:
|
||||
result = reduce(set_bit, enumerate(bitlist), long(0))
|
||||
|
||||
|
||||
#print("{0:016x}".format(result))
|
||||
return result
|
||||
"""
|
||||
|
||||
|
@ -1,19 +1,19 @@
|
||||
"""A PyQT4 widget to display a popup image"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
@ -26,7 +26,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class ImagePopup(QtWidgets.QDialog):
|
||||
def __init__(self, parent: QtWidgets.QWidget, image_pixmap: QtGui.QPixmap) -> None:
|
||||
super(ImagePopup, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("imagepopup.ui"), self)
|
||||
|
||||
|
@ -1,23 +1,24 @@
|
||||
"""A class to automatically identify a comic archive"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import logging
|
||||
import sys
|
||||
from typing import Any, Callable, List, Optional
|
||||
from typing import Any, Callable
|
||||
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
@ -42,11 +43,11 @@ except ImportError:
|
||||
|
||||
|
||||
class SearchKeys(TypedDict):
|
||||
series: Optional[str]
|
||||
issue_number: Optional[str]
|
||||
month: Optional[int]
|
||||
year: Optional[int]
|
||||
issue_count: Optional[int]
|
||||
series: str | None
|
||||
issue_number: str | None
|
||||
month: int | None
|
||||
year: int | None
|
||||
issue_count: int | None
|
||||
|
||||
|
||||
class Score(TypedDict):
|
||||
@ -101,8 +102,8 @@ class IssueIdentifier:
|
||||
|
||||
self.additional_metadata = GenericMetadata()
|
||||
self.output_function: Callable[[str], None] = IssueIdentifier.default_write_output
|
||||
self.callback: Optional[Callable[[int, int], None]] = None
|
||||
self.cover_url_callback: Optional[Callable[[bytes], None]] = None
|
||||
self.callback: Callable[[int, int], None] | None = None
|
||||
self.cover_url_callback: Callable[[bytes], None] | None = None
|
||||
self.search_result = self.result_no_matches
|
||||
self.cover_page_index = 0
|
||||
self.cancel = False
|
||||
@ -122,7 +123,7 @@ class IssueIdentifier:
|
||||
def set_name_length_delta_threshold(self, delta: int) -> None:
|
||||
self.length_delta_thresh = delta
|
||||
|
||||
def set_publisher_filter(self, flt: List[str]) -> None:
|
||||
def set_publisher_filter(self, flt: list[str]) -> None:
|
||||
self.publisher_filter = flt
|
||||
|
||||
def set_hasher_algorithm(self, algo: int) -> None:
|
||||
@ -144,7 +145,7 @@ class IssueIdentifier:
|
||||
im = Image.open(io.BytesIO(image_data))
|
||||
w, h = im.size
|
||||
return float(h) / float(w)
|
||||
except:
|
||||
except Exception:
|
||||
return 1.5
|
||||
|
||||
def crop_cover(self, image_data: bytes) -> bytes:
|
||||
@ -154,7 +155,7 @@ class IssueIdentifier:
|
||||
|
||||
try:
|
||||
cropped_im = im.crop((int(w / 2), 0, w, h))
|
||||
except:
|
||||
except Exception:
|
||||
logger.exception("cropCover() error")
|
||||
return bytes()
|
||||
|
||||
@ -348,7 +349,7 @@ class IssueIdentifier:
|
||||
|
||||
return best_score_item
|
||||
|
||||
def search(self) -> List[IssueResult]:
|
||||
def search(self) -> list[IssueResult]:
|
||||
ca = self.comic_archive
|
||||
self.match_list = []
|
||||
self.cancel = False
|
||||
@ -524,7 +525,7 @@ class IssueIdentifier:
|
||||
hash_list,
|
||||
use_remote_alternates=False,
|
||||
)
|
||||
except:
|
||||
except Exception:
|
||||
self.match_list = []
|
||||
return self.match_list
|
||||
|
||||
@ -612,7 +613,7 @@ class IssueIdentifier:
|
||||
hash_list,
|
||||
use_remote_alternates=True,
|
||||
)
|
||||
except:
|
||||
except Exception:
|
||||
self.match_list = []
|
||||
return self.match_list
|
||||
self.log_msg(f"--->{score_item['score']}")
|
||||
|
@ -1,22 +1,21 @@
|
||||
"""A PyQT4 dialog to select specific issue from list"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
|
||||
@ -65,7 +64,7 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
)
|
||||
|
||||
self.series_id = series_id
|
||||
self.issue_id: Optional[int] = None
|
||||
self.issue_id: int | None = None
|
||||
self.settings = settings
|
||||
self.url_fetch_thread = None
|
||||
self.issue_list: list[CVIssuesResults] = []
|
||||
@ -75,7 +74,7 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
else:
|
||||
self.issue_number = issue_number
|
||||
|
||||
self.initial_id: Optional[int] = None
|
||||
self.initial_id: int | None = None
|
||||
self.perform_query()
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
@ -163,7 +162,7 @@ class IssueSelectionWindow(QtWidgets.QDialog):
|
||||
def cell_double_clicked(self, r: int, c: int) -> None:
|
||||
self.accept()
|
||||
|
||||
def current_item_changed(self, curr: Optional[QtCore.QModelIndex], prev: Optional[QtCore.QModelIndex]) -> None:
|
||||
def current_item_changed(self, curr: QtCore.QModelIndex | None, prev: QtCore.QModelIndex | None) -> None:
|
||||
|
||||
if curr is None:
|
||||
return
|
||||
|
@ -1,22 +1,21 @@
|
||||
"""A PyQT4 dialog to a text file or log"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Union
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets, uic
|
||||
|
||||
@ -40,7 +39,7 @@ class LogWindow(QtWidgets.QDialog):
|
||||
)
|
||||
)
|
||||
|
||||
def set_text(self, text: Union[str, bytes, None]) -> None:
|
||||
def set_text(self, text: str | bytes | None) -> None:
|
||||
try:
|
||||
if text is not None:
|
||||
if isinstance(text, bytes):
|
||||
|
@ -1,21 +1,21 @@
|
||||
"""A python app to (automatically) tag comic archives"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import pathlib
|
||||
@ -24,7 +24,6 @@ import signal
|
||||
import sys
|
||||
import traceback
|
||||
import types
|
||||
from typing import Optional
|
||||
|
||||
import pkg_resources
|
||||
|
||||
@ -68,7 +67,7 @@ try:
|
||||
self._exception_caught.connect(show_exception_box)
|
||||
|
||||
def exception_hook(
|
||||
self, exc_type: type[BaseException], exc_value: BaseException, exc_traceback: Optional[types.TracebackType]
|
||||
self, exc_type: type[BaseException], exc_value: BaseException, exc_traceback: types.TracebackType | None
|
||||
) -> None:
|
||||
"""Function handling uncaught exceptions.
|
||||
It is triggered each time an uncaught exception occurs.
|
||||
@ -167,7 +166,7 @@ def ctmain() -> None:
|
||||
if opts.no_gui:
|
||||
try:
|
||||
cli.cli_mode(opts, SETTINGS)
|
||||
except:
|
||||
except Exception:
|
||||
logger.exception("CLI mode failed")
|
||||
else:
|
||||
os.environ["QtWidgets.QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
|
||||
@ -186,12 +185,12 @@ def ctmain() -> None:
|
||||
import ctypes
|
||||
|
||||
myappid = "comictagger" # arbitrary string
|
||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
|
||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) # type: ignore[attr-defined]
|
||||
# force close of console window
|
||||
swp_hidewindow = 0x0080
|
||||
console_wnd = ctypes.windll.kernel32.GetConsoleWindow()
|
||||
console_wnd = ctypes.windll.kernel32.GetConsoleWindow() # type: ignore[attr-defined]
|
||||
if console_wnd != 0:
|
||||
ctypes.windll.user32.SetWindowPos(console_wnd, None, 0, 0, 0, 0, swp_hidewindow)
|
||||
ctypes.windll.user32.SetWindowPos(console_wnd, None, 0, 0, 0, 0, swp_hidewindow) # type: ignore[attr-defined]
|
||||
|
||||
if platform.system() != "Linux":
|
||||
img = QtGui.QPixmap(ComicTaggerSettings.get_graphic("tags.png"))
|
||||
|
@ -1,18 +1,19 @@
|
||||
"""A PyQT4 dialog to select from automated issue matches"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
@ -10,23 +10,23 @@ said_yes, checked = OptionalMessageDialog.question(self, "QtWidgets.Question",
|
||||
"Are you sure you wish to do this?",
|
||||
)
|
||||
"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Union
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
@ -71,7 +71,7 @@ class OptionalMessageDialog(QtWidgets.QDialog):
|
||||
|
||||
layout.addWidget(self.theCheckBox)
|
||||
|
||||
btnbox_style: Union[QtWidgets.QDialogButtonBox.StandardButtons, QtWidgets.QDialogButtonBox.StandardButton]
|
||||
btnbox_style: QtWidgets.QDialogButtonBox.StandardButtons | QtWidgets.QDialogButtonBox.StandardButton
|
||||
if style == StyleQuestion:
|
||||
btnbox_style = QtWidgets.QDialogButtonBox.StandardButton.Yes | QtWidgets.QDialogButtonBox.StandardButton.No
|
||||
else:
|
||||
|
@ -1,18 +1,19 @@
|
||||
"""CLI options class for ComicTagger app"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
@ -319,7 +320,7 @@ def launch_script(scriptfile: str, args: list[str]) -> None:
|
||||
|
||||
def parse_cmd_line() -> argparse.Namespace:
|
||||
|
||||
if platform.system() == "Darwin" and hasattr(sys, "frozen") and sys.frozen == 1:
|
||||
if platform.system() == "Darwin" and getattr(sys, "frozen", False):
|
||||
# remove the PSN (process serial number) argument from OS/X
|
||||
input_args = [a for a in sys.argv[1:] if "-psn_0_" not in a]
|
||||
else:
|
||||
|
@ -1,22 +1,22 @@
|
||||
"""A PyQT4 dialog to show pages of a comic archive"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import platform
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
|
||||
@ -48,7 +48,7 @@ class PageBrowserWindow(QtWidgets.QDialog):
|
||||
)
|
||||
)
|
||||
|
||||
self.comic_archive: Optional[ComicArchive] = None
|
||||
self.comic_archive: ComicArchive | None = None
|
||||
self.page_count = 0
|
||||
self.current_page_num = 0
|
||||
self.metadata = metadata
|
||||
|
@ -1,21 +1,21 @@
|
||||
"""A PyQt5 widget for editing the page list info"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
|
||||
@ -102,9 +102,9 @@ class PageListEditor(QtWidgets.QWidget):
|
||||
self.btnUp.clicked.connect(self.move_current_up)
|
||||
self.btnDown.clicked.connect(self.move_current_down)
|
||||
self.pre_move_row = -1
|
||||
self.first_front_page: Optional[int] = None
|
||||
self.first_front_page: int | None = None
|
||||
|
||||
self.comic_archive: Optional[ComicArchive] = None
|
||||
self.comic_archive: ComicArchive | None = None
|
||||
self.pages_list: list[ImageMetadata] = []
|
||||
|
||||
def reset_page(self) -> None:
|
||||
|
@ -1,18 +1,19 @@
|
||||
"""A PyQT4 class to load a page image from a ComicArchive in a background thread"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
|
@ -1,19 +1,19 @@
|
||||
"""A PyQt5 dialog to show ID log and progress"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
|
@ -1,22 +1,23 @@
|
||||
"""A PyQT4 dialog to confirm rename"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import List, TypedDict
|
||||
from typing import TypedDict
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets, uic
|
||||
|
||||
@ -39,7 +40,7 @@ class RenameWindow(QtWidgets.QDialog):
|
||||
def __init__(
|
||||
self,
|
||||
parent: QtWidgets.QWidget,
|
||||
comic_archive_list: List[ComicArchive],
|
||||
comic_archive_list: list[ComicArchive],
|
||||
data_style: int,
|
||||
settings: ComicTaggerSettings,
|
||||
) -> None:
|
||||
|
@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from typing_extensions import NotRequired, Required, TypedDict
|
||||
|
||||
from comicapi.comicarchive import ComicArchive
|
||||
@ -16,9 +14,9 @@ class IssueResult(TypedDict):
|
||||
issue_title: str
|
||||
issue_id: int # int?
|
||||
volume_id: int # int?
|
||||
month: Optional[int]
|
||||
year: Optional[int]
|
||||
publisher: Optional[str]
|
||||
month: int | None
|
||||
year: int | None
|
||||
publisher: str | None
|
||||
image_url: str
|
||||
thumb_url: str
|
||||
page_url: str
|
||||
@ -27,25 +25,25 @@ class IssueResult(TypedDict):
|
||||
|
||||
class OnlineMatchResults:
|
||||
def __init__(self) -> None:
|
||||
self.good_matches: List[str] = []
|
||||
self.no_matches: List[str] = []
|
||||
self.multiple_matches: List[MultipleMatch] = []
|
||||
self.low_confidence_matches: List[MultipleMatch] = []
|
||||
self.write_failures: List[str] = []
|
||||
self.fetch_data_failures: List[str] = []
|
||||
self.good_matches: list[str] = []
|
||||
self.no_matches: list[str] = []
|
||||
self.multiple_matches: list[MultipleMatch] = []
|
||||
self.low_confidence_matches: list[MultipleMatch] = []
|
||||
self.write_failures: list[str] = []
|
||||
self.fetch_data_failures: list[str] = []
|
||||
|
||||
|
||||
class MultipleMatch:
|
||||
def __init__(self, ca: ComicArchive, match_list: List[IssueResult]) -> None:
|
||||
def __init__(self, ca: ComicArchive, match_list: list[IssueResult]) -> None:
|
||||
self.ca: ComicArchive = ca
|
||||
self.matches: list[IssueResult] = match_list
|
||||
|
||||
|
||||
class SelectDetails(TypedDict):
|
||||
image_url: Optional[str]
|
||||
thumb_image_url: Optional[str]
|
||||
cover_date: Optional[str]
|
||||
site_detail_url: Optional[str]
|
||||
image_url: str | None
|
||||
thumb_image_url: str | None
|
||||
cover_date: str | None
|
||||
site_detail_url: str | None
|
||||
|
||||
|
||||
class CVResult(TypedDict):
|
||||
@ -55,14 +53,14 @@ class CVResult(TypedDict):
|
||||
number_of_page_results: int
|
||||
number_of_total_results: int
|
||||
status_code: int
|
||||
results: Union[
|
||||
CVIssuesResults,
|
||||
CVIssueDetailResults,
|
||||
CVVolumeResults,
|
||||
list[CVIssuesResults],
|
||||
list[CVVolumeResults],
|
||||
list[CVIssueDetailResults],
|
||||
]
|
||||
results: (
|
||||
CVIssuesResults
|
||||
| CVIssueDetailResults
|
||||
| CVVolumeResults
|
||||
| list[CVIssuesResults]
|
||||
| list[CVVolumeResults]
|
||||
| list[CVIssueDetailResults]
|
||||
)
|
||||
version: str
|
||||
|
||||
|
||||
|
@ -1,18 +1,19 @@
|
||||
"""Settings class for ComicTagger app"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
import logging
|
||||
@ -21,7 +22,7 @@ import pathlib
|
||||
import platform
|
||||
import sys
|
||||
import uuid
|
||||
from typing import Iterator, TextIO, Union, no_type_check
|
||||
from typing import Iterator, TextIO, no_type_check
|
||||
|
||||
from comicapi import utils
|
||||
|
||||
@ -29,7 +30,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ComicTaggerSettings:
|
||||
folder: Union[pathlib.Path, str] = ""
|
||||
folder: pathlib.Path | str = ""
|
||||
|
||||
@staticmethod
|
||||
def get_settings_folder() -> pathlib.Path:
|
||||
@ -42,20 +43,20 @@ class ComicTaggerSettings:
|
||||
|
||||
@staticmethod
|
||||
def base_dir() -> pathlib.Path:
|
||||
if getattr(sys, "frozen", None):
|
||||
return pathlib.Path(sys._MEIPASS)
|
||||
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
|
||||
return pathlib.Path(sys._MEIPASS) # type: ignore[attr-defined]
|
||||
|
||||
return pathlib.Path(__file__).parent
|
||||
|
||||
@staticmethod
|
||||
def get_graphic(filename: Union[str, pathlib.Path]) -> str:
|
||||
def get_graphic(filename: str | pathlib.Path) -> str:
|
||||
return str(ComicTaggerSettings.base_dir() / "graphics" / filename)
|
||||
|
||||
@staticmethod
|
||||
def get_ui_file(filename: Union[str, pathlib.Path]) -> pathlib.Path:
|
||||
def get_ui_file(filename: str | pathlib.Path) -> pathlib.Path:
|
||||
return ComicTaggerSettings.base_dir() / "ui" / filename
|
||||
|
||||
def __init__(self, folder: Union[str, pathlib.Path, None]) -> None:
|
||||
def __init__(self, folder: str | pathlib.Path | None) -> None:
|
||||
# General Settings
|
||||
self.rar_exe_path = ""
|
||||
self.allow_cbi_in_rar = True
|
||||
@ -168,6 +169,10 @@ class ComicTaggerSettings:
|
||||
# make sure rar program is now in the path for the rar class
|
||||
utils.add_to_path(os.path.dirname(self.rar_exe_path))
|
||||
|
||||
def reset(self):
|
||||
os.unlink(self.settings_file)
|
||||
self.__init__(ComicTaggerSettings.folder)
|
||||
|
||||
def load(self) -> None:
|
||||
def readline_generator(f: TextIO) -> Iterator[str]:
|
||||
line = f.readline()
|
||||
@ -175,7 +180,7 @@ class ComicTaggerSettings:
|
||||
yield line
|
||||
line = f.readline()
|
||||
|
||||
with open(self.settings_file, "r", encoding="utf-8") as f:
|
||||
with open(self.settings_file, encoding="utf-8") as f:
|
||||
self.config.read_file(readline_generator(f))
|
||||
|
||||
self.rar_exe_path = self.config.get("settings", "rar_exe_path")
|
||||
|
@ -1,23 +1,23 @@
|
||||
"""A PyQT4 dialog to enter app settings"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets, uic
|
||||
|
||||
@ -171,7 +171,7 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
|
||||
self.leRenameTemplate.setToolTip(template_tooltip)
|
||||
self.settings_to_form()
|
||||
self.rename_error: Optional[Exception] = None
|
||||
self.rename_error: Exception | None = None
|
||||
self.rename_test()
|
||||
|
||||
self.btnBrowseRar.clicked.connect(self.select_rar)
|
||||
@ -374,6 +374,6 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
|
||||
class TemplateHelpWindow(QtWidgets.QDialog):
|
||||
def __init__(self, parent: QtWidgets.QWidget) -> None:
|
||||
super(TemplateHelpWindow, self).__init__(parent)
|
||||
super().__init__(parent)
|
||||
|
||||
uic.loadUi(ComicTaggerSettings.get_ui_file("TemplateHelp.ui"), self)
|
||||
|
@ -1,18 +1,19 @@
|
||||
"""The main window of the ComicTagger app"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
@ -25,7 +26,7 @@ import pprint
|
||||
import re
|
||||
import sys
|
||||
import webbrowser
|
||||
from typing import Any, Callable, List, Optional, Union, cast
|
||||
from typing import Any, Callable, cast
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import natsort
|
||||
@ -75,8 +76,8 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self,
|
||||
file_list: list[str],
|
||||
settings: ComicTaggerSettings,
|
||||
parent: Optional[QtWidgets.QWidget] = None,
|
||||
opts: Optional[argparse.Namespace] = None,
|
||||
parent: QtWidgets.QWidget | None = None,
|
||||
opts: argparse.Namespace | None = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
@ -161,14 +162,14 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.statusBar()
|
||||
self.populate_combo_boxes()
|
||||
|
||||
self.page_browser: Optional[PageBrowserWindow] = None
|
||||
self.comic_archive: Optional[ComicArchive] = None
|
||||
self.page_browser: PageBrowserWindow | None = None
|
||||
self.comic_archive: ComicArchive | None = None
|
||||
self.dirty_flag = False
|
||||
self.droppedFile = None
|
||||
self.page_loader = None
|
||||
self.droppedFiles: list[str] = []
|
||||
self.metadata = GenericMetadata()
|
||||
self.atprogdialog: Optional[AutoTagProgressWindow] = None
|
||||
self.atprogdialog: AutoTagProgressWindow | None = None
|
||||
self.reset_app()
|
||||
|
||||
# set up some basic field validators
|
||||
@ -444,10 +445,13 @@ Have fun!
|
||||
EW = ExportWindow(
|
||||
self,
|
||||
self.settings,
|
||||
f"""You have selected {rar_count} archive(s) to export to Zip format. New archives will be created in the same folder as the original.
|
||||
(
|
||||
f"You have selected {rar_count} archive(s) to export to Zip format. "
|
||||
""" New archives will be created in the same folder as the original.
|
||||
|
||||
Please choose options below, and select OK.
|
||||
""",
|
||||
Please choose options below, and select OK.
|
||||
"""
|
||||
),
|
||||
)
|
||||
EW.adjustSize()
|
||||
EW.setModal(True)
|
||||
@ -537,7 +541,7 @@ Please choose options below, and select OK.
|
||||
license_name = "Apache License 2.0"
|
||||
|
||||
msg_box = QtWidgets.QMessageBox()
|
||||
msg_box.setWindowTitle(("About " + self.appName))
|
||||
msg_box.setWindowTitle("About " + self.appName)
|
||||
msg_box.setTextFormat(QtCore.Qt.TextFormat.RichText)
|
||||
msg_box.setIconPixmap(QtGui.QPixmap(ComicTaggerSettings.get_graphic("about.png")))
|
||||
msg_box.setText(
|
||||
@ -750,7 +754,7 @@ Please choose options below, and select OK.
|
||||
# Copy all of the metadata object into the form.
|
||||
# Merging of metadata should be done via the overlay function
|
||||
def metadata_to_form(self) -> None:
|
||||
def assign_text(field: Union[QtWidgets.QLineEdit, QtWidgets.QTextEdit], value: Any) -> None:
|
||||
def assign_text(field: QtWidgets.QLineEdit | QtWidgets.QTextEdit, value: Any) -> None:
|
||||
if value is not None:
|
||||
field.setText(str(value))
|
||||
|
||||
@ -784,7 +788,7 @@ Please choose options below, and select OK.
|
||||
|
||||
try:
|
||||
self.dsbCommunityRating.setValue(md.community_rating)
|
||||
except:
|
||||
except ValueError:
|
||||
self.dsbCommunityRating.setValue(0.0)
|
||||
|
||||
if md.format is not None and md.format != "":
|
||||
@ -1333,7 +1337,7 @@ Please choose options below, and select OK.
|
||||
try:
|
||||
result = urlparse(web_link)
|
||||
valid = all([result.scheme in ["http", "https"], result.netloc])
|
||||
except:
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if valid:
|
||||
@ -1720,7 +1724,7 @@ Please choose options below, and select OK.
|
||||
ii.set_cover_url_callback(self.atprogdialog.set_test_image)
|
||||
ii.set_name_length_delta_threshold(dlg.name_length_match_tolerance)
|
||||
|
||||
matches: List[IssueResult] = ii.search()
|
||||
matches: list[IssueResult] = ii.search()
|
||||
|
||||
result = ii.search_result
|
||||
|
||||
@ -1802,10 +1806,10 @@ Please choose options below, and select OK.
|
||||
atstartdlg = AutoTagStartWindow(
|
||||
self,
|
||||
self.settings,
|
||||
f"""You have selected {len(ca_list)} archive(s) to automatically identify and write {MetaDataStyle.name[style]} tags to.
|
||||
|
||||
Please choose options below, and select OK to Auto-Tag.
|
||||
""",
|
||||
(
|
||||
f"You have selected {len(ca_list)} archive(s) to automatically identify and write {MetaDataStyle.name[style]} tags to."
|
||||
"\n\nPlease choose options below, and select OK to Auto-Tag."
|
||||
),
|
||||
)
|
||||
|
||||
atstartdlg.adjustSize()
|
||||
@ -2059,7 +2063,7 @@ Please choose options below, and select OK to Auto-Tag.
|
||||
new_w = self.scrollArea.width() - scrollbar_w - 5
|
||||
self.scrollAreaWidgetContents.resize(new_w, self.scrollAreaWidgetContents.height())
|
||||
|
||||
def resizeEvent(self, ev: Optional[QtGui.QResizeEvent]) -> None:
|
||||
def resizeEvent(self, ev: QtGui.QResizeEvent | None) -> None:
|
||||
self.splitter_moved_event(0, 0)
|
||||
|
||||
def tab_changed(self, idx: int) -> None:
|
||||
|
@ -33,7 +33,7 @@
|
||||
</property>
|
||||
<property name="html">
|
||||
<string><html>
|
||||
<head>
|
||||
<head>
|
||||
<style>
|
||||
table {
|
||||
font-family: arial, sans-serif;
|
||||
@ -52,12 +52,12 @@ tr:nth-child(even) {
|
||||
}
|
||||
</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
|
||||
<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:
|
||||
<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>
|
||||
@ -118,7 +118,7 @@ Spider-Geddon 1 (2018)
|
||||
Spider-Geddon #1 - New Players; Check In
|
||||
|
||||
</pre>
|
||||
</body>
|
||||
</body>
|
||||
</html></string></property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextBrowserInteraction</set>
|
||||
|
@ -0,0 +1 @@
|
||||
from __future__ import annotations
|
@ -1,9 +1,10 @@
|
||||
"""Some utilities for the GUI"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import logging
|
||||
import traceback
|
||||
from typing import Optional
|
||||
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
|
||||
@ -79,7 +80,7 @@ if qt_available:
|
||||
img.load(ComicTaggerSettings.get_graphic("nocover.png"))
|
||||
return img
|
||||
|
||||
def qt_error(msg: str, e: Optional[Exception] = None) -> None:
|
||||
def qt_error(msg: str, e: Exception | None = None) -> None:
|
||||
trace = ""
|
||||
if e:
|
||||
trace = "\n".join(traceback.format_exception(type(e), e, e.__traceback__))
|
||||
|
@ -1,18 +1,19 @@
|
||||
"""Version checker"""
|
||||
|
||||
#
|
||||
# Copyright 2013 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import platform
|
||||
|
@ -1,21 +1,21 @@
|
||||
"""A PyQT4 dialog to select specific series/volume from list"""
|
||||
|
||||
#
|
||||
# Copyright 2012-2014 Anthony Beville
|
||||
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets, uic
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
@ -44,7 +44,7 @@ class SearchThread(QtCore.QThread):
|
||||
QtCore.QThread.__init__(self)
|
||||
self.series_name = series_name
|
||||
self.refresh: bool = refresh
|
||||
self.error_code: Optional[int] = None
|
||||
self.error_code: int | None = None
|
||||
self.cv_error = False
|
||||
self.cv_search_results: list[CVVolumeResults] = []
|
||||
|
||||
@ -95,7 +95,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
parent: QtWidgets.QWidget,
|
||||
series_name: str,
|
||||
issue_number: str,
|
||||
year: Optional[int],
|
||||
year: int | None,
|
||||
issue_count: int,
|
||||
cover_index_list: list[int],
|
||||
comic_archive: ComicArchive,
|
||||
@ -132,11 +132,11 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
self.immediate_autoselect = autoselect
|
||||
self.cover_index_list = cover_index_list
|
||||
self.cv_search_results: list[CVVolumeResults] = []
|
||||
self.ii: Optional[IssueIdentifier] = None
|
||||
self.iddialog: Optional[IDProgressWindow] = None
|
||||
self.id_thread: Optional[IdentifyThread] = None
|
||||
self.progdialog: Optional[QtWidgets.QProgressDialog] = None
|
||||
self.search_thread: Optional[SearchThread] = None
|
||||
self.ii: IssueIdentifier | None = None
|
||||
self.iddialog: IDProgressWindow | None = None
|
||||
self.id_thread: IdentifyThread | None = None
|
||||
self.progdialog: QtWidgets.QProgressDialog | None = None
|
||||
self.search_thread: SearchThread | None = None
|
||||
|
||||
self.use_filter = self.settings.always_use_publisher_filter
|
||||
|
||||
@ -355,8 +355,8 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
self.cv_search_results,
|
||||
)
|
||||
)
|
||||
except:
|
||||
logger.exception("bad data error filtering filter publishers")
|
||||
except Exception:
|
||||
logger.exception("bad data error filtering publishers")
|
||||
|
||||
# pre sort the data - so that we can put exact matches first afterwards
|
||||
# compare as str incase extra chars ie. '1976?'
|
||||
@ -369,14 +369,14 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
key=lambda i: (str(i["start_year"]), str(i["count_of_issues"])),
|
||||
reverse=True,
|
||||
)
|
||||
except:
|
||||
except Exception:
|
||||
logger.exception("bad data error sorting results by start_year,count_of_issues")
|
||||
else:
|
||||
try:
|
||||
self.cv_search_results = sorted(
|
||||
self.cv_search_results, key=lambda i: str(i["count_of_issues"]), reverse=True
|
||||
)
|
||||
except:
|
||||
except Exception:
|
||||
logger.exception("bad data error sorting results by count_of_issues")
|
||||
|
||||
# move sanitized matches to the front
|
||||
@ -390,7 +390,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
filter(lambda d: utils.sanitize_title(str(d["name"])) not in sanitized, self.cv_search_results)
|
||||
)
|
||||
self.cv_search_results = exact_matches + non_matches
|
||||
except:
|
||||
except Exception:
|
||||
logger.exception("bad data error filtering exact/near matches")
|
||||
|
||||
self.update_buttons()
|
||||
@ -454,7 +454,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
def cell_double_clicked(self, r: int, c: int) -> None:
|
||||
self.show_issues()
|
||||
|
||||
def current_item_changed(self, curr: Optional[QtCore.QModelIndex], prev: Optional[QtCore.QModelIndex]) -> None:
|
||||
def current_item_changed(self, curr: QtCore.QModelIndex | None, prev: QtCore.QModelIndex | None) -> None:
|
||||
|
||||
if curr is None:
|
||||
return
|
||||
|
@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
|
@ -63,4 +63,3 @@ diskimage:
|
||||
# move finished product to release folder
|
||||
mkdir -p $(TAGGER_BASE)/release
|
||||
mv $(DMG_FILE) $(TAGGER_BASE)/release
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
extend-exclude = "scripts/"
|
||||
force-exclude = "scripts"
|
||||
|
||||
[tool.isort]
|
||||
line_length = 120
|
||||
|
@ -1,10 +1,10 @@
|
||||
beautifulsoup4 >= 4.1
|
||||
natsort>=8.1.0
|
||||
pillow>=4.3.0
|
||||
requests==2.*
|
||||
pathvalidate
|
||||
pycountry
|
||||
pillow>=4.3.0
|
||||
py7zr
|
||||
pycountry
|
||||
requests==2.*
|
||||
text2digits
|
||||
typing_extensions
|
||||
wordninja
|
||||
|
@ -1,9 +1,9 @@
|
||||
pyinstaller>=4.10
|
||||
setuptools>=42
|
||||
setuptools_scm[toml]>=3.4
|
||||
wheel
|
||||
black>=22
|
||||
flake8==4.*
|
||||
flake8-encodings
|
||||
isort>=5.10
|
||||
pytest==7.*
|
||||
pyinstaller>=4.10
|
||||
pytest==7.*
|
||||
setuptools>=42
|
||||
setuptools_scm[toml]>=3.4
|
||||
wheel
|
||||
|
2
setup.py
2
setup.py
@ -6,6 +6,8 @@
|
||||
# It seems that post installation tweaks are broken by wheel files.
|
||||
# Kept here for further research
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import glob
|
||||
import os
|
||||
|
||||
|
@ -9,6 +9,8 @@ format is
|
||||
bool(xfail: expected failure on the old parser)
|
||||
)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
fnames = [
|
||||
(
|
||||
"batman 3 title (DC).cbz",
|
@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from comicapi import utils
|
||||
|
@ -1,12 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
from os.path import abspath, dirname, join
|
||||
from os.path import dirname, join
|
||||
|
||||
import pytest
|
||||
|
||||
from comicapi.comicarchive import ComicArchive, rar_support
|
||||
from comicapi.genericmetadata import GenericMetadata, md_test
|
||||
|
||||
thisdir = dirname(abspath(__file__))
|
||||
thisdir = dirname(__file__)
|
||||
|
||||
|
||||
@pytest.mark.xfail(not rar_support, reason="rar support")
|
||||
|
@ -1,7 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from filenames import fnames
|
||||
|
||||
import comicapi.filenameparser
|
||||
from testing.filenames import fnames
|
||||
|
||||
|
||||
@pytest.mark.parametrize("filename, reason, expected, xfail", fnames)
|
||||
|
@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
import comicapi.issuestring
|
||||
|
@ -1,10 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
|
||||
import pytest
|
||||
from filenames import rnames
|
||||
|
||||
from comicapi.genericmetadata import md_test
|
||||
from comictaggerlib.filerenamer import FileRenamer
|
||||
from testing.filenames import rnames
|
||||
|
||||
|
||||
@pytest.mark.parametrize("template, move, platform, expected", rnames)
|
||||
|
Loading…
Reference in New Issue
Block a user