Compare commits
23 Commits
1.3.0-alph
...
1.3.0
Author | SHA1 | Date | |
---|---|---|---|
628251c75b | |||
71499c3d7c | |||
03b8bf4671 | |||
773735bf6e | |||
b62e291749 | |||
a66b5ea0e3 | |||
615650f822 | |||
ed16199940 | |||
7005bd296e | |||
c6e1dc87dc | |||
ef37158e57 | |||
444e67100c | |||
82d054fd05 | |||
f82c024f8d | |||
da4daa6a8a | |||
6e1e8959c9 | |||
aedc5bedb4 | |||
93f5061c8f | |||
d46e171bd6 | |||
e7fe520660 | |||
91f288e8f4 | |||
d7bd3bb94b | |||
9e0b0ac01c |
63
.github/workflows/build.yaml
vendored
Normal file
63
.github/workflows/build.yaml
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+.[0-9]+*"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.9]
|
||||
os: [ubuntu-latest, macos-10.15, windows-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python3 -m pip install -r requirements_dev.txt
|
||||
python3 -m pip install -r requirements.txt
|
||||
for requirement in requirements-*.txt; do
|
||||
python3 -m pip install -r "$requirement"
|
||||
done
|
||||
shell: bash
|
||||
- name: Install Windows dependencies
|
||||
run: |
|
||||
choco install -y zip
|
||||
if: runner.os == 'Windows'
|
||||
- name: build
|
||||
run: |
|
||||
make pydist
|
||||
make dist
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
if: runner.os != 'Linux' # linux binary currently has a segfault when running on latest fedora
|
||||
with:
|
||||
name: "${{ format('ComicTagger-{0}', runner.os) }}"
|
||||
path: dist/*.zip
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: "${{ contains(github.ref, '-') }}" # alpha-releases should be 1.3.0-alpha.x full releases should be 1.3.0
|
||||
draft: true
|
||||
files: dist/*.zip
|
||||
- name: "Publish distribution 📦 to PyPI"
|
||||
if: startsWith(github.ref, 'refs/tags/') && runner.os == 'Linux'
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
with:
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
packages_dir: piprelease
|
167
.gitignore
vendored
167
.gitignore
vendored
@ -1,12 +1,157 @@
|
||||
/.idea/
|
||||
/nbproject/
|
||||
/dist
|
||||
*.pyc
|
||||
/.vscode
|
||||
venv
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
build/
|
||||
# generated by setuptools_scm
|
||||
ctversion.py
|
||||
.eggs
|
||||
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion
|
||||
|
||||
*.iml
|
||||
|
||||
## Directory-based project format:
|
||||
.idea/
|
||||
|
||||
### Other editors
|
||||
.*.swp
|
||||
nbproject/
|
||||
.vscode
|
||||
|
||||
comictaggerlib/_version.py
|
||||
*.exe
|
||||
*.zip
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
31
.travis.yml
31
.travis.yml
@ -1,30 +1,37 @@
|
||||
language: python
|
||||
# Only build tags
|
||||
if: type = pull_request OR tag IS present
|
||||
branches:
|
||||
only:
|
||||
- develop
|
||||
- /^\d+\.\d+\.\d+.*$/
|
||||
env:
|
||||
global:
|
||||
- PYTHON=python
|
||||
- PIP=pip
|
||||
- PYTHON=python3
|
||||
- PIP=pip3
|
||||
- SETUPTOOLS_SCM_PRETEND_VERSION=$TRAVIS_TAG
|
||||
- MAKE=make
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
- os: osx
|
||||
language: generic
|
||||
osx_image: xcode8.3
|
||||
env: PYTHON=python3 PIP=pip3 MACOSX_DEPLOYMENT_TARGET=10.11
|
||||
python: 3.8
|
||||
- name: "Python: 3.7"
|
||||
os: osx
|
||||
language: shell
|
||||
python: 3.7
|
||||
env: PYTHON=python3 PIP="python3 -m pip"
|
||||
cache:
|
||||
- directories:
|
||||
- $HOME/Library/Caches/pip
|
||||
- os: windows
|
||||
language: bash
|
||||
env: PATH=/C/Python37:/C/Python37/Scripts:$PATH MAKE=mingw32-make
|
||||
env: PATH=/C/Python37:/C/Python37/Scripts:$PATH MAKE=mingw32-make PIP=pip PYTHON=python
|
||||
before_install:
|
||||
- if [ "$TRAVIS_OS_NAME" = "windows" ]; then choco install -y python mingw zip; fi
|
||||
- if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew upgrade python3 ; fi
|
||||
- if [ "$TRAVIS_OS_NAME" = "windows" ]; then choco install -y python --version 3.7.9; choco install -y mingw zip; fi
|
||||
install:
|
||||
- $PIP install --upgrade setuptools
|
||||
- $PIP install -r requirements.txt
|
||||
- $PIP install -r requirements_dev.txt
|
||||
- $PIP install -r requirements-GUI.txt
|
||||
- $PIP install -r requirements-CBR.txt
|
||||
script:
|
||||
- if [ "$TRAVIS_OS_NAME" != "linux" ]; then $MAKE dist ; fi
|
||||
|
||||
@ -49,4 +56,4 @@ deploy:
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
condition: $TRAVIS_OS_NAME = "linux"
|
||||
condition: $TRAVIS_OS_NAME = "linux"
|
||||
|
15
Makefile
15
Makefile
@ -1,5 +1,6 @@
|
||||
PIP ?= pip
|
||||
VERSION_STR := $(shell python setup.py --version)
|
||||
PIP ?= pip3
|
||||
PYTHON ?= python3
|
||||
VERSION_STR := $(shell $(PYTHON) setup.py --version)
|
||||
|
||||
ifeq ($(OS),Windows_NT)
|
||||
OS_VERSION=win-$(PROCESSOR_ARCHITECTURE)
|
||||
@ -28,19 +29,19 @@ clean:
|
||||
$(MAKE) -C mac clean
|
||||
rm -rf build
|
||||
rm -rf comictaggerlib/ui/__pycache__
|
||||
rm comitaggerlib/ctversion.py
|
||||
rm comictaggerlib/ctversion.py
|
||||
|
||||
pydist:
|
||||
make clean
|
||||
mkdir -p piprelease
|
||||
rm -f comictagger-$(VERSION_STR).zip
|
||||
python setup.py sdist --formats=zip #,gztar
|
||||
mv dist/comictagger-$(VERSION_STR).zip piprelease
|
||||
$(PYTHON) setup.py sdist --formats=gztar
|
||||
mv dist/comictagger-$(VERSION_STR).tar.gz piprelease
|
||||
rm -rf comictagger.egg-info dist
|
||||
|
||||
upload:
|
||||
python setup.py register
|
||||
python setup.py sdist --formats=zip upload
|
||||
$(PYTHON) setup.py register
|
||||
$(PYTHON) setup.py sdist --formats=gztar upload
|
||||
|
||||
dist:
|
||||
$(PIP) install .
|
||||
|
12
README.md
12
README.md
@ -37,12 +37,14 @@ Just unzip the archive in any folder and run, no additional installation steps a
|
||||
A pip package is provided, you can install it with:
|
||||
|
||||
```
|
||||
$ pip install comictagger
|
||||
$ pip3 install comictagger[GUI]
|
||||
```
|
||||
|
||||
### From source
|
||||
|
||||
1. ensure you have a recent version of python3 and setuptools installed
|
||||
2. clone this repository `git clone https://github.com/comictagger/comictagger.git`
|
||||
3. `pip install -r requirements.txt`
|
||||
4. `python comictagger.py`
|
||||
1. Ensure you have a recent version of python3 installed
|
||||
2. Clone this repository `git clone https://github.com/comictagger/comictagger.git`
|
||||
3. `pip3 install -r requirements_dev.txt`
|
||||
4. Optionally install the GUI `pip3 install -r requirements-GUI.txt`
|
||||
5. Optionally install CBR support `pip3 install -r requirements-CBR.txt`
|
||||
6. `python3 comictagger.py`
|
||||
|
@ -24,9 +24,12 @@ import platform
|
||||
import time
|
||||
import io
|
||||
|
||||
from natsort import natsorted
|
||||
from PyPDF2 import PdfFileReader
|
||||
from unrar.cffi import rarfile
|
||||
import natsort
|
||||
try:
|
||||
from unrar.cffi import rarfile
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
import Image
|
||||
pil_available = True
|
||||
@ -524,34 +527,6 @@ class UnknownArchiver:
|
||||
def getArchiveFilenameList(self):
|
||||
return []
|
||||
|
||||
class PdfArchiver:
|
||||
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
|
||||
def getArchiveComment(self):
|
||||
return ""
|
||||
|
||||
def setArchiveComment(self, comment):
|
||||
return False
|
||||
|
||||
def readArchiveFile(self, page_num):
|
||||
return subprocess.check_output(
|
||||
['mudraw', '-o', '-', self.path, str(int(os.path.basename(page_num)[:-4]))])
|
||||
|
||||
def writeArchiveFile(self, archive_file, data):
|
||||
return False
|
||||
|
||||
def removeArchiveFile(self, archive_file):
|
||||
return False
|
||||
|
||||
def getArchiveFilenameList(self):
|
||||
out = []
|
||||
pdf = PdfFileReader(open(self.path, 'rb'))
|
||||
for page in range(1, pdf.getNumPages() + 1):
|
||||
out.append("/%04d.jpg" % (page))
|
||||
return out
|
||||
|
||||
class ComicArchive:
|
||||
logo_data = None
|
||||
class ArchiveType:
|
||||
@ -592,9 +567,6 @@ class ComicArchive:
|
||||
self.archiver = RarArchiver(
|
||||
self.path,
|
||||
rar_exe_path=self.rar_exe_path)
|
||||
elif os.path.basename(self.path)[-3:] == 'pdf':
|
||||
self.archive_type = self.ArchiveType.Pdf
|
||||
self.archiver = PdfArchiver(self.path)
|
||||
|
||||
if ComicArchive.logo_data is None:
|
||||
#fname = ComicTaggerSettings.getGraphic('nocover.png')
|
||||
@ -627,7 +599,10 @@ class ComicArchive:
|
||||
return zipfile.is_zipfile(self.path)
|
||||
|
||||
def rarTest(self):
|
||||
return rarfile.is_rarfile(self.path)
|
||||
try:
|
||||
return rarfile.is_rarfile(self.path)
|
||||
except:
|
||||
return False
|
||||
|
||||
def isZip(self):
|
||||
return self.archive_type == self.ArchiveType.Zip
|
||||
@ -670,7 +645,7 @@ class ComicArchive:
|
||||
|
||||
if (
|
||||
# or self.isFolder() )
|
||||
(self.isZip() or self.isRar() or self.isPdf())
|
||||
(self.isZip() or self.isRar())
|
||||
and
|
||||
(self.getNumberOfPages() > 0)
|
||||
|
||||
@ -809,13 +784,9 @@ class ComicArchive:
|
||||
# about case-sensitivity!
|
||||
if sort_list:
|
||||
def keyfunc(k):
|
||||
# hack to account for some weird scanner ID pages
|
||||
# basename=os.path.split(k)[1]
|
||||
# if basename < '0':
|
||||
# k = os.path.join(os.path.split(k)[0], "z" + basename)
|
||||
return k.lower()
|
||||
|
||||
files = natsorted(files, key=keyfunc, signed=False)
|
||||
files = natsort.natsorted(files, alg=natsort.ns.IC | natsort.ns.I)
|
||||
|
||||
# make a sub-list of image files
|
||||
self.page_list = []
|
||||
@ -921,7 +892,10 @@ class ComicArchive:
|
||||
def writeCIX(self, metadata):
|
||||
if metadata is not None:
|
||||
self.applyArchiveInfoToMetadata(metadata, calc_page_sizes=True)
|
||||
cix_string = ComicInfoXml().stringFromMetadata(metadata)
|
||||
rawCIX = self.readRawCIX()
|
||||
if rawCIX == "":
|
||||
rawCIX = None
|
||||
cix_string = ComicInfoXml().stringFromMetadata(metadata, xml=rawCIX)
|
||||
write_success = self.archiver.writeArchiveFile(
|
||||
self.ci_xml_filename,
|
||||
cix_string)
|
||||
|
@ -50,11 +50,11 @@ class ComicInfoXml:
|
||||
tree = ET.ElementTree(ET.fromstring(string))
|
||||
return self.convertXMLToMetadata(tree)
|
||||
|
||||
def stringFromMetadata(self, metadata):
|
||||
def stringFromMetadata(self, metadata, xml=None):
|
||||
|
||||
header = '<?xml version="1.0"?>\n'
|
||||
|
||||
tree = self.convertMetadataToXML(self, metadata)
|
||||
tree = self.convertMetadataToXML(self, metadata, xml)
|
||||
tree_str = ET.tostring(tree.getroot()).decode()
|
||||
return header + tree_str
|
||||
|
||||
@ -74,20 +74,28 @@ class ComicInfoXml:
|
||||
if level and (not elem.tail or not elem.tail.strip()):
|
||||
elem.tail = i
|
||||
|
||||
def convertMetadataToXML(self, filename, metadata):
|
||||
def convertMetadataToXML(self, filename, metadata, xml=None):
|
||||
|
||||
# shorthand for the metadata
|
||||
md = metadata
|
||||
|
||||
# build a tree structure
|
||||
root = ET.Element("ComicInfo")
|
||||
root.attrib['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
root.attrib['xmlns:xsd'] = "http://www.w3.org/2001/XMLSchema"
|
||||
if xml:
|
||||
root = ET.ElementTree(ET.fromstring(xml)).getroot()
|
||||
else:
|
||||
# build a tree structure
|
||||
root = ET.Element("ComicInfo")
|
||||
root.attrib['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance"
|
||||
root.attrib['xmlns:xsd'] = "http://www.w3.org/2001/XMLSchema"
|
||||
# helper func
|
||||
|
||||
def assign(cix_entry, md_entry):
|
||||
if md_entry is not None:
|
||||
ET.SubElement(root, cix_entry).text = "{0}".format(md_entry)
|
||||
print(cix_entry, md_entry)
|
||||
et_entry = root.find(cix_entry)
|
||||
if et_entry is not None:
|
||||
et_entry.text = "{0}".format(md_entry)
|
||||
else:
|
||||
ET.SubElement(root, cix_entry).text = "{0}".format(md_entry)
|
||||
|
||||
assign('Title', md.title)
|
||||
assign('Series', md.series)
|
||||
@ -141,33 +149,19 @@ class ComicInfoXml:
|
||||
credit_editor_list.append(credit['person'].replace(",", ""))
|
||||
|
||||
# second, convert each list to string, and add to XML struct
|
||||
if len(credit_writer_list) > 0:
|
||||
node = ET.SubElement(root, 'Writer')
|
||||
node.text = utils.listToString(credit_writer_list)
|
||||
assign('Writer', utils.listToString(credit_writer_list))
|
||||
|
||||
if len(credit_penciller_list) > 0:
|
||||
node = ET.SubElement(root, 'Penciller')
|
||||
node.text = utils.listToString(credit_penciller_list)
|
||||
assign('Penciller', utils.listToString(credit_penciller_list))
|
||||
|
||||
if len(credit_inker_list) > 0:
|
||||
node = ET.SubElement(root, 'Inker')
|
||||
node.text = utils.listToString(credit_inker_list)
|
||||
assign('Inker', utils.listToString(credit_inker_list))
|
||||
|
||||
if len(credit_colorist_list) > 0:
|
||||
node = ET.SubElement(root, 'Colorist')
|
||||
node.text = utils.listToString(credit_colorist_list)
|
||||
assign('Colorist', utils.listToString(credit_colorist_list))
|
||||
|
||||
if len(credit_letterer_list) > 0:
|
||||
node = ET.SubElement(root, 'Letterer')
|
||||
node.text = utils.listToString(credit_letterer_list)
|
||||
assign('Letterer', utils.listToString(credit_letterer_list))
|
||||
|
||||
if len(credit_cover_list) > 0:
|
||||
node = ET.SubElement(root, 'CoverArtist')
|
||||
node.text = utils.listToString(credit_cover_list)
|
||||
assign('CoverArtist', utils.listToString(credit_cover_list))
|
||||
|
||||
if len(credit_editor_list) > 0:
|
||||
node = ET.SubElement(root, 'Editor')
|
||||
node.text = utils.listToString(credit_editor_list)
|
||||
assign('Editor', utils.listToString(credit_editor_list))
|
||||
|
||||
assign('Publisher', md.publisher)
|
||||
assign('Imprint', md.imprint)
|
||||
@ -178,7 +172,7 @@ class ComicInfoXml:
|
||||
assign('Format', md.format)
|
||||
assign('AgeRating', md.maturityRating)
|
||||
if md.blackAndWhite is not None and md.blackAndWhite:
|
||||
ET.SubElement(root, 'BlackAndWhite').text = "Yes"
|
||||
assign('BlackAndWhite', "Yes")
|
||||
assign('Manga', md.manga)
|
||||
assign('Characters', md.characters)
|
||||
assign('Teams', md.teams)
|
||||
@ -186,11 +180,15 @@ class ComicInfoXml:
|
||||
assign('ScanInformation', md.scanInfo)
|
||||
|
||||
# loop and add the page entries under pages node
|
||||
if len(md.pages) > 0:
|
||||
pages_node = root.find('Pages')
|
||||
if pages_node is not None:
|
||||
pages_node.clear()
|
||||
else:
|
||||
pages_node = ET.SubElement(root, 'Pages')
|
||||
for page_dict in md.pages:
|
||||
page_node = ET.SubElement(pages_node, 'Page')
|
||||
page_node.attrib = page_dict
|
||||
|
||||
for page_dict in md.pages:
|
||||
page_node = ET.SubElement(pages_node, 'Page')
|
||||
page_node.attrib = page_dict
|
||||
|
||||
# self pretty-print
|
||||
self.indent(root)
|
||||
@ -276,9 +274,9 @@ class ComicInfoXml:
|
||||
|
||||
return md
|
||||
|
||||
def writeToExternalFile(self, filename, metadata):
|
||||
def writeToExternalFile(self, filename, metadata, xml=None):
|
||||
|
||||
tree = self.convertMetadataToXML(self, metadata)
|
||||
tree = self.convertMetadataToXML(self, metadata, xml)
|
||||
# ET.dump(tree)
|
||||
tree.write(filename, encoding='utf-8')
|
||||
|
||||
|
@ -38,8 +38,7 @@ class IssueString:
|
||||
if text is None:
|
||||
return
|
||||
|
||||
if isinstance(text, int):
|
||||
text = str(text)
|
||||
text = str(text)
|
||||
|
||||
if len(text) == 0:
|
||||
return
|
||||
|
@ -21,6 +21,7 @@ import re
|
||||
import platform
|
||||
import locale
|
||||
import codecs
|
||||
import unicodedata
|
||||
|
||||
|
||||
class UtilsVars:
|
||||
@ -151,6 +152,21 @@ def removearticles(text):
|
||||
return newText
|
||||
|
||||
|
||||
def sanitize_title(text):
|
||||
# normalize unicode and convert to ascii. Does not work for everything eg ½ to 1⁄2 not 1/2
|
||||
# this will probably cause issues with titles in other character sets e.g. chinese, japanese
|
||||
text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode('ascii')
|
||||
# comicvine keeps apostrophes a part of the word
|
||||
text = text.replace("'", "")
|
||||
text = text.replace("\"", "")
|
||||
# comicvine ignores punctuation and accents
|
||||
text = re.sub(r'[^A-Za-z0-9]+',' ', text)
|
||||
# remove extra space and articles and all lower case
|
||||
text = removearticles(text).lower().strip()
|
||||
|
||||
return text
|
||||
|
||||
|
||||
def unique_file(file_name):
|
||||
counter = 1
|
||||
# returns ('/path/file', '.ext')
|
||||
|
@ -9,13 +9,6 @@ binaries = []
|
||||
block_cipher = None
|
||||
|
||||
if platform.system() == "Windows":
|
||||
from site import getsitepackages
|
||||
sitepackages = getsitepackages()[1]
|
||||
# add ssl qt libraries not discovered automatically
|
||||
binaries.extend([
|
||||
(join(sitepackages, "PyQt5/Qt/bin/libeay32.dll"), "./PyQt5/Qt/bin"),
|
||||
(join(sitepackages, "PyQt5/Qt/bin/ssleay32.dll"), "./PyQt5/Qt/bin")
|
||||
])
|
||||
enable_console = True
|
||||
|
||||
a = Analysis(['comictagger.py'],
|
||||
@ -54,4 +47,4 @@ app = BUNDLE(exe,
|
||||
'CFBundleShortVersionString': ctversion.version,
|
||||
'CFBundleVersion': ctversion.version
|
||||
},
|
||||
bundle_identifier=None)
|
||||
bundle_identifier=None)
|
||||
|
@ -21,7 +21,6 @@ import time
|
||||
import datetime
|
||||
import sys
|
||||
import ssl
|
||||
import unicodedata
|
||||
#from pprint import pprint
|
||||
#import math
|
||||
|
||||
@ -204,13 +203,8 @@ class ComicVineTalker(QObject):
|
||||
|
||||
def searchForSeries(self, series_name, callback=None, refresh_cache=False):
|
||||
|
||||
# normalize unicode and convert to ascii. Does not work for everything eg ½ to 1⁄2 not 1/2
|
||||
search_series_name = unicodedata.normalize('NFKD', series_name).encode('ascii', 'ignore').decode('ascii')
|
||||
# comicvine ignores punctuation and accents
|
||||
search_series_name = re.sub(r'[^A-Za-z0-9]+',' ', search_series_name)
|
||||
# remove extra space and articles and all lower case
|
||||
search_series_name = utils.removearticles(search_series_name).lower().strip()
|
||||
|
||||
# Sanitize the series name for comicvine searching, comicvine search ignore symbols
|
||||
search_series_name = utils.sanitize_title(series_name)
|
||||
|
||||
# before we search online, look in our cache, since we might have
|
||||
# done this same search recently
|
||||
@ -227,7 +221,8 @@ class ComicVineTalker(QObject):
|
||||
'resources': 'volume',
|
||||
'query': search_series_name,
|
||||
'field_list': 'volume,name,id,start_year,publisher,image,description,count_of_issues',
|
||||
'page': 1
|
||||
'page': 1,
|
||||
'limit': 100
|
||||
}
|
||||
|
||||
cv_response = self.getCVContent(self.api_base_url + "/search", params)
|
||||
@ -270,12 +265,8 @@ class ComicVineTalker(QObject):
|
||||
|
||||
last_result = search_results[-1]['name']
|
||||
|
||||
# normalize unicode and convert to ascii. Does not work for everything eg ½ to 1⁄2 not 1/2
|
||||
last_result = unicodedata.normalize('NFKD', last_result).encode('ascii', 'ignore').decode('ascii')
|
||||
# comicvine ignores punctuation and accents
|
||||
last_result = re.sub(r'[^A-Za-z0-9]+',' ', last_result)
|
||||
# remove extra space and articles and all lower case
|
||||
last_result = utils.removearticles(last_result).lower().strip()
|
||||
# Sanitize the series name for comicvine searching, comicvine search ignore symbols
|
||||
last_result = utils.sanitize_title(last_result)
|
||||
|
||||
# See if the last result's name has all the of the search terms.
|
||||
# if not, break out of this, loop, we're done.
|
||||
@ -314,13 +305,9 @@ class ComicVineTalker(QObject):
|
||||
# (iterate backwards for easy removal)
|
||||
for i in range(len(search_results) - 1, -1, -1):
|
||||
record = search_results[i]
|
||||
# Sanitize the series name for comicvine searching, comicvine search ignore symbols
|
||||
recordName = utils.sanitize_title(record['name'])
|
||||
for term in search_series_name.split():
|
||||
# normalize unicode and convert to ascii. Does not work for everything eg ½ to 1⁄2 not 1/2
|
||||
recordName = unicodedata.normalize('NFKD', record['name']).encode('ascii', 'ignore').decode('ascii')
|
||||
# comicvine ignores punctuation and accents
|
||||
recordName = re.sub(r'[^A-Za-z0-9]+',' ', recordName)
|
||||
# remove extra space and articles and all lower case
|
||||
recordName = utils.removearticles(recordName).lower().strip()
|
||||
|
||||
if term not in recordName:
|
||||
del search_results[i]
|
||||
|
@ -320,7 +320,7 @@ class CoverImageWidget(QWidget):
|
||||
img_w = scaled_pixmap.width()
|
||||
img_h = scaled_pixmap.height()
|
||||
self.lblImage.resize(img_w, img_h)
|
||||
self.lblImage.move((frame_w - img_w) / 2, (frame_h - img_h) / 2)
|
||||
self.lblImage.move(int((frame_w - img_w) / 2), int((frame_h - img_h) / 2))
|
||||
|
||||
def showPopup(self):
|
||||
self.popup = ImagePopup(self, self.current_pixmap)
|
||||
|
@ -20,7 +20,6 @@ from functools import reduce
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
from PIL import WebPImagePlugin
|
||||
pil_available = True
|
||||
except ImportError:
|
||||
pil_available = False
|
||||
|
@ -87,7 +87,7 @@ class ImagePopup(QtWidgets.QDialog):
|
||||
img_w = display_pixmap.width()
|
||||
img_h = display_pixmap.height()
|
||||
self.lblImage.resize(img_w, img_h)
|
||||
self.lblImage.move((win_w - img_w) / 2, (win_h - img_h) / 2)
|
||||
self.lblImage.move(int((win_w - img_w) / 2), int((win_h - img_h) / 2))
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
self.close()
|
||||
|
@ -19,7 +19,6 @@ import io
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
from PIL import WebPImagePlugin
|
||||
pil_available = True
|
||||
except ImportError:
|
||||
pil_available = False
|
||||
@ -76,8 +75,8 @@ class IssueIdentifier:
|
||||
self.length_delta_thresh = settings.id_length_delta_thresh
|
||||
|
||||
# used to eliminate unlikely publishers
|
||||
self.publisher_blacklist = [
|
||||
s.strip().lower() for s in settings.id_publisher_blacklist.split(',')]
|
||||
self.publisher_filter = [
|
||||
s.strip().lower() for s in settings.id_publisher_filter.split(',')]
|
||||
|
||||
self.additional_metadata = GenericMetadata()
|
||||
self.output_function = IssueIdentifier.defaultWriteOutput
|
||||
@ -100,8 +99,8 @@ class IssueIdentifier:
|
||||
def setNameLengthDeltaThreshold(self, delta):
|
||||
self.length_delta_thresh = delta
|
||||
|
||||
def setPublisherBlackList(self, blacklist):
|
||||
self.publisher_blacklist = blacklist
|
||||
def setPublisherFilter(self, filter):
|
||||
self.publisher_filter = filter
|
||||
|
||||
def setHasherAlgorithm(self, algo):
|
||||
self.image_hasher = algo
|
||||
@ -231,9 +230,9 @@ class IssueIdentifier:
|
||||
sys.stdout.flush()
|
||||
|
||||
def log_msg(self, msg, newline=True):
|
||||
self.output_function(msg)
|
||||
if newline:
|
||||
self.output_function("\n")
|
||||
msg += "\n"
|
||||
self.output_function(msg)
|
||||
|
||||
def getIssueCoverMatchScore(
|
||||
self,
|
||||
@ -394,7 +393,7 @@ class IssueIdentifier:
|
||||
if keys['month'] is not None:
|
||||
self.log_msg("\tMonth: " + str(keys['month']))
|
||||
|
||||
#self.log_msg("Publisher Blacklist: " + str(self.publisher_blacklist))
|
||||
#self.log_msg("Publisher Filter: " + str(self.publisher_filter))
|
||||
comicVine = ComicVineTalker()
|
||||
comicVine.wait_for_rate_limit = self.waitAndRetryOnRateLimit
|
||||
|
||||
@ -434,17 +433,19 @@ class IssueIdentifier:
|
||||
|
||||
# assume that our search name is close to the actual name, say
|
||||
# within ,e.g. 5 chars
|
||||
shortened_key = utils.removearticles(keys['series'])
|
||||
shortened_item_name = utils.removearticles(item['name'])
|
||||
# sanitize both the search string and the result so that
|
||||
# we are comparing the same type of data
|
||||
shortened_key = utils.sanitize_title(keys['series'])
|
||||
shortened_item_name = utils.sanitize_title(item['name'])
|
||||
if len(shortened_item_name) < (
|
||||
len(shortened_key) + self.length_delta_thresh):
|
||||
length_approved = True
|
||||
|
||||
# remove any series from publishers on the blacklist
|
||||
# remove any series from publishers on the filter
|
||||
if item['publisher'] is not None:
|
||||
publisher = item['publisher']['name']
|
||||
if publisher is not None and publisher.lower(
|
||||
) in self.publisher_blacklist:
|
||||
) in self.publisher_filter:
|
||||
publisher_approved = False
|
||||
|
||||
if length_approved and publisher_approved and date_approved:
|
||||
|
@ -15,6 +15,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
#import os
|
||||
from operator import itemgetter, attrgetter
|
||||
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtGui import *
|
||||
@ -126,25 +127,75 @@ class PageListEditor(QWidget):
|
||||
self.comic_archive = None
|
||||
self.pages_list = None
|
||||
|
||||
def getNewIndexes(self, movement):
|
||||
selection = self.listWidget.selectionModel().selectedRows()
|
||||
selection.sort(reverse=movement>0)
|
||||
current = 0
|
||||
newindexes = []
|
||||
oldindexes = []
|
||||
for x in selection:
|
||||
current = x.row()
|
||||
oldindexes.append(current)
|
||||
if current + movement >= 0 and current + movement <= self.listWidget.count()-1:
|
||||
if len(newindexes) < 1 or current + movement != newindexes[-1]:
|
||||
current += movement
|
||||
else:
|
||||
prev = current
|
||||
newindexes.append(current)
|
||||
oldindexes.sort()
|
||||
newindexes.sort()
|
||||
return list(zip(newindexes, oldindexes))
|
||||
|
||||
def SetSelection(self, indexes):
|
||||
selectionRanges = []
|
||||
first = 0
|
||||
for i, selection in enumerate(indexes):
|
||||
if i == 0:
|
||||
first = selection[0]
|
||||
continue
|
||||
|
||||
if selection != indexes[i-1][0]+1:
|
||||
selectionRanges.append((first,indexes[i-1][0]))
|
||||
first = selection[0]
|
||||
|
||||
selectionRanges.append((first, indexes[-1][0]))
|
||||
selection = QItemSelection()
|
||||
for x in selectionRanges:
|
||||
selection.merge(QItemSelection(self.listWidget.model().index(x[0], 0), self.listWidget.model().index(x[1], 0)), QItemSelectionModel.Select)
|
||||
|
||||
self.listWidget.selectionModel().select(selection, QItemSelectionModel.ClearAndSelect)
|
||||
return selectionRanges
|
||||
|
||||
|
||||
def moveCurrentUp(self):
|
||||
row = self.listWidget.currentRow()
|
||||
selection = self.getNewIndexes(-1)
|
||||
for sel in selection:
|
||||
item = self.listWidget.takeItem(sel[1])
|
||||
self.listWidget.insertItem(sel[0], item)
|
||||
|
||||
if row > 0:
|
||||
item = self.listWidget.takeItem(row)
|
||||
self.listWidget.insertItem(row - 1, item)
|
||||
self.listWidget.setCurrentRow(row - 1)
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
self.SetSelection(selection)
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
|
||||
|
||||
def moveCurrentDown(self):
|
||||
row = self.listWidget.currentRow()
|
||||
selection = self.getNewIndexes(1)
|
||||
selection.sort(reverse=True)
|
||||
for sel in selection:
|
||||
item = self.listWidget.takeItem(sel[1])
|
||||
self.listWidget.insertItem(sel[0], item)
|
||||
|
||||
if row < self.listWidget.count() - 1:
|
||||
item = self.listWidget.takeItem(row)
|
||||
self.listWidget.insertItem(row + 1, item)
|
||||
self.listWidget.setCurrentRow(row + 1)
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.modified.emit()
|
||||
self.listOrderChanged.emit()
|
||||
self.emitFrontCoverChange()
|
||||
self.SetSelection(selection)
|
||||
self.modified.emit()
|
||||
|
||||
def itemMoveEvent(self, s):
|
||||
# print "move event: ", s, self.listWidget.currentRow()
|
||||
|
@ -78,8 +78,8 @@ class ComicTaggerSettings:
|
||||
|
||||
# identifier settings
|
||||
self.id_length_delta_thresh = 5
|
||||
self.id_publisher_blacklist = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa, Dino Comics"
|
||||
|
||||
self.id_publisher_filter = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa, Dino Comics"
|
||||
|
||||
# Show/ask dialog flags
|
||||
self.ask_about_cbi_in_rar = True
|
||||
self.show_disclaimer = True
|
||||
@ -95,6 +95,10 @@ class ComicTaggerSettings:
|
||||
self.remove_html_tables = False
|
||||
self.cv_api_key = ""
|
||||
|
||||
self.sort_series_by_year = True
|
||||
self.exact_series_matches_first = True
|
||||
self.always_use_publisher_filter = False
|
||||
|
||||
# CBL Tranform settings
|
||||
|
||||
self.assume_lone_credit_is_primary = False
|
||||
@ -222,9 +226,9 @@ class ComicTaggerSettings:
|
||||
if self.config.has_option('identifier', 'id_length_delta_thresh'):
|
||||
self.id_length_delta_thresh = self.config.getint(
|
||||
'identifier', 'id_length_delta_thresh')
|
||||
if self.config.has_option('identifier', 'id_publisher_blacklist'):
|
||||
self.id_publisher_blacklist = self.config.get(
|
||||
'identifier', 'id_publisher_blacklist')
|
||||
if self.config.has_option('identifier', 'id_publisher_filter'):
|
||||
self.id_publisher_filter = self.config.get(
|
||||
'identifier', 'id_publisher_filter')
|
||||
|
||||
if self.config.has_option('filenameparser', 'parse_scan_info'):
|
||||
self.parse_scan_info = self.config.getboolean(
|
||||
@ -254,6 +258,17 @@ class ComicTaggerSettings:
|
||||
if self.config.has_option('comicvine', 'remove_html_tables'):
|
||||
self.remove_html_tables = self.config.getboolean(
|
||||
'comicvine', 'remove_html_tables')
|
||||
|
||||
if self.config.has_option('comicvine', 'sort_series_by_year'):
|
||||
self.sort_series_by_year = self.config.getboolean(
|
||||
'comicvine', 'sort_series_by_year')
|
||||
if self.config.has_option('comicvine', 'exact_series_matches_first'):
|
||||
self.exact_series_matches_first = self.config.getboolean(
|
||||
'comicvine', 'exact_series_matches_first')
|
||||
if self.config.has_option('comicvine', 'always_use_publisher_filter'):
|
||||
self.always_use_publisher_filter = self.config.getboolean(
|
||||
'comicvine', 'always_use_publisher_filter')
|
||||
|
||||
if self.config.has_option('comicvine', 'cv_api_key'):
|
||||
self.cv_api_key = self.config.get('comicvine', 'cv_api_key')
|
||||
|
||||
@ -375,8 +390,8 @@ class ComicTaggerSettings:
|
||||
self.id_length_delta_thresh)
|
||||
self.config.set(
|
||||
'identifier',
|
||||
'id_publisher_blacklist',
|
||||
self.id_publisher_blacklist)
|
||||
'id_publisher_filter',
|
||||
self.id_publisher_filter)
|
||||
|
||||
if not self.config.has_section('dialogflags'):
|
||||
self.config.add_section('dialogflags')
|
||||
@ -408,6 +423,14 @@ class ComicTaggerSettings:
|
||||
self.clear_form_before_populating_from_cv)
|
||||
self.config.set(
|
||||
'comicvine', 'remove_html_tables', self.remove_html_tables)
|
||||
|
||||
self.config.set(
|
||||
'comicvine', 'sort_series_by_year', self.sort_series_by_year)
|
||||
self.config.set(
|
||||
'comicvine', 'exact_series_matches_first', self.exact_series_matches_first)
|
||||
self.config.set(
|
||||
'comicvine', 'always_use_publisher_filter', self.always_use_publisher_filter)
|
||||
|
||||
self.config.set('comicvine', 'cv_api_key', self.cv_api_key)
|
||||
|
||||
if not self.config.has_section('cbl_transform'):
|
||||
|
@ -94,12 +94,12 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
|
||||
pblTip = (
|
||||
"""<html>
|
||||
The <b>Publisher Blacklist</b> is for eliminating automatic matches to certain publishers
|
||||
The <b>Publisher Filter</b> is for eliminating automatic matches to certain publishers
|
||||
that you know are incorrect. Useful for avoiding international re-prints with same
|
||||
covers or series names. Enter publisher names separated by commas.
|
||||
</html>"""
|
||||
)
|
||||
self.tePublisherBlacklist.setToolTip(pblTip)
|
||||
self.tePublisherFilter.setToolTip(pblTip)
|
||||
|
||||
validator = QtGui.QIntValidator(1, 4, self)
|
||||
self.leIssueNumPadding.setValidator(validator)
|
||||
@ -120,8 +120,8 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
self.leRarExePath.setText(self.settings.rar_exe_path)
|
||||
self.leNameLengthDeltaThresh.setText(
|
||||
str(self.settings.id_length_delta_thresh))
|
||||
self.tePublisherBlacklist.setPlainText(
|
||||
self.settings.id_publisher_blacklist)
|
||||
self.tePublisherFilter.setPlainText(
|
||||
self.settings.id_publisher_filter)
|
||||
|
||||
if self.settings.check_for_new_version:
|
||||
self.cbxCheckForNewVersion.setCheckState(QtCore.Qt.Checked)
|
||||
@ -135,6 +135,14 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
self.cbxClearFormBeforePopulating.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.remove_html_tables:
|
||||
self.cbxRemoveHtmlTables.setCheckState(QtCore.Qt.Checked)
|
||||
|
||||
if self.settings.always_use_publisher_filter:
|
||||
self.cbxUseFilter.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.sort_series_by_year:
|
||||
self.cbxSortByYear.setCheckState(QtCore.Qt.Checked)
|
||||
if self.settings.exact_series_matches_first:
|
||||
self.cbxExactMatches.setCheckState(QtCore.Qt.Checked)
|
||||
|
||||
self.leKey.setText(str(self.settings.cv_api_key))
|
||||
|
||||
if self.settings.assume_lone_credit_is_primary:
|
||||
@ -185,14 +193,19 @@ class SettingsWindow(QtWidgets.QDialog):
|
||||
|
||||
self.settings.id_length_delta_thresh = int(
|
||||
self.leNameLengthDeltaThresh.text())
|
||||
self.settings.id_publisher_blacklist = str(
|
||||
self.tePublisherBlacklist.toPlainText())
|
||||
self.settings.id_publisher_filter = str(
|
||||
self.tePublisherFilter.toPlainText())
|
||||
|
||||
self.settings.parse_scan_info = self.cbxParseScanInfo.isChecked()
|
||||
|
||||
self.settings.use_series_start_as_volume = self.cbxUseSeriesStartAsVolume.isChecked()
|
||||
self.settings.clear_form_before_populating_from_cv = self.cbxClearFormBeforePopulating.isChecked()
|
||||
self.settings.remove_html_tables = self.cbxRemoveHtmlTables.isChecked()
|
||||
|
||||
self.settings.always_use_publisher_filter = self.cbxUseFilter.isChecked()
|
||||
self.settings.sort_series_by_year = self.cbxSortByYear.isChecked()
|
||||
self.settings.exact_series_matches_first = self.cbxExactMatches.isChecked()
|
||||
|
||||
self.settings.cv_api_key = str(self.leKey.text())
|
||||
ComicVineTalker.api_key = self.settings.cv_api_key.strip()
|
||||
self.settings.assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked()
|
||||
|
@ -1385,8 +1385,8 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
else:
|
||||
screen = QtWidgets.QDesktopWidget().screenGeometry()
|
||||
size = self.frameGeometry()
|
||||
self.move((screen.width() - size.width()) / 2,
|
||||
(screen.height() - size.height()) / 2)
|
||||
self.move(int((screen.width() - size.width()) / 2),
|
||||
int((screen.height() - size.height()) / 2))
|
||||
|
||||
def adjustLoadStyleCombo(self):
|
||||
# select the current style
|
||||
@ -1422,7 +1422,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
# Add the entries to the language combobox
|
||||
self.cbLanguage.addItem("", "")
|
||||
lang_dict = utils.getLanguageDict()
|
||||
for key in sorted(lang_dict, key=functools.cmp_to_key(locale.strcoll)):
|
||||
for key in sorted(lang_dict, key=lang_dict.get):
|
||||
self.cbLanguage.addItem(lang_dict[key], key)
|
||||
|
||||
# Add the entries to the manga combobox
|
||||
@ -1721,7 +1721,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
def autoTagLog(self, text):
|
||||
IssueIdentifier.defaultWriteOutput(text)
|
||||
if self.atprogdialog is not None:
|
||||
self.atprogdialog.textEdit.insertPlainText(text)
|
||||
self.atprogdialog.textEdit.append(text.rstrip())
|
||||
self.atprogdialog.textEdit.ensureCursorVisible()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
@ -2064,8 +2064,13 @@ class TaggerWindow(QtWidgets.QMainWindow):
|
||||
self.comic_archive = None
|
||||
self.clearForm()
|
||||
|
||||
self.settings.last_opened_folder = os.path.abspath(
|
||||
os.path.split(comic_archive.path)[0])
|
||||
if not os.path.exists(comic_archive.path):
|
||||
self.fileSelectionList.dirty_flag = False
|
||||
self.fileSelectionList.removeArchiveList([comic_archive])
|
||||
QtCore.QTimer.singleShot(1, self.fileSelectionList.revertSelection)
|
||||
return
|
||||
|
||||
self.settings.last_opened_folder = os.path.abspath(os.path.split(comic_archive.path)[0])
|
||||
self.comic_archive = comic_archive
|
||||
self.metadata = self.comic_archive.readMetadata(self.load_data_style)
|
||||
if self.metadata is None:
|
||||
|
@ -37,6 +37,9 @@
|
||||
<property name="defaultDropAction">
|
||||
<enum>Qt::MoveAction</enum>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
|
@ -51,9 +51,9 @@ if qt_available:
|
||||
mysize = window.geometry()
|
||||
# The horizontal position is calculated as screen width - window width
|
||||
# /2
|
||||
hpos = (main_window_size.width() - window.width()) / 2
|
||||
hpos = int((main_window_size.width() - window.width()) / 2)
|
||||
# And vertical position the same, but with the height dimensions
|
||||
vpos = (main_window_size.height() - window.height()) / 2
|
||||
vpos = int((main_window_size.height() - window.height()) / 2)
|
||||
# And the move call repositions the window
|
||||
window.move(
|
||||
hpos +
|
||||
@ -63,7 +63,6 @@ if qt_available:
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
from PIL import WebPImagePlugin
|
||||
import io
|
||||
pil_available = True
|
||||
except ImportError:
|
||||
|
@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>702</width>
|
||||
<height>432</height>
|
||||
<height>478</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@ -28,7 +28,7 @@
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
<number>1</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="tab">
|
||||
<attribute name="title">
|
||||
@ -133,7 +133,7 @@
|
||||
</widget>
|
||||
<widget class="QWidget" name="tab_2">
|
||||
<attribute name="title">
|
||||
<string>Identifier</string>
|
||||
<string>Searching</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0">
|
||||
@ -187,15 +187,15 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Publisher Blacklist:</string>
|
||||
<string>Publisher Filter:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QPlainTextEdit" name="tePublisherBlacklist">
|
||||
<item row="2" column="1">
|
||||
<widget class="QPlainTextEdit" name="tePublisherFilter">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
@ -204,6 +204,23 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QCheckBox" name="cbxUseFilter">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Applies the <span style=" font-weight:600;">Publisher Filter</span> on all searches.<br/>The search window has a dynamic toggle to show the unfiltered results.</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="text">
|
||||
<string>Always use Publisher Filter on "manual" searches:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
@ -263,6 +280,33 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="line_4">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cbxSortByYear">
|
||||
<property name="text">
|
||||
<string>Initally sort Series search results by Starting Year instead of No. Issues</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cbxExactMatches">
|
||||
<property name="text">
|
||||
<string>Initally show Series Name exact matches first</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
@ -359,7 +403,7 @@
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="btnTestKey">
|
||||
<property name="text">
|
||||
<string>Tesk Key</string>
|
||||
<string>Test Key</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -148,6 +148,16 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="cbxFilter">
|
||||
<property name="text">
|
||||
<string>Filter Publishers</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Filter the publishers based on the publisher filter.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
|
@ -32,9 +32,11 @@ from .settings import ComicTaggerSettings
|
||||
from .matchselectionwindow import MatchSelectionWindow
|
||||
from .coverimagewidget import CoverImageWidget
|
||||
from comictaggerlib.ui.qtutils import reduceWidgetFontSize, centerWindowOnParent
|
||||
#from imagefetcher import ImageFetcher
|
||||
#import utils
|
||||
|
||||
from comictaggerlib import settings
|
||||
#from imagefetcher import ImageFetcher
|
||||
|
||||
from . import utils
|
||||
|
||||
class SearchThread(QtCore.QThread):
|
||||
|
||||
@ -124,6 +126,8 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
self.cover_index_list = cover_index_list
|
||||
self.cv_search_results = None
|
||||
|
||||
self.use_filter = self.settings.always_use_publisher_filter
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.currentItemChanged.connect(self.currentItemChanged)
|
||||
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
|
||||
@ -131,6 +135,9 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
self.btnIssues.clicked.connect(self.showIssues)
|
||||
self.btnAutoSelect.clicked.connect(self.autoSelect)
|
||||
|
||||
self.cbxFilter.setChecked(self.use_filter)
|
||||
self.cbxFilter.toggled.connect(self.filterToggled)
|
||||
|
||||
self.updateButtons()
|
||||
self.performQuery()
|
||||
self.twList.selectRow(0)
|
||||
@ -151,6 +158,10 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
self.performQuery(refresh=True)
|
||||
self.twList.selectRow(0)
|
||||
|
||||
def filterToggled(self):
|
||||
self.use_filter = not self.use_filter
|
||||
self.performQuery(refresh=False)
|
||||
|
||||
def autoSelect(self):
|
||||
|
||||
if self.comic_archive is None:
|
||||
@ -293,7 +304,6 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
self.progdialog.canceled.connect(self.searchCanceled)
|
||||
self.progdialog.setModal(True)
|
||||
self.progdialog.setMinimumDuration(300)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
self.search_thread = SearchThread(self.series_name, refresh)
|
||||
self.search_thread.searchComplete.connect(self.searchComplete)
|
||||
self.search_thread.progressUpdate.connect(self.searchProgressUpdate)
|
||||
@ -334,6 +344,40 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
return
|
||||
|
||||
self.cv_search_results = self.search_thread.cv_search_results
|
||||
# filter the publishers if enabled set
|
||||
if self.use_filter:
|
||||
try:
|
||||
publisher_filter = {s.strip().lower() for s in self.settings.id_publisher_filter.split(',')}
|
||||
# use '' as publisher name if None
|
||||
self.cv_search_results = list(filter(lambda d: ('' if d['publisher'] is None else str(d['publisher']['name']).lower()) not in publisher_filter, self.cv_search_results))
|
||||
except:
|
||||
print('bad data error filtering filter publishers')
|
||||
|
||||
# pre sort the data - so that we can put exact matches first afterwards
|
||||
# compare as str incase extra chars ie. '1976?'
|
||||
# - missing (none) values being converted to 'None' - consistant with prior behaviour in v1.2.3
|
||||
# sort by start_year if set
|
||||
if self.settings.sort_series_by_year:
|
||||
try:
|
||||
self.cv_search_results = sorted(self.cv_search_results, key = lambda i: (str(i['start_year']), str(i['count_of_issues'])), reverse=True)
|
||||
except:
|
||||
print('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:
|
||||
print('bad data error sorting results by count_of_issues')
|
||||
|
||||
# move sanitized matches to the front
|
||||
if self.settings.exact_series_matches_first:
|
||||
try:
|
||||
sanitized = utils.sanitize_title(self.series_name)
|
||||
exactMatches = list(filter(lambda d: utils.sanitize_title(str(d['name'])) in sanitized, self.cv_search_results))
|
||||
nonMatches = list(filter(lambda d: utils.sanitize_title(str(d['name'])) not in sanitized, self.cv_search_results))
|
||||
self.cv_search_results = exactMatches + nonMatches
|
||||
except:
|
||||
print('bad data error filtering exact/near matches')
|
||||
|
||||
self.updateButtons()
|
||||
|
||||
self.twList.setSortingEnabled(False)
|
||||
@ -375,9 +419,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
|
||||
row += 1
|
||||
|
||||
self.twList.resizeColumnsToContents()
|
||||
self.twList.setSortingEnabled(True)
|
||||
self.twList.sortItems(2, QtCore.Qt.DescendingOrder)
|
||||
self.twList.selectRow(0)
|
||||
self.twList.resizeColumnsToContents()
|
||||
|
||||
@ -406,7 +448,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
|
||||
return
|
||||
|
||||
self.volume_id = self.twList.item(curr.row(), 0).data(QtCore.Qt.UserRole)
|
||||
|
||||
|
||||
# list selection was changed, update the info on the volume
|
||||
for record in self.cv_search_results:
|
||||
if record['id'] == self.volume_id:
|
||||
|
@ -5,7 +5,7 @@ TAGGER_BASE ?= ../
|
||||
TAGGER_SRC := $(TAGGER_BASE)/comictaggerlib
|
||||
|
||||
APP_NAME := ComicTagger
|
||||
VERSION_STR := $(shell python setup.py --version)
|
||||
VERSION_STR := $(shell cd .. && python setup.py --version)
|
||||
|
||||
MAC_BASE := $(TAGGER_BASE)/mac
|
||||
DIST_DIR := $(MAC_BASE)/dist
|
||||
|
13
pyproject.toml
Normal file
13
pyproject.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[tool.black]
|
||||
line-length = 150
|
||||
|
||||
[tool.isort]
|
||||
line_length = 150
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
write_to = "comictaggerlib/ctversion.py"
|
||||
local_scheme = "no-local-version"
|
1
requirements-CBR.txt
Normal file
1
requirements-CBR.txt
Normal file
@ -0,0 +1 @@
|
||||
unrar-cffi>=0.2.2
|
1
requirements-GUI.txt
Normal file
1
requirements-GUI.txt
Normal file
@ -0,0 +1 @@
|
||||
PyQt5<=5.15.3
|
@ -1,9 +1,5 @@
|
||||
configparser
|
||||
requests
|
||||
beautifulsoup4 >= 4.1
|
||||
natsort==3.5.2
|
||||
PyPDF2==1.24
|
||||
configparser
|
||||
natsort
|
||||
pillow>=4.3.0
|
||||
PyQt5>=5.12.2
|
||||
pyinstaller>=3.5
|
||||
unrar-cffi==0.1.0a5
|
||||
requests
|
||||
|
4
requirements_dev.txt
Normal file
4
requirements_dev.txt
Normal file
@ -0,0 +1,4 @@
|
||||
pyinstaller==4.3
|
||||
setuptools>=42
|
||||
setuptools_scm[toml]>=3.4
|
||||
wheel
|
222
setup.py
222
setup.py
@ -6,174 +6,76 @@
|
||||
# It seems that post installation tweaks are broken by wheel files.
|
||||
# Kept here for further research
|
||||
|
||||
from __future__ import print_function
|
||||
from setuptools import setup
|
||||
from setuptools import dist
|
||||
from setuptools import Command
|
||||
import setuptools.command.build_py
|
||||
import setuptools.command.install
|
||||
import subprocess
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import platform
|
||||
import tempfile
|
||||
|
||||
python_requires='>=3',
|
||||
from setuptools import setup
|
||||
|
||||
with open('requirements.txt') as f:
|
||||
required = f.read().splitlines()
|
||||
|
||||
with open('README.md') as f:
|
||||
long_description = f.read()
|
||||
def read(fname):
|
||||
"""
|
||||
Read the contents of a file.
|
||||
Parameters
|
||||
----------
|
||||
fname : str
|
||||
Path to file.
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
File contents.
|
||||
"""
|
||||
with open(os.path.join(os.path.dirname(__file__), fname)) as f:
|
||||
return f.read()
|
||||
|
||||
platform_data_files = []
|
||||
|
||||
"""
|
||||
if platform.system() in [ "Windows" ]:
|
||||
required.append("winshell")
|
||||
install_requires = read("requirements.txt").splitlines()
|
||||
|
||||
# Some files to install on different platforms
|
||||
# Dynamically determine extra dependencies
|
||||
extras_require = {}
|
||||
extra_req_files = glob.glob("requirements-*.txt")
|
||||
for extra_req_file in extra_req_files:
|
||||
name = os.path.splitext(extra_req_file)[0].replace("requirements-", "", 1)
|
||||
extras_require[name] = read(extra_req_file).splitlines()
|
||||
|
||||
if platform.system() == "Linux":
|
||||
linux_desktop_shortcut = "/usr/local/share/applications/ComicTagger.desktop"
|
||||
platform_data_files = [("/usr/local/share/applications",
|
||||
["desktop-integration/linux/ComicTagger.desktop"]),
|
||||
("/usr/local/share/comictagger",
|
||||
["comictaggerlib/graphics/app.png"]),
|
||||
]
|
||||
|
||||
if platform.system() == "Windows":
|
||||
win_desktop_folder = os.path.join(os.environ["USERPROFILE"], "Desktop")
|
||||
win_appdata_folder = os.path.join(os.environ["APPDATA"], "comictagger")
|
||||
win_desktop_shortcut = os.path.join(win_desktop_folder, "ComicTagger-pip.lnk")
|
||||
platform_data_files = [(win_desktop_folder,
|
||||
["desktop-integration/windows/ComicTagger-pip.lnk"]),
|
||||
(win_appdata_folder,
|
||||
["windows/app.ico"]),
|
||||
]
|
||||
# If there are any extras, add a catch-all case that includes everything.
|
||||
# This assumes that entries in extras_require are lists (not single strings),
|
||||
# and that there are no duplicated packages across the extras.
|
||||
if extras_require:
|
||||
extras_require["all"] = sorted({x for v in extras_require.values() for x in v})
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
mac_app_folder = "/Applications"
|
||||
ct_app_name = "ComicTagger-pip.app"
|
||||
mac_app_infoplist = os.path.join(mac_app_folder, ct_app_name, "Contents", "Info.plist")
|
||||
mac_app_main = os.path.join(mac_app_folder, ct_app_name, "MacOS", "main.sh")
|
||||
mac_python_link = os.path.join(mac_app_folder, ct_app_name, "MacOS", "ComicTagger")
|
||||
platform_data_files = [(os.path.join(mac_app_folder, ct_app_name, "Contents"),
|
||||
["desktop-integration/mac/Info.plist"]),
|
||||
(os.path.join(mac_app_folder, ct_app_name, "Contents/Resources"),
|
||||
["mac/app.icns"]),
|
||||
(os.path.join(mac_app_folder, ct_app_name, "Contents/MacOS"),
|
||||
["desktop-integration/mac/main.sh",
|
||||
"desktop-integration/mac/ComicTagger"]),
|
||||
]
|
||||
|
||||
def fileTokenReplace(filename, token, replacement):
|
||||
with open(filename, "rt") as fin:
|
||||
fd, tmpfile = tempfile.mkstemp()
|
||||
with open(tmpfile, "wt") as fout:
|
||||
for line in fin:
|
||||
fout.write(line.replace('%%{}%%'.format(token), replacement))
|
||||
os.close(fd)
|
||||
# fix permissions of temp file
|
||||
os.chmod(tmpfile, 420) #Octal 0o644
|
||||
os.rename(tmpfile, filename)
|
||||
|
||||
def postInstall(scripts_folder):
|
||||
entry_point_script = os.path.join(scripts_folder, "comictagger")
|
||||
|
||||
if platform.system() == "Windows":
|
||||
# doctor the shortcut for this windows system after deployment
|
||||
import winshell
|
||||
winshell.CreateShortcut(
|
||||
Path=os.path.abspath(win_desktop_shortcut),
|
||||
Target=entry_point_script + ".exe",
|
||||
Icon=(os.path.join(win_appdata_folder, 'app.ico'), 0),
|
||||
Description="Launch ComicTagger as installed by PIP"
|
||||
)
|
||||
|
||||
if platform.system() == "Linux":
|
||||
# doctor the script path in the desktop file
|
||||
fileTokenReplace(linux_desktop_shortcut,
|
||||
"CTSCRIPT",
|
||||
entry_point_script)
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
# doctor the plist app version
|
||||
fileTokenReplace(mac_app_infoplist,
|
||||
"CTVERSION",
|
||||
comictaggerlib.ctversion.version)
|
||||
# doctor the script path in main.sh
|
||||
fileTokenReplace(mac_app_main,
|
||||
"CTSCRIPT",
|
||||
entry_point_script)
|
||||
# Make the launcher script executable
|
||||
os.chmod(mac_app_main, 509) #Octal 0o775
|
||||
|
||||
# Final install step: create a symlink to Python OS X application
|
||||
punt = False
|
||||
pythonpath,top = os.path.split(os.path.realpath(sys.executable))
|
||||
while top:
|
||||
if 'Resources' in pythonpath:
|
||||
pass
|
||||
elif os.path.exists(os.path.join(pythonpath,'Resources')):
|
||||
break
|
||||
pythonpath,top = os.path.split(pythonpath)
|
||||
else:
|
||||
print("Failed to find a Resources directory associated with ", str(sys.executable))
|
||||
punt = True
|
||||
|
||||
if not punt:
|
||||
pythonapp = os.path.join(pythonpath, 'Resources','Python.app','Contents','MacOS','Python')
|
||||
if not os.path.exists(pythonapp):
|
||||
print("Failed to find a Python app in ", str(pythonapp))
|
||||
punt = True
|
||||
|
||||
# remove the placeholder
|
||||
os.remove(mac_python_link)
|
||||
if not punt:
|
||||
os.symlink(pythonapp, mac_python_link)
|
||||
else:
|
||||
# We failed, but we can still be functional
|
||||
os.symlink(sys.executable, mac_python_link)
|
||||
"""
|
||||
|
||||
setup(name="comictagger",
|
||||
install_requires=required,
|
||||
description="A cross-platform GUI/CLI app for writing metadata to comic archives",
|
||||
author="ComicTagger team",
|
||||
author_email="comictagger@gmail.com",
|
||||
url="https://github.com/comictagger/comictagger",
|
||||
packages=["comictaggerlib", "comicapi"],
|
||||
package_data={
|
||||
'comictaggerlib': ['ui/*', 'graphics/*'],
|
||||
},
|
||||
entry_points=dict(console_scripts=['comictagger=comictaggerlib.main:ctmain']),
|
||||
data_files=platform_data_files,
|
||||
setup_requires=[
|
||||
"setuptools_scm"
|
||||
],
|
||||
use_scm_version={
|
||||
'write_to': 'comictaggerlib/ctversion.py'
|
||||
},
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Console",
|
||||
"Environment :: Win32 (MS Windows)",
|
||||
"Environment :: MacOS X",
|
||||
"Environment :: X11 Applications :: Qt",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Natural Language :: English",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Topic :: Utilities",
|
||||
"Topic :: Other/Nonlisted Topic",
|
||||
"Topic :: Multimedia :: Graphics"
|
||||
],
|
||||
keywords=['comictagger', 'comics', 'comic', 'metadata', 'tagging', 'tagger'],
|
||||
license="Apache License 2.0",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown"
|
||||
setup(
|
||||
name="comictagger",
|
||||
install_requires=install_requires,
|
||||
extras_require=extras_require,
|
||||
python_requires=">=3",
|
||||
description="A cross-platform GUI/CLI app for writing metadata to comic archives",
|
||||
author="ComicTagger team",
|
||||
author_email="comictagger@gmail.com",
|
||||
url="https://github.com/comictagger/comictagger",
|
||||
packages=["comictaggerlib", "comicapi"],
|
||||
package_data={
|
||||
"comictaggerlib": ["ui/*", "graphics/*"],
|
||||
},
|
||||
entry_points=dict(console_scripts=["comictagger=comictaggerlib.main:ctmain"]),
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Console",
|
||||
"Environment :: Win32 (MS Windows)",
|
||||
"Environment :: MacOS X",
|
||||
"Environment :: X11 Applications :: Qt",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Natural Language :: English",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Topic :: Utilities",
|
||||
"Topic :: Other/Nonlisted Topic",
|
||||
"Topic :: Multimedia :: Graphics",
|
||||
],
|
||||
keywords=["comictagger", "comics", "comic", "metadata", "tagging", "tagger"],
|
||||
license="Apache License 2.0",
|
||||
long_description=read("README.md"),
|
||||
long_description_content_type='text/markdown'
|
||||
)
|
||||
|
Reference in New Issue
Block a user