Compare commits

..

2 Commits

Author SHA1 Message Date
50c53e14da Add seriesYear attribute 2021-12-15 11:02:50 -08:00
2e01a4db14 Improve file renaming
Moves to Python format strings for renaming, handles directory
structures, moving of files to a destination directory, sanitizes
file paths with pathvalidate and takes a different approach to
smart filename cleanup using the Python string.Formatter class

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

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

The only changes to the string.Formatter class is:
1. format_field returns
an empty string if the value is none or an empty string regardless of
the format specifier.
2. _vformat drops the previous literal text if the field value
is an empty string and lstrips the following literal text of closing
special characters.
2021-12-15 11:00:01 -08:00
83 changed files with 7026 additions and 5843 deletions

View File

@ -1,4 +0,0 @@
[flake8]
max-line-length = 120
extend-ignore = E203, E501, E722
extend-exclude = venv, scripts

View File

@ -15,8 +15,8 @@ else
FINAL_NAME=ComicTagger-$(VERSION_STR)
endif
.PHONY: all clean pydist upload dist CI
.PHONY: all clean pydist upload dist
all: clean dist
clean:
@ -26,29 +26,24 @@ clean:
rm -rf dist MANIFEST
rm -rf *.deb
rm -rf logdict*.log
$(MAKE) -C mac clean
$(MAKE) -C mac clean
rm -rf build
rm -rf comictaggerlib/ui/__pycache__
rm comictaggerlib/ctversion.py
CI:
black .
isort .
flake8 .
pydist: CI
pydist:
make clean
mkdir -p piprelease
rm -f comictagger-$(VERSION_STR).zip
$(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=gztar upload
dist: CI
dist:
$(PIP) install .
pyinstaller -y comictagger.spec
cd dist && zip -r $(FINAL_NAME).zip $(APP_NAME)

100
README.md
View File

@ -1,50 +1,50 @@
[![Build Status](https://travis-ci.org/comictagger/comictagger.svg?branch=develop)](https://travis-ci.org/comictagger/comictagger)
[![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/comictagger/community)
[![Google Group](https://img.shields.io/badge/discuss-on%20groups-%23207de5)](https://groups.google.com/forum/#!forum/comictagger)
[![Twitter](https://img.shields.io/badge/%40comictagger-twitter-lightgrey)](https://twitter.com/comictagger)
[![Facebook](https://img.shields.io/badge/comictagger-facebook-lightgrey)](https://www.facebook.com/ComicTagger-139615369550787/)
# ComicTagger
ComicTagger is a **multi-platform** app for **writing metadata to digital comics**, written in Python and PyQt.
![ComicTagger logo](https://raw.githubusercontent.com/comictagger/comictagger/develop/comictaggerlib/graphics/app.png)
## Features
* Runs on macOS, Microsoft Windows, and Linux systems
* Get comic information from [Comic Vine](https://comicvine.gamespot.com/)
* **Automatic issue matching** using advanced image processing techniques
* **Batch processing** in the GUI for tagging hundreds or more comics at a time
* Support for **ComicRack** and **ComicBookLover** tagging formats
* Native full support for **CBZ** digital comics
* Native read only support for **CBR** digital comics: full support enabled installing additional [rar tools](https://www.rarlab.com/download.htm)
* Command line interface (CLI) enabling **custom scripting** and **batch operations on large collections**
For details, screen-shots, release notes, and more, visit [the Wiki](https://github.com/comictagger/comictagger/wiki)
## Installation
### Binaries
Windows and macOS binaries are provided in the [Releases Page](https://github.com/comictagger/comictagger/releases).
Just unzip the archive in any folder and run, no additional installation steps are required.
### PIP installation
A pip package is provided, you can install it with:
```
$ pip3 install comictagger[GUI]
```
### From source
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`
[![Build Status](https://travis-ci.org/comictagger/comictagger.svg?branch=develop)](https://travis-ci.org/comictagger/comictagger)
[![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/comictagger/community)
[![Google Group](https://img.shields.io/badge/discuss-on%20groups-%23207de5)](https://groups.google.com/forum/#!forum/comictagger)
[![Twitter](https://img.shields.io/badge/%40comictagger-twitter-lightgrey)](https://twitter.com/comictagger)
[![Facebook](https://img.shields.io/badge/comictagger-facebook-lightgrey)](https://www.facebook.com/ComicTagger-139615369550787/)
# ComicTagger
ComicTagger is a **multi-platform** app for **writing metadata to digital comics**, written in Python and PyQt.
![ComicTagger logo](https://raw.githubusercontent.com/comictagger/comictagger/develop/comictaggerlib/graphics/app.png)
## Features
* Runs on macOS, Microsoft Windows, and Linux systems
* Get comic information from [Comic Vine](https://comicvine.gamespot.com/)
* **Automatic issue matching** using advanced image processing techniques
* **Batch processing** in the GUI for tagging hundreds or more comics at a time
* Support for **ComicRack** and **ComicBookLover** tagging formats
* Native full support for **CBZ** digital comics
* Native read only support for **CBR** digital comics: full support enabled installing additional [rar tools](https://www.rarlab.com/download.htm)
* Command line interface (CLI) enabling **custom scripting** and **batch operations on large collections**
For details, screen-shots, release notes, and more, visit [the Wiki](https://github.com/comictagger/comictagger/wiki)
## Installation
### Binaries
Windows and macOS binaries are provided in the [Releases Page](https://github.com/comictagger/comictagger/releases).
Just unzip the archive in any folder and run, no additional installation steps are required.
### PIP installation
A pip package is provided, you can install it with:
```
$ pip3 install comictagger[GUI]
```
### From source
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`

View File

@ -1 +1 @@
__author__ = "dromanin"
__author__ = 'dromanin'

View File

@ -14,125 +14,176 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import xml.etree.ElementTree as ET
#from datetime import datetime
#from pprint import pprint
#import zipfile
from comicapi import utils
from comicapi.genericmetadata import GenericMetadata
logger = logging.getLogger(__name__)
from .genericmetadata import GenericMetadata
from . import utils
class CoMet:
writer_synonyms = ["writer", "plotter", "scripter"]
penciller_synonyms = ["artist", "penciller", "penciler", "breakdowns"]
inker_synonyms = ["inker", "artist", "finishes"]
colorist_synonyms = ["colorist", "colourist", "colorer", "colourer"]
letterer_synonyms = ["letterer"]
cover_synonyms = ["cover", "covers", "coverartist", "cover artist"]
editor_synonyms = ["editor"]
writer_synonyms = ['writer', 'plotter', 'scripter']
penciller_synonyms = ['artist', 'penciller', 'penciler', 'breakdowns']
inker_synonyms = ['inker', 'artist', 'finishes']
colorist_synonyms = ['colorist', 'colourist', 'colorer', 'colourer']
letterer_synonyms = ['letterer']
cover_synonyms = ['cover', 'covers', 'coverartist', 'cover artist']
editor_synonyms = ['editor']
def metadata_from_string(self, string):
def metadataFromString(self, string):
tree = ET.ElementTree(ET.fromstring(string))
return self.convert_xml_to_metadata(tree)
return self.convertXMLToMetadata(tree)
def string_from_metadata(self, metadata):
def stringFromMetadata(self, metadata):
header = '<?xml version="1.0" encoding="UTF-8"?>\n'
tree = self.convert_metadata_to_xml(metadata)
tree = self.convertMetadataToXML(self, metadata)
return header + ET.tostring(tree.getroot())
def convert_metadata_to_xml(self, metadata):
def indent(self, elem, level=0):
# for making the XML output readable
i = "\n" + level * " "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
self.indent(elem, level + 1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
def convertMetadataToXML(self, filename, metadata):
# shorthand for the metadata
md = metadata
# build a tree structure
root = ET.Element("comet")
root.attrib["xmlns:comet"] = "http://www.denvog.com/comet/"
root.attrib["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance"
root.attrib["xsi:schemaLocation"] = "http://www.denvog.com http://www.denvog.com/comet/comet.xsd"
root.attrib['xmlns:comet'] = "http://www.denvog.com/comet/"
root.attrib['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance"
root.attrib[
'xsi:schemaLocation'] = "http://www.denvog.com http://www.denvog.com/comet/comet.xsd"
# helper func
def assign(comet_entry, md_entry):
if md_entry is not None:
ET.SubElement(root, comet_entry).text = str(md_entry)
ET.SubElement(root, comet_entry).text = "{0}".format(md_entry)
# title is manditory
if md.title is None:
md.title = ""
assign("title", md.title)
assign("series", md.series)
assign("issue", md.issue) # must be int??
assign("volume", md.volume)
assign("description", md.comments)
assign("publisher", md.publisher)
assign("pages", md.page_count)
assign("format", md.format)
assign("language", md.language)
assign("rating", md.maturity_rating)
assign("price", md.price)
assign("isVersionOf", md.is_version_of)
assign("rights", md.rights)
assign("identifier", md.identifier)
assign("lastMark", md.last_mark)
assign("genre", md.genre) # TODO repeatable
assign('title', md.title)
assign('series', md.series)
assign('issue', md.issue) # must be int??
assign('volume', md.volume)
assign('description', md.comments)
assign('publisher', md.publisher)
assign('pages', md.pageCount)
assign('format', md.format)
assign('language', md.language)
assign('rating', md.maturityRating)
assign('price', md.price)
assign('isVersionOf', md.isVersionOf)
assign('rights', md.rights)
assign('identifier', md.identifier)
assign('lastMark', md.lastMark)
assign('genre', md.genre) # TODO repeatable
if md.characters is not None:
char_list = [c.strip() for c in md.characters.split(",")]
char_list = [c.strip() for c in md.characters.split(',')]
for c in char_list:
assign("character", c)
assign('character', c)
if md.manga is not None and md.manga == "YesAndRightToLeft":
assign("readingDirection", "rtl")
assign('readingDirection', "rtl")
date_str = ""
if md.year is not None:
date_str = str(md.year).zfill(4)
if md.month is not None:
date_str += "-" + str(md.month).zfill(2)
assign("date", date_str)
assign('date', date_str)
assign("coverImage", md.cover_image)
assign('coverImage', md.coverImage)
# need to specially process the credits, since they are structured
# differently than CIX
credit_writer_list = list()
credit_penciller_list = list()
credit_inker_list = list()
credit_colorist_list = list()
credit_letterer_list = list()
credit_cover_list = list()
credit_editor_list = list()
# loop thru credits, and build a list for each role that CoMet supports
for credit in metadata.credits:
if credit["role"].lower() in set(self.writer_synonyms):
ET.SubElement(root, "writer").text = str(credit["person"])
if credit['role'].lower() in set(self.writer_synonyms):
ET.SubElement(
root,
'writer').text = "{0}".format(
credit['person'])
if credit["role"].lower() in set(self.penciller_synonyms):
ET.SubElement(root, "penciller").text = str(credit["person"])
if credit['role'].lower() in set(self.penciller_synonyms):
ET.SubElement(
root,
'penciller').text = "{0}".format(
credit['person'])
if credit["role"].lower() in set(self.inker_synonyms):
ET.SubElement(root, "inker").text = str(credit["person"])
if credit['role'].lower() in set(self.inker_synonyms):
ET.SubElement(
root,
'inker').text = "{0}".format(
credit['person'])
if credit["role"].lower() in set(self.colorist_synonyms):
ET.SubElement(root, "colorist").text = str(credit["person"])
if credit['role'].lower() in set(self.colorist_synonyms):
ET.SubElement(
root,
'colorist').text = "{0}".format(
credit['person'])
if credit["role"].lower() in set(self.letterer_synonyms):
ET.SubElement(root, "letterer").text = str(credit["person"])
if credit['role'].lower() in set(self.letterer_synonyms):
ET.SubElement(
root,
'letterer').text = "{0}".format(
credit['person'])
if credit["role"].lower() in set(self.cover_synonyms):
ET.SubElement(root, "coverDesigner").text = str(credit["person"])
if credit['role'].lower() in set(self.cover_synonyms):
ET.SubElement(
root,
'coverDesigner').text = "{0}".format(
credit['person'])
if credit["role"].lower() in set(self.editor_synonyms):
ET.SubElement(root, "editor").text = str(credit["person"])
if credit['role'].lower() in set(self.editor_synonyms):
ET.SubElement(
root,
'editor').text = "{0}".format(
credit['person'])
utils.indent(root)
# self pretty-print
self.indent(root)
# wrap it in an ElementTree instance, and save as XML
tree = ET.ElementTree(root)
return tree
def convert_xml_to_metadata(self, tree):
def convertXMLToMetadata(self, tree):
root = tree.getroot()
if root.tag != "comet":
raise "1"
if root.tag != 'comet':
raise 1
return None
metadata = GenericMetadata()
md = metadata
@ -142,85 +193,84 @@ class CoMet:
node = root.find(tag)
if node is not None:
return node.text
return None
else:
return None
md.series = xlate("series")
md.title = xlate("title")
md.issue = xlate("issue")
md.volume = xlate("volume")
md.comments = xlate("description")
md.publisher = xlate("publisher")
md.language = xlate("language")
md.format = xlate("format")
md.page_count = xlate("pages")
md.maturity_rating = xlate("rating")
md.price = xlate("price")
md.is_version_of = xlate("isVersionOf")
md.rights = xlate("rights")
md.identifier = xlate("identifier")
md.last_mark = xlate("lastMark")
md.genre = xlate("genre") # TODO - repeatable field
md.series = xlate('series')
md.title = xlate('title')
md.issue = xlate('issue')
md.volume = xlate('volume')
md.comments = xlate('description')
md.publisher = xlate('publisher')
md.language = xlate('language')
md.format = xlate('format')
md.pageCount = xlate('pages')
md.maturityRating = xlate('rating')
md.price = xlate('price')
md.isVersionOf = xlate('isVersionOf')
md.rights = xlate('rights')
md.identifier = xlate('identifier')
md.lastMark = xlate('lastMark')
md.genre = xlate('genre') # TODO - repeatable field
date = xlate("date")
date = xlate('date')
if date is not None:
parts = date.split("-")
parts = date.split('-')
if len(parts) > 0:
md.year = parts[0]
if len(parts) > 1:
md.month = parts[1]
md.cover_image = xlate("coverImage")
md.coverImage = xlate('coverImage')
reading_direction = xlate("readingDirection")
if reading_direction is not None and reading_direction == "rtl":
readingDirection = xlate('readingDirection')
if readingDirection is not None and readingDirection == "rtl":
md.manga = "YesAndRightToLeft"
# loop for character tags
char_list = []
for n in root:
if n.tag == "character":
if n.tag == 'character':
char_list.append(n.text.strip())
md.characters = utils.list_to_string(char_list)
md.characters = utils.listToString(char_list)
# Now extract the credit info
for n in root:
if any(
[
n.tag == "writer",
n.tag == "penciller",
n.tag == "inker",
n.tag == "colorist",
n.tag == "letterer",
n.tag == "editor",
]
):
metadata.add_credit(n.text.strip(), n.tag.title())
if (n.tag == 'writer' or
n.tag == 'penciller' or
n.tag == 'inker' or
n.tag == 'colorist' or
n.tag == 'letterer' or
n.tag == 'editor'
):
metadata.addCredit(n.text.strip(), n.tag.title())
if n.tag == "coverDesigner":
metadata.add_credit(n.text.strip(), "Cover")
if n.tag == 'coverDesigner':
metadata.addCredit(n.text.strip(), "Cover")
metadata.is_empty = False
metadata.isEmpty = False
return metadata
# verify that the string actually contains CoMet data in XML format
def validate_string(self, string):
def validateString(self, string):
try:
tree = ET.ElementTree(ET.fromstring(string))
root = tree.getroot()
if root.tag != "comet":
if root.tag != 'comet':
raise Exception
except:
return False
return True
def write_to_external_file(self, filename, metadata):
def writeToExternalFile(self, filename, metadata):
tree = self.convert_metadata_to_xml(metadata)
tree.write(filename, encoding="utf-8")
tree = self.convertMetadataToXML(self, metadata)
# ET.dump(tree)
tree.write(filename, encoding='utf-8')
def read_from_external_file(self, filename):
def readFromExternalFile(self, filename):
tree = ET.parse(filename)
return self.convert_xml_to_metadata(tree)
return self.convertXMLToMetadata(tree)

File diff suppressed because it is too large Load Diff

View File

@ -15,42 +15,43 @@
# limitations under the License.
import json
import logging
from collections import defaultdict
from datetime import datetime
#import zipfile
from comicapi import utils
from comicapi.genericmetadata import GenericMetadata
logger = logging.getLogger(__name__)
from .genericmetadata import GenericMetadata
from . import utils
#import ctversion
class ComicBookInfo:
def metadata_from_string(self, string):
cbi_container = json.loads(str(string, "utf-8"))
def metadataFromString(self, string):
class Default(dict):
def __missing__(self, key):
return None
cbi_container = json.loads(str(string, 'utf-8'))
metadata = GenericMetadata()
cbi = defaultdict(lambda: None, cbi_container["ComicBookInfo/1.0"])
cbi = Default(cbi_container['ComicBookInfo/1.0'])
metadata.series = utils.xlate(cbi["series"])
metadata.title = utils.xlate(cbi["title"])
metadata.issue = utils.xlate(cbi["issue"])
metadata.publisher = utils.xlate(cbi["publisher"])
metadata.month = utils.xlate(cbi["publicationMonth"], True)
metadata.year = utils.xlate(cbi["publicationYear"], True)
metadata.issue_count = utils.xlate(cbi["numberOfIssues"], True)
metadata.comments = utils.xlate(cbi["comments"])
metadata.genre = utils.xlate(cbi["genre"])
metadata.volume = utils.xlate(cbi["volume"], True)
metadata.volume_count = utils.xlate(cbi["numberOfVolumes"], True)
metadata.language = utils.xlate(cbi["language"])
metadata.country = utils.xlate(cbi["country"])
metadata.critical_rating = utils.xlate(cbi["rating"])
metadata.series = utils.xlate(cbi['series'])
metadata.title = utils.xlate(cbi['title'])
metadata.issue = utils.xlate(cbi['issue'])
metadata.publisher = utils.xlate(cbi['publisher'])
metadata.month = utils.xlate(cbi['publicationMonth'], True)
metadata.year = utils.xlate(cbi['publicationYear'], True)
metadata.seriesYear = utils.xlate('seriesPublicationYear', True)
metadata.issueCount = utils.xlate(cbi['numberOfIssues'], True)
metadata.comments = utils.xlate(cbi['comments'])
metadata.genre = utils.xlate(cbi['genre'])
metadata.volume = utils.xlate(cbi['volume'], True)
metadata.volumeCount = utils.xlate(cbi['numberOfVolumes'], True)
metadata.language = utils.xlate(cbi['language'])
metadata.country = utils.xlate(cbi['country'])
metadata.criticalRating = utils.xlate(cbi['rating'])
metadata.credits = cbi["credits"]
metadata.tags = cbi["tags"]
metadata.credits = cbi['credits']
metadata.tags = cbi['tags']
# make sure credits and tags are at least empty lists and not None
if metadata.credits is None:
@ -58,20 +59,26 @@ class ComicBookInfo:
if metadata.tags is None:
metadata.tags = []
# need the language string to be ISO
# need to massage the language string to be ISO
if metadata.language is not None:
metadata.language = utils.get_language(metadata.language)
# reverse look-up
pattern = metadata.language
metadata.language = None
for key in utils.getLanguageDict():
if utils.getLanguageDict()[key] == pattern.encode('utf-8'):
metadata.language = key
break
metadata.is_empty = False
metadata.isEmpty = False
return metadata
def string_from_metadata(self, metadata):
def stringFromMetadata(self, metadata):
cbi_container = self.create_json_dictionary(metadata)
cbi_container = self.createJSONDictionary(metadata)
return json.dumps(cbi_container)
def validate_string(self, string):
def validateString(self, string):
"""Verify that the string actually contains CBI data in JSON format"""
try:
@ -79,45 +86,45 @@ class ComicBookInfo:
except:
return False
return "ComicBookInfo/1.0" in cbi_container
return ('ComicBookInfo/1.0' in cbi_container)
def create_json_dictionary(self, metadata):
def createJSONDictionary(self, metadata):
"""Create the dictionary that we will convert to JSON text"""
cbi = {}
cbi_container = {
"appID": "ComicTagger/" + "1.0.0",
"lastModified": str(datetime.now()),
"ComicBookInfo/1.0": cbi,
} # TODO: ctversion.version,
cbi = dict()
cbi_container = {'appID': 'ComicTagger/' + '1.0.0', # ctversion.version,
'lastModified': str(datetime.now()),
'ComicBookInfo/1.0': cbi}
# helper func
def assign(cbi_entry, md_entry):
if md_entry is not None or isinstance(md_entry, str) and md_entry != "":
cbi[cbi_entry] = md_entry
assign("series", utils.xlate(metadata.series))
assign("title", utils.xlate(metadata.title))
assign("issue", utils.xlate(metadata.issue))
assign("publisher", utils.xlate(metadata.publisher))
assign("publicationMonth", utils.xlate(metadata.month, True))
assign("publicationYear", utils.xlate(metadata.year, True))
assign("numberOfIssues", utils.xlate(metadata.issue_count, True))
assign("comments", utils.xlate(metadata.comments))
assign("genre", utils.xlate(metadata.genre))
assign("volume", utils.xlate(metadata.volume, True))
assign("numberOfVolumes", utils.xlate(metadata.volume_count, True))
assign("language", utils.xlate(utils.get_language_from_iso(metadata.language)))
assign("country", utils.xlate(metadata.country))
assign("rating", utils.xlate(metadata.critical_rating))
assign("credits", metadata.credits)
assign("tags", metadata.tags)
assign('series', utils.xlate(metadata.series))
assign('title', utils.xlate(metadata.title))
assign('issue', utils.xlate(metadata.issue))
assign('publisher', utils.xlate(metadata.publisher))
assign('publicationMonth', utils.xlate(metadata.month, True))
assign('publicationYear', utils.xlate(metadata.year, True))
assign('seriesPublicationYear', utils.xlate(metadata.seriesYear, True))
assign('numberOfIssues', utils.xlate(metadata.issueCount, True))
assign('comments', utils.xlate(metadata.comments))
assign('genre', utils.xlate(metadata.genre))
assign('volume', utils.xlate(metadata.volume, True))
assign('numberOfVolumes', utils.xlate(metadata.volumeCount, True))
assign('language', utils.xlate(utils.getLanguageFromISO(metadata.language)))
assign('country', utils.xlate(metadata.country))
assign('rating', utils.xlate(metadata.criticalRating))
assign('credits', metadata.credits)
assign('tags', metadata.tags)
return cbi_container
def write_to_external_file(self, filename, metadata):
def writeToExternalFile(self, filename, metadata):
cbi_container = self.create_json_dictionary(metadata)
cbi_container = self.createJSONDictionary(metadata)
with open(filename, "w") as f:
f.write(json.dumps(cbi_container, indent=4))
f = open(filename, 'w')
f.write(json.dumps(cbi_container, indent=4))
f.close

View File

@ -14,27 +14,27 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import xml.etree.ElementTree as ET
#from datetime import datetime
#from pprint import pprint
#import zipfile
from comicapi import utils
from comicapi.genericmetadata import GenericMetadata
from comicapi.issuestring import IssueString
logger = logging.getLogger(__name__)
from .genericmetadata import GenericMetadata
from .issuestring import IssueString
from . import utils
class ComicInfoXml:
writer_synonyms = ["writer", "plotter", "scripter"]
penciller_synonyms = ["artist", "penciller", "penciler", "breakdowns"]
inker_synonyms = ["inker", "artist", "finishes"]
colorist_synonyms = ["colorist", "colourist", "colorer", "colourer"]
letterer_synonyms = ["letterer"]
cover_synonyms = ["cover", "covers", "coverartist", "cover artist"]
editor_synonyms = ["editor"]
writer_synonyms = ['writer', 'plotter', 'scripter']
penciller_synonyms = ['artist', 'penciller', 'penciler', 'breakdowns']
inker_synonyms = ['inker', 'artist', 'finishes']
colorist_synonyms = ['colorist', 'colourist', 'colorer', 'colourer']
letterer_synonyms = ['letterer']
cover_synonyms = ['cover', 'covers', 'coverartist', 'cover artist']
editor_synonyms = ['editor']
def get_parseable_credits(self):
def getParseableCredits(self):
parsable_credits = []
parsable_credits.extend(self.writer_synonyms)
parsable_credits.extend(self.penciller_synonyms)
@ -45,146 +45,168 @@ class ComicInfoXml:
parsable_credits.extend(self.editor_synonyms)
return parsable_credits
def metadata_from_string(self, string):
def metadataFromString(self, string):
tree = ET.ElementTree(ET.fromstring(string))
return self.convert_xml_to_metadata(tree)
return self.convertXMLToMetadata(tree)
def string_from_metadata(self, metadata, xml=None):
tree = self.convert_metadata_to_xml(self, metadata, xml)
tree_str = ET.tostring(tree.getroot(), encoding="utf-8", xml_declaration=True).decode()
return tree_str
def stringFromMetadata(self, metadata):
def convert_metadata_to_xml(self, filename, metadata, xml=None):
header = '<?xml version="1.0"?>\n'
tree = self.convertMetadataToXML(self, metadata)
tree_str = ET.tostring(tree.getroot()).decode()
return header + tree_str
def indent(self, elem, level=0):
# for making the XML output readable
i = "\n" + level * " "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
self.indent(elem, level + 1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
def convertMetadataToXML(self, filename, metadata):
# shorthand for the metadata
md = metadata
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"
# 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 and md_entry:
et_entry = root.find(cix_entry)
if et_entry is not None:
et_entry.text = str(md_entry)
else:
ET.SubElement(root, cix_entry).text = str(md_entry)
else:
et_entry = root.find(cix_entry)
if et_entry is not None:
et_entry.clear()
if md_entry is not None:
ET.SubElement(root, cix_entry).text = "{0}".format(md_entry)
assign("Title", md.title)
assign("Series", md.series)
assign("Number", md.issue)
assign("Count", md.issue_count)
assign("Volume", md.volume)
assign("AlternateSeries", md.alternate_series)
assign("AlternateNumber", md.alternate_number)
assign("StoryArc", md.story_arc)
assign("SeriesGroup", md.series_group)
assign("AlternateCount", md.alternate_count)
assign("Summary", md.comments)
assign("Notes", md.notes)
assign("Year", md.year)
assign("Month", md.month)
assign("Day", md.day)
assign('Title', md.title)
assign('Series', md.series)
assign('Number', md.issue)
assign('Count', md.issueCount)
assign('Volume', md.volume)
assign('AlternateSeries', md.alternateSeries)
assign('AlternateNumber', md.alternateNumber)
assign('StoryArc', md.storyArc)
assign('SeriesGroup', md.seriesGroup)
assign('AlternateCount', md.alternateCount)
assign('Summary', md.comments)
assign('Notes', md.notes)
assign('Year', md.year)
assign('Month', md.month)
assign('Day', md.day)
assign('SeriesYear', md.seriesYear)
# need to specially process the credits, since they are structured
# differently than CIX
credit_writer_list = []
credit_penciller_list = []
credit_inker_list = []
credit_colorist_list = []
credit_letterer_list = []
credit_cover_list = []
credit_editor_list = []
credit_writer_list = list()
credit_penciller_list = list()
credit_inker_list = list()
credit_colorist_list = list()
credit_letterer_list = list()
credit_cover_list = list()
credit_editor_list = list()
# first, loop thru credits, and build a list for each role that CIX
# supports
for credit in metadata.credits:
if credit["role"].lower() in set(self.writer_synonyms):
credit_writer_list.append(credit["person"].replace(",", ""))
if credit['role'].lower() in set(self.writer_synonyms):
credit_writer_list.append(credit['person'].replace(",", ""))
if credit["role"].lower() in set(self.penciller_synonyms):
credit_penciller_list.append(credit["person"].replace(",", ""))
if credit['role'].lower() in set(self.penciller_synonyms):
credit_penciller_list.append(credit['person'].replace(",", ""))
if credit["role"].lower() in set(self.inker_synonyms):
credit_inker_list.append(credit["person"].replace(",", ""))
if credit['role'].lower() in set(self.inker_synonyms):
credit_inker_list.append(credit['person'].replace(",", ""))
if credit["role"].lower() in set(self.colorist_synonyms):
credit_colorist_list.append(credit["person"].replace(",", ""))
if credit['role'].lower() in set(self.colorist_synonyms):
credit_colorist_list.append(credit['person'].replace(",", ""))
if credit["role"].lower() in set(self.letterer_synonyms):
credit_letterer_list.append(credit["person"].replace(",", ""))
if credit['role'].lower() in set(self.letterer_synonyms):
credit_letterer_list.append(credit['person'].replace(",", ""))
if credit["role"].lower() in set(self.cover_synonyms):
credit_cover_list.append(credit["person"].replace(",", ""))
if credit['role'].lower() in set(self.cover_synonyms):
credit_cover_list.append(credit['person'].replace(",", ""))
if credit["role"].lower() in set(self.editor_synonyms):
credit_editor_list.append(credit["person"].replace(",", ""))
if credit['role'].lower() in set(self.editor_synonyms):
credit_editor_list.append(credit['person'].replace(",", ""))
# second, convert each list to string, and add to XML struct
assign("Writer", utils.list_to_string(credit_writer_list))
if len(credit_writer_list) > 0:
node = ET.SubElement(root, 'Writer')
node.text = utils.listToString(credit_writer_list)
assign("Penciller", utils.list_to_string(credit_penciller_list))
if len(credit_penciller_list) > 0:
node = ET.SubElement(root, 'Penciller')
node.text = utils.listToString(credit_penciller_list)
assign("Inker", utils.list_to_string(credit_inker_list))
if len(credit_inker_list) > 0:
node = ET.SubElement(root, 'Inker')
node.text = utils.listToString(credit_inker_list)
assign("Colorist", utils.list_to_string(credit_colorist_list))
if len(credit_colorist_list) > 0:
node = ET.SubElement(root, 'Colorist')
node.text = utils.listToString(credit_colorist_list)
assign("Letterer", utils.list_to_string(credit_letterer_list))
if len(credit_letterer_list) > 0:
node = ET.SubElement(root, 'Letterer')
node.text = utils.listToString(credit_letterer_list)
assign("CoverArtist", utils.list_to_string(credit_cover_list))
if len(credit_cover_list) > 0:
node = ET.SubElement(root, 'CoverArtist')
node.text = utils.listToString(credit_cover_list)
assign("Editor", utils.list_to_string(credit_editor_list))
if len(credit_editor_list) > 0:
node = ET.SubElement(root, 'Editor')
node.text = utils.listToString(credit_editor_list)
assign("Publisher", md.publisher)
assign("Imprint", md.imprint)
assign("Genre", md.genre)
assign("Web", md.web_link)
assign("PageCount", md.page_count)
assign("LanguageISO", md.language)
assign("Format", md.format)
assign("AgeRating", md.maturity_rating)
assign("BlackAndWhite", "Yes" if md.black_and_white else None)
assign("Manga", md.manga)
assign("Characters", md.characters)
assign("Teams", md.teams)
assign("Locations", md.locations)
assign("ScanInformation", md.scan_info)
assign('Publisher', md.publisher)
assign('Imprint', md.imprint)
assign('Genre', md.genre)
assign('Web', md.webLink)
assign('PageCount', md.pageCount)
assign('LanguageISO', md.language)
assign('Format', md.format)
assign('AgeRating', md.maturityRating)
if md.blackAndWhite is not None and md.blackAndWhite:
ET.SubElement(root, 'BlackAndWhite').text = "Yes"
assign('Manga', md.manga)
assign('Characters', md.characters)
assign('Teams', md.teams)
assign('Locations', md.locations)
assign('ScanInformation', md.scanInfo)
# loop and add the page entries under pages node
pages_node = root.find("Pages")
if pages_node is not None:
pages_node.clear()
else:
pages_node = ET.SubElement(root, "Pages")
if len(md.pages) > 0:
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 = dict(sorted(page_dict.items()))
utils.indent(root)
# self pretty-print
self.indent(root)
# wrap it in an ElementTree instance, and save as XML
tree = ET.ElementTree(root)
return tree
def convert_xml_to_metadata(self, tree):
def convertXMLToMetadata(self, tree):
root = tree.getroot()
if root.tag != "ComicInfo":
raise "1"
if root.tag != 'ComicInfo':
raise 1
return None
def get(name):
tag = root.find(name)
@ -194,75 +216,75 @@ class ComicInfoXml:
md = GenericMetadata()
md.series = utils.xlate(get("Series"))
md.title = utils.xlate(get("Title"))
md.issue = IssueString(utils.xlate(get("Number"))).as_string()
md.issue_count = utils.xlate(get("Count"), True)
md.volume = utils.xlate(get("Volume"), True)
md.alternate_series = utils.xlate(get("AlternateSeries"))
md.alternate_number = IssueString(utils.xlate(get("AlternateNumber"))).as_string()
md.alternate_count = utils.xlate(get("AlternateCount"), True)
md.comments = utils.xlate(get("Summary"))
md.notes = utils.xlate(get("Notes"))
md.year = utils.xlate(get("Year"), True)
md.month = utils.xlate(get("Month"), True)
md.day = utils.xlate(get("Day"), True)
md.publisher = utils.xlate(get("Publisher"))
md.imprint = utils.xlate(get("Imprint"))
md.genre = utils.xlate(get("Genre"))
md.web_link = utils.xlate(get("Web"))
md.language = utils.xlate(get("LanguageISO"))
md.format = utils.xlate(get("Format"))
md.manga = utils.xlate(get("Manga"))
md.characters = utils.xlate(get("Characters"))
md.teams = utils.xlate(get("Teams"))
md.locations = utils.xlate(get("Locations"))
md.page_count = utils.xlate(get("PageCount"), True)
md.scan_info = utils.xlate(get("ScanInformation"))
md.story_arc = utils.xlate(get("StoryArc"))
md.series_group = utils.xlate(get("SeriesGroup"))
md.maturity_rating = utils.xlate(get("AgeRating"))
md.series = utils.xlate(get('Series'))
md.title = utils.xlate(get('Title'))
md.issue = IssueString(utils.xlate(get('Number'))).asString()
md.issueCount = utils.xlate(get('Count'), True)
md.volume = utils.xlate(get('Volume'), True)
md.alternateSeries = utils.xlate(get('AlternateSeries'))
md.alternateNumber = IssueString(utils.xlate(get('AlternateNumber'))).asString()
md.alternateCount = utils.xlate(get('AlternateCount'), True)
md.comments = utils.xlate(get('Summary'))
md.notes = utils.xlate(get('Notes'))
md.year = utils.xlate(get('Year'), True)
md.month = utils.xlate(get('Month'), True)
md.day = utils.xlate(get('Day'), True)
md.seriesYear = utils.xlate(get('SeriesYear'), True)
md.publisher = utils.xlate(get('Publisher'))
md.imprint = utils.xlate(get('Imprint'))
md.genre = utils.xlate(get('Genre'))
md.webLink = utils.xlate(get('Web'))
md.language = utils.xlate(get('LanguageISO'))
md.format = utils.xlate(get('Format'))
md.manga = utils.xlate(get('Manga'))
md.characters = utils.xlate(get('Characters'))
md.teams = utils.xlate(get('Teams'))
md.locations = utils.xlate(get('Locations'))
md.pageCount = utils.xlate(get('PageCount'), True)
md.scanInfo = utils.xlate(get('ScanInformation'))
md.storyArc = utils.xlate(get('StoryArc'))
md.seriesGroup = utils.xlate(get('SeriesGroup'))
md.maturityRating = utils.xlate(get('AgeRating'))
tmp = utils.xlate(get("BlackAndWhite"))
tmp = utils.xlate(get('BlackAndWhite'))
if tmp is not None and tmp.lower() in ["yes", "true", "1"]:
md.black_and_white = True
md.blackAndWhite = True
# Now extract the credit info
for n in root:
if any(
[
n.tag == "Writer",
n.tag == "Penciller",
n.tag == "Inker",
n.tag == "Colorist",
n.tag == "Letterer",
n.tag == "Editor",
]
):
if (n.tag == 'Writer' or
n.tag == 'Penciller' or
n.tag == 'Inker' or
n.tag == 'Colorist' or
n.tag == 'Letterer' or
n.tag == 'Editor'
):
if n.text is not None:
for name in n.text.split(","):
md.add_credit(name.strip(), n.tag)
for name in n.text.split(','):
md.addCredit(name.strip(), n.tag)
if n.tag == "CoverArtist":
if n.tag == 'CoverArtist':
if n.text is not None:
for name in n.text.split(","):
md.add_credit(name.strip(), "Cover")
for name in n.text.split(','):
md.addCredit(name.strip(), "Cover")
# parse page data now
pages_node = root.find("Pages")
if pages_node is not None:
for page in pages_node:
md.pages.append(page.attrib)
# print page.attrib
md.is_empty = False
md.isEmpty = False
return md
def write_to_external_file(self, filename, metadata, xml=None):
def writeToExternalFile(self, filename, metadata):
tree = self.convert_metadata_to_xml(self, metadata, xml)
tree.write(filename, encoding="utf-8", xml_declaration=True)
tree = self.convertMetadataToXML(self, metadata)
# ET.dump(tree)
tree.write(filename, encoding='utf-8')
def read_from_external_file(self, filename):
def readFromExternalFile(self, filename):
tree = ET.parse(filename)
return self.convert_xml_to_metadata(tree)
return self.convertXMLToMetadata(tree)

View File

@ -20,65 +20,56 @@ This should probably be re-written, but, well, it mostly works!
# Some portions of this code were modified from pyComicMetaThis project
# http://code.google.com/p/pycomicmetathis/
import logging
import os
import re
import os
from urllib.parse import unquote
logger = logging.getLogger(__name__)
class FileNameParser:
def __init__(self):
self.series = ""
self.volume = ""
self.year = ""
self.issue_count = ""
self.remainder = ""
self.issue = ""
def repl(self, m):
return " " * len(m.group())
return ' ' * len(m.group())
def fix_spaces(self, string, remove_dashes=True):
def fixSpaces(self, string, remove_dashes=True):
if remove_dashes:
placeholders = [r"[-_]", r" +"]
placeholders = ['[-_]', ' +']
else:
placeholders = [r"[_]", r" +"]
placeholders = ['[_]', ' +']
for ph in placeholders:
string = re.sub(ph, self.repl, string)
return string # .strip()
def get_issue_count(self, filename, issue_end):
def getIssueCount(self, filename, issue_end):
count = ""
filename = filename[issue_end:]
# replace any name separators with spaces
tmpstr = self.fix_spaces(filename)
tmpstr = self.fixSpaces(filename)
found = False
match = re.search(r"(?<=\sof\s)\d+(?=\s)", tmpstr, re.IGNORECASE)
match = re.search('(?<=\sof\s)\d+(?=\s)', tmpstr, re.IGNORECASE)
if match:
count = match.group()
found = True
if not found:
match = re.search(r"(?<=\(of\s)\d+(?=\))", tmpstr, re.IGNORECASE)
match = re.search('(?<=\(of\s)\d+(?=\))', tmpstr, re.IGNORECASE)
if match:
count = match.group()
found = True
count = count.lstrip("0")
return count
def get_issue_number(self, filename):
def getIssueNumber(self, filename):
"""Returns a tuple of issue number string, and start and end indexes in the filename
(The indexes will be used to split the string up for further parsing)
"""
found = False
issue = ""
issue = ''
start = 0
end = 0
@ -87,25 +78,25 @@ class FileNameParser:
if "--" in filename:
# the pattern seems to be that anything to left of the first "--"
# is the series name followed by issue
filename = re.sub(r"--.*", self.repl, filename)
filename = re.sub("--.*", 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
filename = re.sub(r"__.*", self.repl, filename)
filename = re.sub("__.*", self.repl, filename)
filename = filename.replace("+", " ")
# replace parenthetical phrases with spaces
filename = re.sub(r"\(.*?\)", self.repl, filename)
filename = re.sub(r"\[.*?]", self.repl, filename)
filename = re.sub("\(.*?\)", self.repl, filename)
filename = re.sub("\[.*?\]", self.repl, filename)
# replace any name separators with spaces
filename = self.fix_spaces(filename)
filename = self.fixSpaces(filename)
# remove any "of NN" phrase with spaces (problem: this could break on
# some titles)
filename = re.sub(r"of [\d]+", self.repl, filename)
filename = re.sub("of [\d]+", self.repl, filename)
# print u"[{0}]".format(filename)
@ -113,8 +104,8 @@ class FileNameParser:
# the same positions as original filename
# make a list of each word and its position
word_list = []
for m in re.finditer(r"\S+", filename):
word_list = list()
for m in re.finditer("\S+", filename):
word_list.append((m.group(0), m.start(), m.end()))
# remove the first word, since it can't be the issue number
@ -129,7 +120,7 @@ class FileNameParser:
# first look for a word with "#" followed by digits with optional suffix
# this is almost certainly the issue number
for w in reversed(word_list):
if re.match(r"#[-]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", w[0]):
if re.match("#[-]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", w[0]):
found = True
break
@ -137,13 +128,13 @@ class FileNameParser:
# list
if not found:
w = word_list[-1]
if re.match(r"[-]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", w[0]):
if re.match("[-]?(([0-9]*\.[0-9]+|[0-9]+)(\w*))", w[0]):
found = True
# now try to look for a # followed by any characters
if not found:
for w in reversed(word_list):
if re.match(r"#\S+", w[0]):
if re.match("#\S+", w[0]):
found = True
break
@ -151,12 +142,12 @@ class FileNameParser:
issue = w[0]
start = w[1]
end = w[2]
if issue[0] == "#":
if issue[0] == '#':
issue = issue[1:]
return issue, start, end
def get_series_name(self, filename, issue_start):
def getSeriesName(self, filename, issue_start):
"""Use the issue number string index to split the filename string"""
if issue_start != 0:
@ -166,15 +157,15 @@ class FileNameParser:
if "--" in filename:
# the pattern seems to be that anything to left of the first "--"
# is the series name followed by issue
filename = re.sub(r"--.*", self.repl, filename)
filename = re.sub("--.*", 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
filename = re.sub(r"__.*", self.repl, filename)
filename = re.sub("__.*", self.repl, filename)
filename = filename.replace("+", " ")
tmpstr = self.fix_spaces(filename, remove_dashes=False)
tmpstr = self.fixSpaces(filename, remove_dashes=False)
series = tmpstr
volume = ""
@ -186,10 +177,10 @@ class FileNameParser:
last_word = ""
# remove any parenthetical phrases
series = re.sub(r"\(.*?\)", "", series)
series = re.sub("\(.*?\)", "", series)
# search for volume number
match = re.search(r"(.+)([vV]|[Vv][oO][Ll]\.?\s?)(\d+)\s*$", series)
match = re.search('(.+)([vV]|[Vv][oO][Ll]\.?\s?)(\d+)\s*$', series)
if match:
series = match.group(1)
volume = match.group(3)
@ -198,7 +189,7 @@ class FileNameParser:
# since that's a common way to designate the volume
if volume == "":
# match either (YEAR), (YEAR-), or (YEAR-YEAR2)
match = re.search(r"(\()(\d{4})(-(\d{4}|)|)(\))", last_word)
match = re.search("(\()(\d{4})(-(\d{4}|)|)(\))", last_word)
if match:
volume = match.group(2)
@ -212,26 +203,26 @@ class FileNameParser:
try:
last_word = series.split()[-1]
if last_word.lower() in one_shot_words:
series = series.rsplit(" ", 1)[0]
series = series.rsplit(' ', 1)[0]
except:
pass
return series, volume.strip()
def get_year(self, filename, issue_end):
def getYear(self, filename, issue_end):
filename = filename[issue_end:]
year = ""
# look for four digit number with "(" ")" or "--" around it
match = re.search(r"(\(\d\d\d\d\))|(--\d\d\d\d--)", filename)
match = re.search('(\(\d\d\d\d\))|(--\d\d\d\d--)', filename)
if match:
year = match.group()
# remove non-digits
year = re.sub(r"[^0-9]", "", year)
year = re.sub("[^0-9]", "", year)
return year
def get_remainder(self, filename, year, count, volume, issue_end):
def getRemainder(self, filename, year, count, volume, issue_end):
"""Make a guess at where the the non-interesting stuff begins"""
remainder = ""
@ -243,7 +234,7 @@ class FileNameParser:
elif issue_end != 0:
remainder = filename[issue_end:]
remainder = self.fix_spaces(remainder, remove_dashes=False)
remainder = self.fixSpaces(remainder, remove_dashes=False)
if volume != "":
remainder = remainder.replace("Vol." + volume, "", 1)
if year != "":
@ -252,11 +243,13 @@ class FileNameParser:
remainder = remainder.replace("of " + count, "", 1)
remainder = remainder.replace("()", "")
remainder = remainder.replace(" ", " ") # cleans some whitespace mess
remainder = remainder.replace(
" ",
" ") # cleans some whitespace mess
return remainder.strip()
def parse_filename(self, filename):
def parseFilename(self, filename):
# remove the path
filename = os.path.basename(filename)
@ -274,16 +267,21 @@ class FileNameParser:
filename = filename.replace("_28", "(")
filename = filename.replace("_29", ")")
self.issue, issue_start, issue_end = self.get_issue_number(filename)
self.series, self.volume = self.get_series_name(filename, issue_start)
self.issue, issue_start, issue_end = self.getIssueNumber(filename)
self.series, self.volume = self.getSeriesName(filename, issue_start)
# provides proper value when the filename doesn't have a issue number
if issue_end == 0:
issue_end = len(self.series)
self.year = self.get_year(filename, issue_end)
self.issue_count = self.get_issue_count(filename, issue_end)
self.remainder = self.get_remainder(filename, self.year, self.issue_count, self.volume, issue_end)
self.year = self.getYear(filename, issue_end)
self.issue_count = self.getIssueCount(filename, issue_end)
self.remainder = self.getRemainder(
filename,
self.year,
self.issue_count,
self.volume,
issue_end)
if self.issue != "":
# strip off leading zeros

View File

@ -20,12 +20,7 @@ possible, however lossy it might be
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from typing import List, TypedDict
from comicapi import utils
logger = logging.getLogger(__name__)
from . import utils
class PageType:
@ -47,83 +42,74 @@ class PageType:
Other = "Other"
Deleted = "Deleted"
class ImageMetadata(TypedDict):
Type: PageType
Image: int
ImageSize: str
ImageHeight: str
ImageWidth: str
class CreditMetadata(TypedDict):
person: str
role: str
primary: bool
"""
class PageInfo:
Image = 0
Type = PageType.Story
DoublePage = False
ImageSize = 0
Key = ""
ImageWidth = 0
ImageHeight = 0
"""
class GenericMetadata:
writer_synonyms = ["writer", "plotter", "scripter"]
penciller_synonyms = ["artist", "penciller", "penciler", "breakdowns"]
inker_synonyms = ["inker", "artist", "finishes"]
colorist_synonyms = ["colorist", "colourist", "colorer", "colourer"]
letterer_synonyms = ["letterer"]
cover_synonyms = ["cover", "covers", "coverartist", "cover artist"]
editor_synonyms = ["editor"]
def __init__(self):
self.is_empty = True
self.tag_origin = None
self.isEmpty = True
self.tagOrigin = None
self.series = None
self.issue = None
self.title = None
self.publisher = None
self.seriesYear = None
self.month = None
self.year = None
self.day = None
self.issue_count = None
self.issueCount = None
self.volume = None
self.genre = None
self.language = None # 2 letter iso code
self.comments = None # use same way as Summary in CIX
self.volume_count = None
self.critical_rating = None
self.volumeCount = None
self.criticalRating = None
self.country = None
self.alternate_series = None
self.alternate_number = None
self.alternate_count = None
self.alternateSeries = None
self.alternateNumber = None
self.alternateCount = None
self.imprint = None
self.notes = None
self.web_link = None
self.webLink = None
self.format = None
self.manga = None
self.black_and_white = None
self.page_count = None
self.maturity_rating = None
self.blackAndWhite = None
self.pageCount = None
self.maturityRating = None
self.story_arc = None
self.series_group = None
self.scan_info = None
self.storyArc = None
self.seriesGroup = None
self.scanInfo = None
self.characters = None
self.teams = None
self.locations = None
self.credits: List[CreditMetadata] = []
self.tags: List[str] = []
self.pages: List[ImageMetadata] = []
self.credits = list()
self.tags = list()
self.pages = list()
# Some CoMet-only items
self.price = None
self.is_version_of = None
self.isVersionOf = None
self.rights = None
self.identifier = None
self.last_mark = None
self.cover_image = None
self.lastMark = None
self.coverImage = None
def overlay(self, new_md):
"""Overlay a metadata object on this one
@ -139,36 +125,36 @@ class GenericMetadata:
else:
setattr(self, cur, new)
new_md: GenericMetadata
if not new_md.is_empty:
self.is_empty = False
if not new_md.isEmpty:
self.isEmpty = False
assign("series", new_md.series)
assign('series', new_md.series)
assign("issue", new_md.issue)
assign("issue_count", new_md.issue_count)
assign("issueCount", new_md.issueCount)
assign("title", new_md.title)
assign("publisher", new_md.publisher)
assign("seriesYear", new_md.seriesYear)
assign("day", new_md.day)
assign("month", new_md.month)
assign("year", new_md.year)
assign("volume", new_md.volume)
assign("volume_count", new_md.volume_count)
assign("volumeCount", new_md.volumeCount)
assign("genre", new_md.genre)
assign("language", new_md.language)
assign("country", new_md.country)
assign("critical_rating", new_md.critical_rating)
assign("alternate_series", new_md.alternate_series)
assign("alternate_number", new_md.alternate_number)
assign("alternate_count", new_md.alternate_count)
assign("criticalRating", new_md.criticalRating)
assign("alternateSeries", new_md.alternateSeries)
assign("alternateNumber", new_md.alternateNumber)
assign("alternateCount", new_md.alternateCount)
assign("imprint", new_md.imprint)
assign("web_link", new_md.web_link)
assign("webLink", new_md.webLink)
assign("format", new_md.format)
assign("manga", new_md.manga)
assign("black_and_white", new_md.black_and_white)
assign("maturity_rating", new_md.maturity_rating)
assign("story_arc", new_md.story_arc)
assign("series_group", new_md.series_group)
assign("scan_info", new_md.scan_info)
assign("blackAndWhite", new_md.blackAndWhite)
assign("maturityRating", new_md.maturityRating)
assign("storyArc", new_md.storyArc)
assign("seriesGroup", new_md.seriesGroup)
assign("scanInfo", new_md.scanInfo)
assign("characters", new_md.characters)
assign("teams", new_md.teams)
assign("locations", new_md.locations)
@ -176,12 +162,12 @@ class GenericMetadata:
assign("notes", new_md.notes)
assign("price", new_md.price)
assign("is_version_of", new_md.is_version_of)
assign("isVersionOf", new_md.isVersionOf)
assign("rights", new_md.rights)
assign("identifier", new_md.identifier)
assign("last_mark", new_md.last_mark)
assign("lastMark", new_md.lastMark)
self.overlay_credits(new_md.credits)
self.overlayCredits(new_md.credits)
# TODO
# not sure if the tags and pages should broken down, or treated
@ -195,62 +181,66 @@ class GenericMetadata:
if len(new_md.pages) > 0:
assign("pages", new_md.pages)
def overlay_credits(self, new_credits):
def overlayCredits(self, new_credits):
for c in new_credits:
primary = bool("primary" in c and c["primary"])
if 'primary' in c and c['primary']:
primary = True
else:
primary = False
# Remove credit role if person is blank
if c["person"] == "":
if c['person'] == "":
for r in reversed(self.credits):
if r["role"].lower() == c["role"].lower():
if r['role'].lower() == c['role'].lower():
self.credits.remove(r)
# otherwise, add it!
else:
self.add_credit(c["person"], c["role"], primary)
self.addCredit(c['person'], c['role'], primary)
def set_default_page_list(self, count):
def setDefaultPageList(self, count):
# generate a default page list, with the first page marked as the cover
for i in range(count):
page_dict = {}
page_dict["Image"] = str(i)
page_dict = dict()
page_dict['Image'] = str(i)
if i == 0:
page_dict["Type"] = PageType.FrontCover
page_dict['Type'] = PageType.FrontCover
self.pages.append(page_dict)
def get_archive_page_index(self, pagenum):
def getArchivePageIndex(self, pagenum):
# 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"])
return int(self.pages[pagenum]['Image'])
else:
return 0
return 0
def get_cover_page_index_list(self):
def getCoverPageIndexList(self):
# return a list of archive page indices of cover pages
coverlist = []
for p in self.pages:
if "Type" in p and p["Type"] == PageType.FrontCover:
coverlist.append(int(p["Image"]))
if 'Type' in p and p['Type'] == PageType.FrontCover:
coverlist.append(int(p['Image']))
if len(coverlist) == 0:
coverlist.append(0)
return coverlist
def add_credit(self, person, role, primary=False):
def addCredit(self, person, role, primary=False):
credit = {}
credit["person"] = person
credit["role"] = role
credit = dict()
credit['person'] = person
credit['role'] = role
if primary:
credit["primary"] = primary
credit['primary'] = primary
# look to see if it's not already there...
found = False
for c in self.credits:
if c["person"].lower() == person.lower() and c["role"].lower() == role.lower():
if (c['person'].lower() == person.lower() and
c['role'].lower() == role.lower()):
# no need to add it. just adjust the "primary" flag as needed
c["primary"] = primary
c['primary'] = primary
found = True
break
@ -259,63 +249,65 @@ class GenericMetadata:
def __str__(self):
vals = []
if self.is_empty:
if self.isEmpty:
return "No metadata"
def add_string(tag, val):
if val is not None and str(val) != "":
if val is not None and "{0}".format(val) != "":
vals.append((tag, val))
def add_attr_string(tag):
val = getattr(self, tag)
add_string(tag, getattr(self, tag))
add_attr_string("series")
add_attr_string("issue")
add_attr_string("issue_count")
add_attr_string("issueCount")
add_attr_string("title")
add_attr_string("publisher")
add_attr_string("seriesYear")
add_attr_string("year")
add_attr_string("month")
add_attr_string("day")
add_attr_string("volume")
add_attr_string("volume_count")
add_attr_string("volumeCount")
add_attr_string("genre")
add_attr_string("language")
add_attr_string("country")
add_attr_string("critical_rating")
add_attr_string("alternate_series")
add_attr_string("alternate_number")
add_attr_string("alternate_count")
add_attr_string("criticalRating")
add_attr_string("alternateSeries")
add_attr_string("alternateNumber")
add_attr_string("alternateCount")
add_attr_string("imprint")
add_attr_string("web_link")
add_attr_string("webLink")
add_attr_string("format")
add_attr_string("manga")
add_attr_string("price")
add_attr_string("is_version_of")
add_attr_string("isVersionOf")
add_attr_string("rights")
add_attr_string("identifier")
add_attr_string("last_mark")
add_attr_string("lastMark")
if self.black_and_white:
add_attr_string("black_and_white")
add_attr_string("maturity_rating")
add_attr_string("story_arc")
add_attr_string("series_group")
add_attr_string("scan_info")
if self.blackAndWhite:
add_attr_string("blackAndWhite")
add_attr_string("maturityRating")
add_attr_string("storyArc")
add_attr_string("seriesGroup")
add_attr_string("scanInfo")
add_attr_string("characters")
add_attr_string("teams")
add_attr_string("locations")
add_attr_string("comments")
add_attr_string("notes")
add_string("tags", utils.list_to_string(self.tags))
add_string("tags", utils.listToString(self.tags))
for c in self.credits:
primary = ""
if "primary" in c and c["primary"]:
if 'primary' in c and c['primary']:
primary = " [P]"
add_string("credit", c["role"] + ": " + c["person"] + primary)
add_string("credit", c['role'] + ": " + c['person'] + primary)
# find the longest field name
flen = 0

View File

@ -1,3 +1,4 @@
# coding=utf-8
"""Support for mixed digit/string type Issue field
Class for handling the odd permutations of an 'issue number' that the
@ -19,13 +20,13 @@ comics industry throws at us.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
logger = logging.getLogger(__name__)
#import utils
#import math
#import re
class IssueString:
def __init__(self, text):
# break up the issue number string into 2 parts: the numeric and suffix string.
@ -37,13 +38,16 @@ class IssueString:
if text is None:
return
text = str(text)
if isinstance(text, int):
text = str(text)
if len(text) == 0:
return
text = str(text)
# skip the minus sign if it's first
if text[0] == "-":
if text[0] == '-':
start = 1
else:
start = 0
@ -75,7 +79,7 @@ class IssueString:
idx = 0
part1 = text[0:idx]
part2 = text[idx : len(text)]
part2 = text[idx:len(text)]
if part1 != "":
self.num = float(part1)
@ -83,7 +87,9 @@ class IssueString:
else:
self.suffix = text
def as_string(self, pad=0):
# print "num: {0} suf: {1}".format(self.num, self.suffix)
def asString(self, pad=0):
# return the float, left side zero-padded, with suffix attached
if self.num is None:
return self.suffix
@ -101,9 +107,9 @@ class IssueString:
# create padding
padding = ""
length = len(str(num_int))
if length < pad:
padding = "0" * (pad - length)
l = len(str(num_int))
if l < pad:
padding = "0" * (pad - l)
num_s = padding + num_s
if negative:
@ -111,16 +117,16 @@ class IssueString:
return num_s
def as_float(self):
def asFloat(self):
# return the float, with no suffix
if self.suffix == "½":
if self.num is not None:
return self.num + 0.5
return 0.5
return self.num + .5
else:
return .5
return self.num
def as_int(self):
def asInt(self):
# return the int version of the float
if self.num is None:
return None

View File

@ -1,3 +1,4 @@
# coding=utf-8
"""Some generic utilities"""
# Copyright 2012-2014 Anthony Beville
@ -14,42 +15,19 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import codecs
import locale
import logging
import os
import platform
import re
import sys
import os
import re
import platform
import locale
import codecs
import unicodedata
from collections import defaultdict
import pycountry
logger = logging.getLogger(__name__)
class UtilsVars:
already_fixed_encoding = False
def indent(elem, level=0):
# for making the XML output readable
i = "\n" + level * " "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
if not elem.tail or not elem.tail.strip():
elem.tail = i
for ele in elem:
indent(ele, level + 1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
def get_actual_preferred_encoding():
preferred_encoding = locale.getpreferredencoding()
if platform.system() == "Darwin":
@ -72,17 +50,26 @@ def fix_output_encoding():
def get_recursive_filelist(pathlist):
"""Get a recursive list of of all files under all path items in the list"""
filename_encoding = sys.getfilesystemencoding()
filelist = []
for p in pathlist:
# if path is a folder, walk it recursively, and all files underneath
if not isinstance(p, str):
if isinstance(p, str):
# make sure string is unicode
#p = p.decode(filename_encoding) # , 'replace')
pass
elif not isinstance(p, str):
# it's probably a QString
p = str(p)
if os.path.isdir(p):
for root, _, files in os.walk(p):
for root, dirs, files in os.walk(p):
for f in files:
if not isinstance(f, str):
if isinstance(f, str):
# make sure string is unicode
#f = f.decode(filename_encoding, 'replace')
pass
elif not isinstance(f, str):
# it's probably a QString
f = str(f)
filelist.append(os.path.join(root, f))
@ -92,26 +79,28 @@ def get_recursive_filelist(pathlist):
return filelist
def list_to_string(lst):
def listToString(l):
string = ""
if lst is not None:
for item in lst:
if l is not None:
for item in l:
if len(string) > 0:
string += ", "
string += item
return string
def add_to_path(dirname):
def addtopath(dirname):
if dirname is not None and dirname != "":
# verify that path doesn't already contain the given dirname
tmpdirname = re.escape(dirname)
pattern = r"(^|{sep}){dir}({sep}|$)".format(dir=tmpdirname, sep=os.pathsep)
pattern = r"{sep}{dir}$|^{dir}{sep}|{sep}{dir}{sep}|^{dir}$".format(
dir=tmpdirname,
sep=os.pathsep)
match = re.search(pattern, os.environ["PATH"])
match = re.search(pattern, os.environ['PATH'])
if not match:
os.environ["PATH"] = dirname + os.pathsep + os.environ["PATH"]
os.environ['PATH'] = dirname + os.pathsep + os.environ['PATH']
def which(program):
@ -120,7 +109,7 @@ def which(program):
def is_exe(fpath):
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
fpath, _ = os.path.split(program)
fpath, fname = os.path.split(program)
if fpath:
if is_exe(program):
return program
@ -133,109 +122,496 @@ def which(program):
return None
def xlate(data, is_int=False):
def xlate(data, isInt=False):
class Default(dict):
def __missing__(self, key):
return None
if data is None or data == "":
return None
if is_int:
i = str(data).translate(defaultdict(lambda: None, zip((ord(c) for c in "1234567890"), "1234567890")))
if isInt:
i = str(data).translate(Default(zip((ord(c) for c in "1234567890"),"1234567890")))
if i == "0":
return "0"
if i == "":
if i is "":
return None
return int(i)
return str(data)
else:
return str(data)
def remove_articles(text):
def removearticles(text):
text = text.lower()
articles = [
"&",
"a",
"am",
"an",
"and",
"as",
"at",
"be",
"but",
"by",
"for",
"if",
"is",
"issue",
"it",
"it's",
"its",
"itself",
"of",
"or",
"so",
"the",
"the",
"with",
]
new_text = ""
for word in text.split(" "):
articles = ['and', 'a', '&', 'issue', 'the']
newText = ''
for word in text.split(' '):
if word not in articles:
new_text += word + " "
newText += word + ' '
new_text = new_text[:-1]
newText = newText[:-1]
return new_text
return newText
def sanitize_title(text):
# normalize unicode and convert to ascii. Does not work for everything eg ½ to 12 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")
text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode('ascii')
# comicvine keeps apostrophes a part of the word
text = text.replace("'", "")
text = text.replace('"', "")
text = text.replace("\"", "")
# comicvine ignores punctuation and accents
text = re.sub(r"[^A-Za-z0-9]+", " ", text)
text = re.sub(r'[^A-Za-z0-9]+',' ', text)
# remove extra space and articles and all lower case
text = remove_articles(text).lower().strip()
text = removearticles(text).lower().strip()
return text
def unique_file(file_name):
counter = 1
# returns ('/path/file', '.ext')
file_name_parts = os.path.splitext(file_name)
while True:
if not os.path.lexists(file_name):
return file_name
file_name = file_name_parts[0] + " (" + str(counter) + ")" + file_name_parts[1]
file_name = file_name_parts[
0] + ' (' + str(counter) + ')' + file_name_parts[1]
counter += 1
languages = defaultdict(lambda: None)
# -o- coding: utf-8 -o-
# ISO639 python dict
# official list in http://www.loc.gov/standards/iso639-2/php/code_list.php
countries = defaultdict(lambda: None)
for c in pycountry.countries:
if "alpha_2" in c._fields:
countries[c.alpha_2] = c.name
for lng in pycountry.languages:
if "alpha_2" in lng._fields:
languages[lng.alpha_2] = lng.name
lang_dict = {
'ab': 'Abkhaz',
'aa': 'Afar',
'af': 'Afrikaans',
'ak': 'Akan',
'sq': 'Albanian',
'am': 'Amharic',
'ar': 'Arabic',
'an': 'Aragonese',
'hy': 'Armenian',
'as': 'Assamese',
'av': 'Avaric',
'ae': 'Avestan',
'ay': 'Aymara',
'az': 'Azerbaijani',
'bm': 'Bambara',
'ba': 'Bashkir',
'eu': 'Basque',
'be': 'Belarusian',
'bn': 'Bengali',
'bh': 'Bihari',
'bi': 'Bislama',
'bs': 'Bosnian',
'br': 'Breton',
'bg': 'Bulgarian',
'my': 'Burmese',
'ca': 'Catalan; Valencian',
'ch': 'Chamorro',
'ce': 'Chechen',
'ny': 'Chichewa; Chewa; Nyanja',
'zh': 'Chinese',
'cv': 'Chuvash',
'kw': 'Cornish',
'co': 'Corsican',
'cr': 'Cree',
'hr': 'Croatian',
'cs': 'Czech',
'da': 'Danish',
'dv': 'Divehi; Maldivian;',
'nl': 'Dutch',
'dz': 'Dzongkha',
'en': 'English',
'eo': 'Esperanto',
'et': 'Estonian',
'ee': 'Ewe',
'fo': 'Faroese',
'fj': 'Fijian',
'fi': 'Finnish',
'fr': 'French',
'ff': 'Fula',
'gl': 'Galician',
'ka': 'Georgian',
'de': 'German',
'el': 'Greek, Modern',
'gn': 'Guaraní',
'gu': 'Gujarati',
'ht': 'Haitian',
'ha': 'Hausa',
'he': 'Hebrew (modern)',
'hz': 'Herero',
'hi': 'Hindi',
'ho': 'Hiri Motu',
'hu': 'Hungarian',
'ia': 'Interlingua',
'id': 'Indonesian',
'ie': 'Interlingue',
'ga': 'Irish',
'ig': 'Igbo',
'ik': 'Inupiaq',
'io': 'Ido',
'is': 'Icelandic',
'it': 'Italian',
'iu': 'Inuktitut',
'ja': 'Japanese',
'jv': 'Javanese',
'kl': 'Kalaallisut',
'kn': 'Kannada',
'kr': 'Kanuri',
'ks': 'Kashmiri',
'kk': 'Kazakh',
'km': 'Khmer',
'ki': 'Kikuyu, Gikuyu',
'rw': 'Kinyarwanda',
'ky': 'Kirghiz, Kyrgyz',
'kv': 'Komi',
'kg': 'Kongo',
'ko': 'Korean',
'ku': 'Kurdish',
'kj': 'Kwanyama, Kuanyama',
'la': 'Latin',
'lb': 'Luxembourgish',
'lg': 'Luganda',
'li': 'Limburgish',
'ln': 'Lingala',
'lo': 'Lao',
'lt': 'Lithuanian',
'lu': 'Luba-Katanga',
'lv': 'Latvian',
'gv': 'Manx',
'mk': 'Macedonian',
'mg': 'Malagasy',
'ms': 'Malay',
'ml': 'Malayalam',
'mt': 'Maltese',
'mi': 'Māori',
'mr': 'Marathi (Marāṭhī)',
'mh': 'Marshallese',
'mn': 'Mongolian',
'na': 'Nauru',
'nv': 'Navajo, Navaho',
'nb': 'Norwegian Bokmål',
'nd': 'North Ndebele',
'ne': 'Nepali',
'ng': 'Ndonga',
'nn': 'Norwegian Nynorsk',
'no': 'Norwegian',
'ii': 'Nuosu',
'nr': 'South Ndebele',
'oc': 'Occitan',
'oj': 'Ojibwe, Ojibwa',
'cu': 'Old Church Slavonic',
'om': 'Oromo',
'or': 'Oriya',
'os': 'Ossetian, Ossetic',
'pa': 'Panjabi, Punjabi',
'pi': 'Pāli',
'fa': 'Persian',
'pl': 'Polish',
'ps': 'Pashto, Pushto',
'pt': 'Portuguese',
'qu': 'Quechua',
'rm': 'Romansh',
'rn': 'Kirundi',
'ro': 'Romanian, Moldavan',
'ru': 'Russian',
'sa': 'Sanskrit (Saṁskṛta)',
'sc': 'Sardinian',
'sd': 'Sindhi',
'se': 'Northern Sami',
'sm': 'Samoan',
'sg': 'Sango',
'sr': 'Serbian',
'gd': 'Scottish Gaelic',
'sn': 'Shona',
'si': 'Sinhala, Sinhalese',
'sk': 'Slovak',
'sl': 'Slovene',
'so': 'Somali',
'st': 'Southern Sotho',
'es': 'Spanish; Castilian',
'su': 'Sundanese',
'sw': 'Swahili',
'ss': 'Swati',
'sv': 'Swedish',
'ta': 'Tamil',
'te': 'Telugu',
'tg': 'Tajik',
'th': 'Thai',
'ti': 'Tigrinya',
'bo': 'Tibetan',
'tk': 'Turkmen',
'tl': 'Tagalog',
'tn': 'Tswana',
'to': 'Tonga',
'tr': 'Turkish',
'ts': 'Tsonga',
'tt': 'Tatar',
'tw': 'Twi',
'ty': 'Tahitian',
'ug': 'Uighur, Uyghur',
'uk': 'Ukrainian',
'ur': 'Urdu',
'uz': 'Uzbek',
've': 'Venda',
'vi': 'Vietnamese',
'vo': 'Volapük',
'wa': 'Walloon',
'cy': 'Welsh',
'wo': 'Wolof',
'fy': 'Western Frisian',
'xh': 'Xhosa',
'yi': 'Yiddish',
'yo': 'Yoruba',
'za': 'Zhuang, Chuang',
'zu': 'Zulu',
}
def get_language_from_iso(iso: str):
return languages[iso]
countries = [
('AF', 'Afghanistan'),
('AL', 'Albania'),
('DZ', 'Algeria'),
('AS', 'American Samoa'),
('AD', 'Andorra'),
('AO', 'Angola'),
('AI', 'Anguilla'),
('AQ', 'Antarctica'),
('AG', 'Antigua And Barbuda'),
('AR', 'Argentina'),
('AM', 'Armenia'),
('AW', 'Aruba'),
('AU', 'Australia'),
('AT', 'Austria'),
('AZ', 'Azerbaijan'),
('BS', 'Bahamas'),
('BH', 'Bahrain'),
('BD', 'Bangladesh'),
('BB', 'Barbados'),
('BY', 'Belarus'),
('BE', 'Belgium'),
('BZ', 'Belize'),
('BJ', 'Benin'),
('BM', 'Bermuda'),
('BT', 'Bhutan'),
('BO', 'Bolivia'),
('BA', 'Bosnia And Herzegowina'),
('BW', 'Botswana'),
('BV', 'Bouvet Island'),
('BR', 'Brazil'),
('BN', 'Brunei Darussalam'),
('BG', 'Bulgaria'),
('BF', 'Burkina Faso'),
('BI', 'Burundi'),
('KH', 'Cambodia'),
('CM', 'Cameroon'),
('CA', 'Canada'),
('CV', 'Cape Verde'),
('KY', 'Cayman Islands'),
('CF', 'Central African Rep'),
('TD', 'Chad'),
('CL', 'Chile'),
('CN', 'China'),
('CX', 'Christmas Island'),
('CC', 'Cocos Islands'),
('CO', 'Colombia'),
('KM', 'Comoros'),
('CG', 'Congo'),
('CK', 'Cook Islands'),
('CR', 'Costa Rica'),
('CI', 'Cote D`ivoire'),
('HR', 'Croatia'),
('CU', 'Cuba'),
('CY', 'Cyprus'),
('CZ', 'Czech Republic'),
('DK', 'Denmark'),
('DJ', 'Djibouti'),
('DM', 'Dominica'),
('DO', 'Dominican Republic'),
('TP', 'East Timor'),
('EC', 'Ecuador'),
('EG', 'Egypt'),
('SV', 'El Salvador'),
('GQ', 'Equatorial Guinea'),
('ER', 'Eritrea'),
('EE', 'Estonia'),
('ET', 'Ethiopia'),
('FK', 'Falkland Islands (Malvinas)'),
('FO', 'Faroe Islands'),
('FJ', 'Fiji'),
('FI', 'Finland'),
('FR', 'France'),
('GF', 'French Guiana'),
('PF', 'French Polynesia'),
('TF', 'French S. Territories'),
('GA', 'Gabon'),
('GM', 'Gambia'),
('GE', 'Georgia'),
('DE', 'Germany'),
('GH', 'Ghana'),
('GI', 'Gibraltar'),
('GR', 'Greece'),
('GL', 'Greenland'),
('GD', 'Grenada'),
('GP', 'Guadeloupe'),
('GU', 'Guam'),
('GT', 'Guatemala'),
('GN', 'Guinea'),
('GW', 'Guinea-bissau'),
('GY', 'Guyana'),
('HT', 'Haiti'),
('HN', 'Honduras'),
('HK', 'Hong Kong'),
('HU', 'Hungary'),
('IS', 'Iceland'),
('IN', 'India'),
('ID', 'Indonesia'),
('IR', 'Iran'),
('IQ', 'Iraq'),
('IE', 'Ireland'),
('IL', 'Israel'),
('IT', 'Italy'),
('JM', 'Jamaica'),
('JP', 'Japan'),
('JO', 'Jordan'),
('KZ', 'Kazakhstan'),
('KE', 'Kenya'),
('KI', 'Kiribati'),
('KP', 'Korea (North)'),
('KR', 'Korea (South)'),
('KW', 'Kuwait'),
('KG', 'Kyrgyzstan'),
('LA', 'Laos'),
('LV', 'Latvia'),
('LB', 'Lebanon'),
('LS', 'Lesotho'),
('LR', 'Liberia'),
('LY', 'Libya'),
('LI', 'Liechtenstein'),
('LT', 'Lithuania'),
('LU', 'Luxembourg'),
('MO', 'Macau'),
('MK', 'Macedonia'),
('MG', 'Madagascar'),
('MW', 'Malawi'),
('MY', 'Malaysia'),
('MV', 'Maldives'),
('ML', 'Mali'),
('MT', 'Malta'),
('MH', 'Marshall Islands'),
('MQ', 'Martinique'),
('MR', 'Mauritania'),
('MU', 'Mauritius'),
('YT', 'Mayotte'),
('MX', 'Mexico'),
('FM', 'Micronesia'),
('MD', 'Moldova'),
('MC', 'Monaco'),
('MN', 'Mongolia'),
('MS', 'Montserrat'),
('MA', 'Morocco'),
('MZ', 'Mozambique'),
('MM', 'Myanmar'),
('NA', 'Namibia'),
('NR', 'Nauru'),
('NP', 'Nepal'),
('NL', 'Netherlands'),
('AN', 'Netherlands Antilles'),
('NC', 'New Caledonia'),
('NZ', 'New Zealand'),
('NI', 'Nicaragua'),
('NE', 'Niger'),
('NG', 'Nigeria'),
('NU', 'Niue'),
('NF', 'Norfolk Island'),
('MP', 'Northern Mariana Islands'),
('NO', 'Norway'),
('OM', 'Oman'),
('PK', 'Pakistan'),
('PW', 'Palau'),
('PA', 'Panama'),
('PG', 'Papua New Guinea'),
('PY', 'Paraguay'),
('PE', 'Peru'),
('PH', 'Philippines'),
('PN', 'Pitcairn'),
('PL', 'Poland'),
('PT', 'Portugal'),
('PR', 'Puerto Rico'),
('QA', 'Qatar'),
('RE', 'Reunion'),
('RO', 'Romania'),
('RU', 'Russian Federation'),
('RW', 'Rwanda'),
('KN', 'Saint Kitts And Nevis'),
('LC', 'Saint Lucia'),
('VC', 'St Vincent/Grenadines'),
('WS', 'Samoa'),
('SM', 'San Marino'),
('ST', 'Sao Tome'),
('SA', 'Saudi Arabia'),
('SN', 'Senegal'),
('SC', 'Seychelles'),
('SL', 'Sierra Leone'),
('SG', 'Singapore'),
('SK', 'Slovakia'),
('SI', 'Slovenia'),
('SB', 'Solomon Islands'),
('SO', 'Somalia'),
('ZA', 'South Africa'),
('ES', 'Spain'),
('LK', 'Sri Lanka'),
('SH', 'St. Helena'),
('PM', 'St.Pierre'),
('SD', 'Sudan'),
('SR', 'Suriname'),
('SZ', 'Swaziland'),
('SE', 'Sweden'),
('CH', 'Switzerland'),
('SY', 'Syrian Arab Republic'),
('TW', 'Taiwan'),
('TJ', 'Tajikistan'),
('TZ', 'Tanzania'),
('TH', 'Thailand'),
('TG', 'Togo'),
('TK', 'Tokelau'),
('TO', 'Tonga'),
('TT', 'Trinidad And Tobago'),
('TN', 'Tunisia'),
('TR', 'Turkey'),
('TM', 'Turkmenistan'),
('TV', 'Tuvalu'),
('UG', 'Uganda'),
('UA', 'Ukraine'),
('AE', 'United Arab Emirates'),
('UK', 'United Kingdom'),
('US', 'United States'),
('UY', 'Uruguay'),
('UZ', 'Uzbekistan'),
('VU', 'Vanuatu'),
('VA', 'Vatican City State'),
('VE', 'Venezuela'),
('VN', 'Viet Nam'),
('VG', 'Virgin Islands (British)'),
('VI', 'Virgin Islands (U.S.)'),
('EH', 'Western Sahara'),
('YE', 'Yemen'),
('YU', 'Yugoslavia'),
('ZR', 'Zaire'),
('ZM', 'Zambia'),
('ZW', 'Zimbabwe')
]
def get_language(string):
if string is None:
def getLanguageDict():
return lang_dict
def getLanguageFromISO(iso):
if iso is None:
return None
lang = get_language_from_iso(string)
if lang is None:
try:
return pycountry.languages.lookup(string).name
except:
return None
return lang
else:
return lang_dict[iso]

View File

@ -1,7 +1,5 @@
#!/usr/bin/env python3
import localefix
from comictaggerlib.main import ctmain
if __name__ == "__main__":
localefix.configure_locale()
if __name__ == '__main__':
ctmain()

View File

@ -14,55 +14,55 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import os
from typing import List, Optional
#import sys
from PyQt5 import QtCore, QtGui, QtWidgets, uic
#from PyQt5.QtCore import QUrl, pyqtSignal, QByteArray
from comicapi.comicarchive import MetaDataStyle
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.resulttypes import MultipleMatch
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.ui.qtutils import reduce_widget_font_size
logger = logging.getLogger(__name__)
from .settings import ComicTaggerSettings
from .comicarchive import MetaDataStyle
from .coverimagewidget import CoverImageWidget
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
#from imagefetcher import ImageFetcher
#from comicvinetalker import ComicVineTalker
#import utils
class AutoTagMatchWindow(QtWidgets.QDialog):
volume_id = 0
def __init__(self, parent, match_set_list: List[MultipleMatch], style, fetch_func):
super().__init__(parent)
def __init__(self, parent, match_set_list, style, fetch_func):
super(AutoTagMatchWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.get_ui_file("matchselectionwindow.ui"), self)
uic.loadUi(
ComicTaggerSettings.getUIFile('matchselectionwindow.ui'), self)
self.current_match_set: Optional[MultipleMatch] = None
self.altCoverWidget = CoverImageWidget(self.altCoverContainer, CoverImageWidget.AltCoverMode)
self.altCoverWidget = CoverImageWidget(
self.altCoverContainer, CoverImageWidget.AltCoverMode)
gridlayout = QtWidgets.QGridLayout(self.altCoverContainer)
gridlayout.addWidget(self.altCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, CoverImageWidget.ArchiveMode)
self.archiveCoverWidget = CoverImageWidget(
self.archiveCoverContainer, CoverImageWidget.ArchiveMode)
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
gridlayout.addWidget(self.archiveCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
reduce_widget_font_size(self.twList)
reduce_widget_font_size(self.teDescription, 1)
reduceWidgetFontSize(self.twList)
reduceWidgetFontSize(self.teDescription, 1)
self.setWindowFlags(
QtCore.Qt.WindowType(
self.windowFlags()
| QtCore.Qt.WindowType.WindowSystemMenuHint
| QtCore.Qt.WindowType.WindowMaximizeButtonHint
)
)
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
self.skipButton = QtWidgets.QPushButton("Skip to Next")
self.buttonBox.addButton(self.skipButton, QtWidgets.QDialogButtonBox.ButtonRole.ActionRole)
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setText("Accept and Write Tags")
self.skipButton = QtWidgets.QPushButton(self.tr("Skip to Next"))
self.buttonBox.addButton(
self.skipButton, QtWidgets.QDialogButtonBox.ActionRole)
self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setText(
"Accept and Write Tags")
self.match_set_list = match_set_list
self.style = style
@ -70,22 +70,25 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
self.current_match_set_idx = 0
self.twList.currentItemChanged.connect(self.current_item_changed)
self.twList.cellDoubleClicked.connect(self.cell_double_clicked)
self.skipButton.clicked.connect(self.skip_to_next)
self.twList.currentItemChanged.connect(self.currentItemChanged)
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
self.skipButton.clicked.connect(self.skipToNext)
self.update_data()
self.updateData()
def update_data(self):
def updateData(self):
self.current_match_set = self.match_set_list[self.current_match_set_idx]
self.current_match_set = self.match_set_list[
self.current_match_set_idx]
if self.current_match_set_idx + 1 == len(self.match_set_list):
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Cancel).setDisabled(True)
self.skipButton.setText("Skip")
self.buttonBox.button(
QtWidgets.QDialogButtonBox.Cancel).setDisabled(True)
# self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setText("Accept")
self.skipButton.setText(self.tr("Skip"))
self.set_cover_image()
self.populate_table()
self.setCoverImage()
self.populateTable()
self.twList.resizeColumnsToContents()
self.twList.selectRow(0)
@ -94,11 +97,10 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
"Select correct match or skip ({0} of {1}): {2}".format(
self.current_match_set_idx + 1,
len(self.match_set_list),
os.path.split(path)[1],
)
os.path.split(path)[1])
)
def populate_table(self):
def populateTable(self):
while self.twList.rowCount() > 0:
self.twList.removeRow(0)
@ -109,134 +111,135 @@ class AutoTagMatchWindow(QtWidgets.QDialog):
for match in self.current_match_set.matches:
self.twList.insertRow(row)
item_text = match["series"]
item_text = match['series']
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setData(QtCore.Qt.ItemDataRole.UserRole, (match,))
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setData(QtCore.Qt.UserRole, (match,))
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 0, item)
if match["publisher"] is not None:
item_text = str(match["publisher"])
if match['publisher'] is not None:
item_text = "{0}".format(match['publisher'])
else:
item_text = "Unknown"
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 1, item)
month_str = ""
year_str = "????"
if match["month"] is not None:
month_str = f"-{int(match['month']):02d}"
if match["year"] is not None:
year_str = str(match["year"])
if match['month'] is not None:
month_str = "-{0:02d}".format(int(match['month']))
if match['year'] is not None:
year_str = "{0}".format(match['year'])
item_text = year_str + month_str
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 2, item)
item_text = match["issue_title"]
item_text = match['issue_title']
if item_text is None:
item_text = ""
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 3, item)
row += 1
self.twList.resizeColumnsToContents()
self.twList.setSortingEnabled(True)
self.twList.sortItems(2, QtCore.Qt.SortOrder.AscendingOrder)
self.twList.sortItems(2, QtCore.Qt.AscendingOrder)
self.twList.selectRow(0)
self.twList.resizeColumnsToContents()
self.twList.horizontalHeader().setStretchLastSection(True)
def cell_double_clicked(self, r, c):
def cellDoubleClicked(self, r, c):
self.accept()
def current_item_changed(self, curr, prev):
def currentItemChanged(self, curr, prev):
if curr is None:
return
if prev is not None and prev.row() == curr.row():
return
self.altCoverWidget.set_issue_id(self.current_match()["issue_id"])
if self.current_match()["description"] is None:
self.altCoverWidget.setIssueID(self.currentMatch()['issue_id'])
if self.currentMatch()['description'] is None:
self.teDescription.setText("")
else:
self.teDescription.setText(self.current_match()["description"])
self.teDescription.setText(self.currentMatch()['description'])
def set_cover_image(self):
def setCoverImage(self):
ca = self.current_match_set.ca
self.archiveCoverWidget.set_archive(ca)
self.archiveCoverWidget.setArchive(ca)
def current_match(self):
def currentMatch(self):
row = self.twList.currentRow()
match = self.twList.item(row, 0).data(QtCore.Qt.ItemDataRole.UserRole)[0]
match = self.twList.item(row, 0).data(
QtCore.Qt.UserRole)[0]
return match
def accept(self):
self.save_match()
self.saveMatch()
self.current_match_set_idx += 1
if self.current_match_set_idx == len(self.match_set_list):
# no more items
QtWidgets.QDialog.accept(self)
else:
self.update_data()
self.updateData()
def skip_to_next(self):
def skipToNext(self):
self.current_match_set_idx += 1
if self.current_match_set_idx == len(self.match_set_list):
# no more items
QtWidgets.QDialog.reject(self)
else:
self.update_data()
self.updateData()
def reject(self):
reply = QtWidgets.QMessageBox.question(
self,
"Cancel Matching",
"Are you sure you wish to cancel the matching process?",
QtWidgets.QMessageBox.StandardButton.Yes,
QtWidgets.QMessageBox.StandardButton.No,
)
self.tr("Cancel Matching"),
self.tr("Are you sure you wish to cancel the matching process?"),
QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.StandardButton.No:
if reply == QtWidgets.QMessageBox.No:
return
QtWidgets.QDialog.reject(self)
def save_match(self):
def saveMatch(self):
match = self.current_match()
match = self.currentMatch()
ca = self.current_match_set.ca
md = ca.read_metadata(self.style)
if md.is_empty:
md = ca.metadata_from_filename()
md = ca.readMetadata(self.style)
if md.isEmpty:
md = ca.metadataFromFilename()
# now get the particular issue data
cv_md = self.fetch_func(match)
if cv_md is None:
QtWidgets.QMessageBox.critical(
self, "Network Issue", "Could not connect to Comic Vine to get issue details!"
)
QtWidgets.QMessageBox.critical(self, self.tr("Network Issue"), self.tr(
"Could not connect to Comic Vine to get issue details!"))
return
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
QtWidgets.QApplication.setOverrideCursor(
QtGui.QCursor(QtCore.Qt.WaitCursor))
md.overlay(cv_md)
success = ca.write_metadata(md, self.style)
ca.load_cache([MetaDataStyle.CBI, MetaDataStyle.CIX])
success = ca.writeMetadata(md, self.style)
ca.loadCache([MetaDataStyle.CBI, MetaDataStyle.CIX])
QtWidgets.QApplication.restoreOverrideCursor()
if not success:
QtWidgets.QMessageBox.warning(self, "Write Error", "Saving the tags to the archive seemed to fail!")
QtWidgets.QMessageBox.warning(self, self.tr("Write Error"), self.tr(
"Saving the tags to the archive seemed to fail!"))

View File

@ -14,54 +14,53 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#import sys
#import os
import logging
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5 import QtCore, QtWidgets, uic
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.ui.qtutils import reduce_widget_font_size
logger = logging.getLogger(__name__)
from .settings import ComicTaggerSettings
from .coverimagewidget import CoverImageWidget
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
#import utils
class AutoTagProgressWindow(QtWidgets.QDialog):
def __init__(self, parent):
super().__init__(parent)
super(AutoTagProgressWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.get_ui_file("autotagprogresswindow.ui"), self)
uic.loadUi(
ComicTaggerSettings.getUIFile('autotagprogresswindow.ui'), self)
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, CoverImageWidget.DataMode, False)
self.archiveCoverWidget = CoverImageWidget(
self.archiveCoverContainer, CoverImageWidget.DataMode, False)
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
gridlayout.addWidget(self.archiveCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.testCoverWidget = CoverImageWidget(self.testCoverContainer, CoverImageWidget.DataMode, False)
self.testCoverWidget = CoverImageWidget(
self.testCoverContainer, CoverImageWidget.DataMode, False)
gridlayout = QtWidgets.QGridLayout(self.testCoverContainer)
gridlayout.addWidget(self.testCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.isdone = False
self.setWindowFlags(
QtCore.Qt.WindowType(
self.windowFlags()
| QtCore.Qt.WindowType.WindowSystemMenuHint
| QtCore.Qt.WindowType.WindowMaximizeButtonHint
)
)
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
reduce_widget_font_size(self.textEdit)
reduceWidgetFontSize(self.textEdit)
def set_archive_image(self, img_data):
self.set_cover_image(img_data, self.archiveCoverWidget)
def setArchiveImage(self, img_data):
self.setCoverImage(img_data, self.archiveCoverWidget)
def set_test_image(self, img_data):
self.set_cover_image(img_data, self.testCoverWidget)
def setTestImage(self, img_data):
self.setCoverImage(img_data, self.testCoverWidget)
def set_cover_image(self, img_data, widget):
widget.set_image_data(img_data)
def setCoverImage(self, img_data, widget):
widget.setImageData(img_data)
QtCore.QCoreApplication.processEvents()
QtCore.QCoreApplication.processEvents()

View File

@ -14,105 +14,114 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
#import os
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comictaggerlib.settings import ComicTaggerSettings
logger = logging.getLogger(__name__)
from .settings import ComicTaggerSettings
#from settingswindow import SettingsWindow
#from filerenamer import FileRenamer
#import utils
class AutoTagStartWindow(QtWidgets.QDialog):
def __init__(self, parent, settings, msg):
super().__init__(parent)
uic.loadUi(ComicTaggerSettings.get_ui_file("autotagstartwindow.ui"), self)
def __init__(self, parent, settings, msg):
super(AutoTagStartWindow, self).__init__(parent)
uic.loadUi(
ComicTaggerSettings.getUIFile('autotagstartwindow.ui'), self)
self.label.setText(msg)
self.setWindowFlags(
QtCore.Qt.WindowType(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint)
)
self.setWindowFlags(self.windowFlags() &
~QtCore.Qt.WindowContextHelpButtonHint)
self.settings = settings
self.cbxSaveOnLowConfidence.setCheckState(QtCore.Qt.CheckState.Unchecked)
self.cbxDontUseYear.setCheckState(QtCore.Qt.CheckState.Unchecked)
self.cbxAssumeIssueOne.setCheckState(QtCore.Qt.CheckState.Unchecked)
self.cbxIgnoreLeadingDigitsInFilename.setCheckState(QtCore.Qt.CheckState.Unchecked)
self.cbxRemoveAfterSuccess.setCheckState(QtCore.Qt.CheckState.Unchecked)
self.cbxSpecifySearchString.setCheckState(QtCore.Qt.CheckState.Unchecked)
self.leNameLengthMatchTolerance.setText(str(self.settings.id_length_delta_thresh))
self.cbxSaveOnLowConfidence.setCheckState(QtCore.Qt.Unchecked)
self.cbxDontUseYear.setCheckState(QtCore.Qt.Unchecked)
self.cbxAssumeIssueOne.setCheckState(QtCore.Qt.Unchecked)
self.cbxIgnoreLeadingDigitsInFilename.setCheckState(
QtCore.Qt.Unchecked)
self.cbxRemoveAfterSuccess.setCheckState(QtCore.Qt.Unchecked)
self.cbxSpecifySearchString.setCheckState(QtCore.Qt.Unchecked)
self.leNameLengthMatchTolerance.setText(
str(self.settings.id_length_delta_thresh))
self.leSearchString.setEnabled(False)
if self.settings.save_on_low_confidence:
self.cbxSaveOnLowConfidence.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxSaveOnLowConfidence.setCheckState(QtCore.Qt.Checked)
if self.settings.dont_use_year_when_identifying:
self.cbxDontUseYear.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxDontUseYear.setCheckState(QtCore.Qt.Checked)
if self.settings.assume_1_if_no_issue_num:
self.cbxAssumeIssueOne.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxAssumeIssueOne.setCheckState(QtCore.Qt.Checked)
if self.settings.ignore_leading_numbers_in_filename:
self.cbxIgnoreLeadingDigitsInFilename.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxIgnoreLeadingDigitsInFilename.setCheckState(
QtCore.Qt.Checked)
if self.settings.remove_archive_after_successful_match:
self.cbxRemoveAfterSuccess.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxRemoveAfterSuccess.setCheckState(QtCore.Qt.Checked)
if self.settings.wait_and_retry_on_rate_limit:
self.cbxWaitForRateLimit.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxWaitForRateLimit.setCheckState(QtCore.Qt.Checked)
nlmt_tip = """ <html>The <b>Name Length Match Tolerance</b> is for eliminating automatic
nlmtTip = (
""" <html>The <b>Name Length Match Tolerance</b> is for eliminating automatic
search matches that are too long compared to your series name search. The higher
it is, the more likely to have a good match, but each search will take longer and
use more bandwidth. Too low, and only the very closest lexical matches will be
explored.</html>"""
explored.</html>""")
self.leNameLengthMatchTolerance.setToolTip(nlmt_tip)
self.leNameLengthMatchTolerance.setToolTip(nlmtTip)
ss_tip = """<html>
ssTip = (
"""<html>
The <b>series search string</b> specifies the search string to be used for all selected archives.
Use this when trying to match archives with hard-to-parse or incorrect filenames. All archives selected
should be from the same series.
</html>"""
self.leSearchString.setToolTip(ss_tip)
self.cbxSpecifySearchString.setToolTip(ss_tip)
)
self.leSearchString.setToolTip(ssTip)
self.cbxSpecifySearchString.setToolTip(ssTip)
validator = QtGui.QIntValidator(0, 99, self)
self.leNameLengthMatchTolerance.setValidator(validator)
self.cbxSpecifySearchString.stateChanged.connect(self.search_string_toggle)
self.cbxSpecifySearchString.stateChanged.connect(
self.searchStringToggle)
self.auto_save_on_low = False
self.dont_use_year = False
self.assume_issue_one = False
self.ignore_leading_digits_in_filename = False
self.remove_after_success = False
self.wait_and_retry_on_rate_limit = False
self.search_string = None
self.name_length_match_tolerance = self.settings.id_length_delta_thresh
self.autoSaveOnLow = False
self.dontUseYear = False
self.assumeIssueOne = False
self.ignoreLeadingDigitsInFilename = False
self.removeAfterSuccess = False
self.waitAndRetryOnRateLimit = False
self.searchString = None
self.nameLengthMatchTolerance = self.settings.id_length_delta_thresh
def search_string_toggle(self):
def searchStringToggle(self):
enable = self.cbxSpecifySearchString.isChecked()
self.leSearchString.setEnabled(enable)
def accept(self):
QtWidgets.QDialog.accept(self)
self.auto_save_on_low = self.cbxSaveOnLowConfidence.isChecked()
self.dont_use_year = self.cbxDontUseYear.isChecked()
self.assume_issue_one = self.cbxAssumeIssueOne.isChecked()
self.ignore_leading_digits_in_filename = self.cbxIgnoreLeadingDigitsInFilename.isChecked()
self.remove_after_success = self.cbxRemoveAfterSuccess.isChecked()
self.name_length_match_tolerance = int(self.leNameLengthMatchTolerance.text())
self.wait_and_retry_on_rate_limit = self.cbxWaitForRateLimit.isChecked()
self.autoSaveOnLow = self.cbxSaveOnLowConfidence.isChecked()
self.dontUseYear = self.cbxDontUseYear.isChecked()
self.assumeIssueOne = self.cbxAssumeIssueOne.isChecked()
self.ignoreLeadingDigitsInFilename = self.cbxIgnoreLeadingDigitsInFilename.isChecked()
self.removeAfterSuccess = self.cbxRemoveAfterSuccess.isChecked()
self.nameLengthMatchTolerance = int(
self.leNameLengthMatchTolerance.text())
self.waitAndRetryOnRateLimit = self.cbxWaitForRateLimit.isChecked()
# persist some settings
self.settings.save_on_low_confidence = self.auto_save_on_low
self.settings.dont_use_year_when_identifying = self.dont_use_year
self.settings.assume_1_if_no_issue_num = self.assume_issue_one
self.settings.ignore_leading_numbers_in_filename = self.ignore_leading_digits_in_filename
self.settings.remove_archive_after_successful_match = self.remove_after_success
self.settings.wait_and_retry_on_rate_limit = self.wait_and_retry_on_rate_limit
self.settings.save_on_low_confidence = self.autoSaveOnLow
self.settings.dont_use_year_when_identifying = self.dontUseYear
self.settings.assume_1_if_no_issue_num = self.assumeIssueOne
self.settings.ignore_leading_numbers_in_filename = self.ignoreLeadingDigitsInFilename
self.settings.remove_archive_after_successful_match = self.removeAfterSuccess
self.settings.wait_and_retry_on_rate_limit = self.waitAndRetryOnRateLimit
if self.cbxSpecifySearchString.isChecked():
self.search_string = str(self.leSearchString.text())
if len(self.search_string) == 0:
self.search_string = None
self.searchString = str(self.leSearchString.text())
if len(self.searchString) == 0:
self.searchString = None

View File

@ -14,15 +14,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
#import os
from comicapi.genericmetadata import GenericMetadata
logger = logging.getLogger(__name__)
#import utils
class CBLTransformer:
def __init__(self, metadata: GenericMetadata, settings):
def __init__(self, metadata, settings):
self.metadata = metadata
self.settings = settings
@ -34,36 +33,36 @@ class CBLTransformer:
def add_string_list_to_tags(str_list):
if str_list is not None and str_list != "":
items = [s.strip() for s in str_list.split(",")]
items = [s.strip() for s in str_list.split(',')]
for item in items:
append_to_tags_if_unique(item)
if self.settings.assume_lone_credit_is_primary:
# helper
def set_lone_primary(role_list):
def setLonePrimary(role_list):
lone_credit = None
count = 0
for c in self.metadata.credits:
if c["role"].lower() in role_list:
if c['role'].lower() in role_list:
count += 1
lone_credit = c
if count > 1:
lone_credit = None
break
if lone_credit is not None:
lone_credit["primary"] = True
lone_credit['primary'] = True
return lone_credit, count
# need to loop three times, once for 'writer', 'artist', and then
# 'penciler' if no artist
set_lone_primary(["writer"])
c, count = set_lone_primary(["artist"])
setLonePrimary(['writer'])
c, count = setLonePrimary(['artist'])
if c is None and count == 0:
c, count = set_lone_primary(["penciler", "penciller"])
c, count = setLonePrimary(['penciler', 'penciller'])
if c is not None:
c["primary"] = False
self.metadata.add_credit(c["person"], "Artist", True)
c['primary'] = False
self.metadata.addCredit(c['person'], 'Artist', True)
if self.settings.copy_characters_to_tags:
add_string_list_to_tags(self.metadata.characters)
@ -75,7 +74,7 @@ class CBLTransformer:
add_string_list_to_tags(self.metadata.locations)
if self.settings.copy_storyarcs_to_tags:
add_string_list_to_tags(self.metadata.story_arc)
add_string_list_to_tags(self.metadata.storyArc)
if self.settings.copy_notes_to_comments:
if self.metadata.notes is not None:
@ -87,12 +86,12 @@ class CBLTransformer:
self.metadata.comments += self.metadata.notes
if self.settings.copy_weblink_to_comments:
if self.metadata.web_link is not None:
if self.metadata.webLink is not None:
if self.metadata.comments is None:
self.metadata.comments = ""
else:
self.metadata.comments += "\n\n"
if self.metadata.web_link not in self.metadata.comments:
self.metadata.comments += self.metadata.web_link
if self.metadata.webLink not in self.metadata.comments:
self.metadata.comments += self.metadata.webLink
return self.metadata

View File

@ -16,34 +16,60 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import logging
import os
import sys
import os
from pprint import pprint
from comicapi import utils
from comicapi.comicarchive import ComicArchive, MetaDataStyle
from comicapi.genericmetadata import GenericMetadata
from comictaggerlib.cbltransformer import CBLTransformer
from comictaggerlib.comicvinetalker import ComicVineTalker, ComicVineTalkerException
from comictaggerlib.filerenamer import FileRenamer
from comictaggerlib.issueidentifier import IssueIdentifier
from comictaggerlib.resulttypes import MultipleMatch, OnlineMatchResults
from comictaggerlib.settings import ComicTaggerSettings
import json
#import signal
#import traceback
#import time
#import platform
#import locale
#import codecs
filename_encoding = sys.getfilesystemencoding()
logger = logging.getLogger(__name__)
from .settings import ComicTaggerSettings
from .options import Options
from .comicarchive import ComicArchive, MetaDataStyle
from .issueidentifier import IssueIdentifier
from .genericmetadata import GenericMetadata
from .comicvinetalker import ComicVineTalker, ComicVineTalkerException
from .filerenamer import FileRenamer
from .cbltransformer import CBLTransformer
from . import utils
class MultipleMatch():
def __init__(self, filename, match_list):
self.filename = filename
self.matches = match_list
class OnlineMatchResults():
def __init__(self):
self.goodMatches = []
self.noMatches = []
self.multipleMatches = []
self.lowConfidenceMatches = []
self.writeFailures = []
self.fetchDataFailures = []
#-----------------------------
def actual_issue_data_fetch(match, settings, opts):
# now get the particular issue data
try:
comic_vine = ComicVineTalker()
comic_vine.wait_for_rate_limit = opts.wait_and_retry_on_rate_limit
cv_md = comic_vine.fetch_issue_data(match["volume_id"], match["issue_number"], settings)
comicVine = ComicVineTalker()
comicVine.wait_for_rate_limit = opts.wait_and_retry_on_rate_limit
cv_md = comicVine.fetchIssueData(
match['volume_id'], match['issue_number'], settings)
except ComicVineTalkerException:
logger.exception("Network error while getting issue details. Save aborted")
print("Network error while getting issue details. Save aborted", file=sys.stderr)
return None
if settings.apply_cbl_transform_on_cv_import:
@ -52,96 +78,100 @@ def actual_issue_data_fetch(match, settings, opts):
return cv_md
def actual_metadata_save(ca: ComicArchive, opts, md):
def actual_metadata_save(ca, opts, md):
if not opts.dryrun:
# write out the new data
if not ca.write_metadata(md, opts.data_style):
logger.error("The tag save seemed to fail!")
if not ca.writeMetadata(md, opts.data_style):
print("The tag save seemed to fail!", file=sys.stderr)
return False
print("Save complete.")
logger.info("Save complete.")
else:
print("Save complete.", file=sys.stderr)
else:
if opts.terse:
logger.info("dry-run option was set, so nothing was written")
print("dry-run option was set, so nothing was written")
print("dry-run option was set, so nothing was written", file=sys.stderr)
else:
logger.info("dry-run option was set, so nothing was written, but here is the final set of tags:")
print("dry-run option was set, so nothing was written, but here is the final set of tags:")
print(f"{md}")
print("dry-run option was set, so nothing was written, but here is the final set of tags:", file=sys.stderr)
print(("{0}".format(md)))
return True
def display_match_set_for_choice(label, match_set: MultipleMatch, opts, settings):
print(f"{match_set.ca.path} -- {label}:")
def display_match_set_for_choice(label, match_set, opts, settings):
print(("{0} -- {1}:".format(match_set.filename, label)))
# sort match list by year
match_set.matches.sort(key=lambda k: k["year"])
match_set.matches.sort(key=lambda k: k['year'])
for (counter, m) in enumerate(match_set.matches):
counter += 1
print(
print((
" {0}. {1} #{2} [{3}] ({4}/{5}) - {6}".format(
counter,
m["series"],
m["issue_number"],
m["publisher"],
m["month"],
m["year"],
m["issue_title"],
)
)
m['series'],
m['issue_number'],
m['publisher'],
m['month'],
m['year'],
m['issue_title'])))
if opts.interactive:
while True:
i = input("Choose a match #, or 's' to skip: ")
if (i.isdigit() and int(i) in range(1, len(match_set.matches) + 1)) or i == "s":
if (i.isdigit() and int(i) in range(
1, len(match_set.matches) + 1)) or i == 's':
break
if i != "s":
if i != 's':
i = int(i) - 1
# save the data!
# we know at this point, that the file is all good to go
ca = match_set.ca
md = create_local_metadata(opts, ca, ca.has_metadata(opts.data_style))
cv_md = actual_issue_data_fetch(match_set.matches[int(i)], settings, opts)
ca = ComicArchive(
match_set.filename,
settings.rar_exe_path,
ComicTaggerSettings.getGraphic('nocover.png'))
md = create_local_metadata(
opts, ca, ca.hasMetadata(opts.data_style))
cv_md = actual_issue_data_fetch(
match_set.matches[int(i)], settings, opts)
md.overlay(cv_md)
actual_metadata_save(ca, opts, md)
def post_process_matches(match_results: OnlineMatchResults, opts, settings):
def post_process_matches(match_results, opts, settings):
# now go through the match results
if opts.show_save_summary:
if len(match_results.good_matches) > 0:
if len(match_results.goodMatches) > 0:
print("\nSuccessful matches:\n------------------")
for f in match_results.good_matches:
for f in match_results.goodMatches:
print(f)
if len(match_results.no_matches) > 0:
if len(match_results.noMatches) > 0:
print("\nNo matches:\n------------------")
for f in match_results.no_matches:
for f in match_results.noMatches:
print(f)
if len(match_results.write_failures) > 0:
if len(match_results.writeFailures) > 0:
print("\nFile Write Failures:\n------------------")
for f in match_results.write_failures:
for f in match_results.writeFailures:
print(f)
if len(match_results.fetch_data_failures) > 0:
if len(match_results.fetchDataFailures) > 0:
print("\nNetwork Data Fetch Failures:\n------------------")
for f in match_results.fetch_data_failures:
for f in match_results.fetchDataFailures:
print(f)
if not opts.show_save_summary and not opts.interactive:
# just quit if we're not interactive or showing the summary
return
if len(match_results.multiple_matches) > 0:
print("\nArchives with multiple high-confidence matches:\n------------------")
for match_set in match_results.multiple_matches:
display_match_set_for_choice("Multiple high-confidence matches", match_set, opts, settings)
if len(match_results.multipleMatches) > 0:
print(
"\nArchives with multiple high-confidence matches:\n------------------")
for match_set in match_results.multipleMatches:
display_match_set_for_choice(
"Multiple high-confidence matches", match_set, opts, settings)
if len(match_results.low_confidence_matches) > 0:
if len(match_results.lowConfidenceMatches) > 0:
print("\nArchives with low-confidence matches:\n------------------")
for match_set in match_results.low_confidence_matches:
for match_set in match_results.lowConfidenceMatches:
if len(match_set.matches) == 1:
label = "Single low-confidence match"
else:
@ -152,28 +182,31 @@ def post_process_matches(match_results: OnlineMatchResults, opts, settings):
def cli_mode(opts, settings):
if len(opts.file_list) < 1:
logger.error("You must specify at least one filename. Use the -h option for more info")
print("You must specify at least one filename. Use the -h option for more info", file=sys.stderr)
return
match_results = OnlineMatchResults()
for f in opts.file_list:
if isinstance(f, str):
pass
process_file_cli(f, opts, settings, match_results)
sys.stdout.flush()
post_process_matches(match_results, opts, settings)
def create_local_metadata(opts, ca: ComicArchive, has_desired_tags):
def create_local_metadata(opts, ca, has_desired_tags):
md = GenericMetadata()
md.set_default_page_list(ca.get_number_of_pages())
md.setDefaultPageList(ca.getNumberOfPages())
if has_desired_tags:
md = ca.readMetadata(opts.data_style)
# now, overlay the parsed filename info
if opts.parse_filename:
md.overlay(ca.metadata_from_filename())
if has_desired_tags:
md = ca.read_metadata(opts.data_style)
md.overlay(ca.metadataFromFilename())
# finally, use explicit stuff
if opts.metadata is not None:
@ -182,54 +215,61 @@ def create_local_metadata(opts, ca: ComicArchive, has_desired_tags):
return md
def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults):
def process_file_cli(filename, opts, settings, match_results):
batch_mode = len(opts.file_list) > 1
ca = ComicArchive(filename, settings.rar_exe_path, ComicTaggerSettings.get_graphic("nocover.png"))
ca = ComicArchive(
filename,
settings.rar_exe_path,
ComicTaggerSettings.getGraphic('nocover.png'))
if not os.path.lexists(filename):
logger.error("Cannot find " + filename)
print("Cannot find " + filename, file=sys.stderr)
return
if not ca.seems_to_be_a_comic_archive():
logger.error("Sorry, but %s is not a comic archive!", filename)
if not ca.seemsToBeAComicArchive():
print("Sorry, but " + \
filename + " is not a comic archive!", file=sys.stderr)
return
if not ca.is_writable() and (opts.delete_tags or opts.copy_tags or opts.save_tags or opts.rename_file):
logger.error("This archive is not writable for that tag type")
# if not ca.isWritableForStyle(opts.data_style) and (opts.delete_tags or
# opts.save_tags or opts.rename_file):
if not ca.isWritable() and (
opts.delete_tags or opts.copy_tags or opts.save_tags or opts.rename_file):
print("This archive is not writable for that tag type", file=sys.stderr)
return
has = [False, False, False]
if ca.has_cix():
if ca.hasCIX():
has[MetaDataStyle.CIX] = True
if ca.has_cbi():
if ca.hasCBI():
has[MetaDataStyle.CBI] = True
if ca.has_comet():
if ca.hasCoMet():
has[MetaDataStyle.COMET] = True
if opts.print_tags:
if opts.data_style is None:
page_count = ca.get_number_of_pages()
page_count = ca.getNumberOfPages()
brief = ""
if batch_mode:
brief = f"{ca.path}: "
brief = "{0}: ".format(filename)
if ca.is_sevenzip():
brief += "7Z archive "
elif ca.is_zip():
if ca.isZip():
brief += "ZIP archive "
elif ca.is_rar():
elif ca.isRar():
brief += "RAR archive "
elif ca.is_folder():
elif ca.isFolder():
brief += "Folder archive "
brief += f"({page_count: >3} pages)"
brief += "({0: >3} pages)".format(page_count)
brief += " tags:[ "
if not (has[MetaDataStyle.CBI] or has[MetaDataStyle.CIX] or has[MetaDataStyle.COMET]):
if not (has[MetaDataStyle.CBI] or has[
MetaDataStyle.CIX] or has[MetaDataStyle.COMET]):
brief += "none "
else:
if has[MetaDataStyle.CBI]:
@ -251,73 +291,88 @@ def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults
if has[MetaDataStyle.CIX]:
print("--------- ComicRack tags ---------")
if opts.raw:
print(ca.read_raw_cix())
print((
"{0}".format(
str(
ca.readRawCIX(),
errors='ignore'))))
else:
print(ca.read_cix())
print(("{0}".format(ca.readCIX())))
if opts.data_style is None or opts.data_style == MetaDataStyle.CBI:
if has[MetaDataStyle.CBI]:
print("------- ComicBookLover tags -------")
if opts.raw:
pprint(json.loads(ca.read_raw_cbi()))
pprint(json.loads(ca.readRawCBI()))
else:
print(ca.read_cbi())
print(("{0}".format(ca.readCBI())))
if opts.data_style is None or opts.data_style == MetaDataStyle.COMET:
if has[MetaDataStyle.COMET]:
print("----------- CoMet tags -----------")
if opts.raw:
print(ca.read_raw_comet())
print(("{0}".format(ca.readRawCoMet())))
else:
print(ca.read_comet())
print(("{0}".format(ca.readCoMet())))
elif opts.delete_tags:
style_name = MetaDataStyle.name[opts.data_style]
if has[opts.data_style]:
if not opts.dryrun:
if not ca.remove_metadata(opts.data_style):
print(f"{filename}: Tag removal seemed to fail!")
if not ca.removeMetadata(opts.data_style):
print(("{0}: Tag removal seemed to fail!".format(filename)))
else:
print(f"{filename}: Removed {style_name} tags.")
print((
"{0}: Removed {1} tags.".format(filename, style_name)))
else:
print(f"{filename}: dry-run. {style_name} tags not removed")
print((
"{0}: dry-run. {1} tags not removed".format(filename, style_name)))
else:
print(f"{filename}: This archive doesn't have {style_name} tags to remove.")
print(("{0}: This archive doesn't have {1} tags to remove.".format(
filename, style_name)))
elif opts.copy_tags:
dst_style_name = MetaDataStyle.name[opts.data_style]
if opts.no_overwrite and has[opts.data_style]:
print(f"{filename}: Already has {dst_style_name} tags. Not overwriting.")
print(("{0}: Already has {1} tags. Not overwriting.".format(
filename, dst_style_name)))
return
if opts.copy_source == opts.data_style:
print(f"{filename}: Destination and source are same: {dst_style_name}. Nothing to do.")
print((
"{0}: Destination and source are same: {1}. Nothing to do.".format(
filename,
dst_style_name)))
return
src_style_name = MetaDataStyle.name[opts.copy_source]
if has[opts.copy_source]:
if not opts.dryrun:
md = ca.read_metadata(opts.copy_source)
md = ca.readMetadata(opts.copy_source)
if settings.apply_cbl_transform_on_bulk_operation and opts.data_style == MetaDataStyle.CBI:
md = CBLTransformer(md, settings).apply()
if not ca.write_metadata(md, opts.data_style):
print(f"{filename}: Tag copy seemed to fail!")
if not ca.writeMetadata(md, opts.data_style):
print(("{0}: Tag copy seemed to fail!".format(filename)))
else:
print(f"{filename}: Copied {src_style_name} tags to {dst_style_name}.")
print(("{0}: Copied {1} tags to {2} .".format(
filename, src_style_name, dst_style_name)))
else:
print(f"{filename}: dry-run. {src_style_name} tags not copied")
print((
"{0}: dry-run. {1} tags not copied".format(filename, src_style_name)))
else:
print(f"{filename}: This archive doesn't have {src_style_name} tags to copy.")
print(("{0}: This archive doesn't have {1} tags to copy.".format(
filename, src_style_name)))
elif opts.save_tags:
if opts.no_overwrite and has[opts.data_style]:
print(f"{filename}: Already has {MetaDataStyle.name[opts.data_style]} tags. Not overwriting.")
print(("{0}: Already has {1} tags. Not overwriting.".format(
filename, MetaDataStyle.name[opts.data_style])))
return
if batch_mode:
print(f"Processing {ca.path}...")
print(("Processing {0}...".format(filename)))
md = create_local_metadata(opts, ca, has[opts.data_style])
if md.issue is None or md.issue == "":
@ -329,17 +384,19 @@ def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults
if opts.issue_id is not None:
# we were given the actual ID to search with
try:
comic_vine = ComicVineTalker()
comic_vine.wait_for_rate_limit = opts.wait_and_retry_on_rate_limit
cv_md = comic_vine.fetch_issue_data_by_issue_id(opts.issue_id, settings)
comicVine = ComicVineTalker()
comicVine.wait_for_rate_limit = opts.wait_and_retry_on_rate_limit
cv_md = comicVine.fetchIssueDataByIssueID(
opts.issue_id, settings)
except ComicVineTalkerException:
logger.exception("Network error while getting issue details. Save aborted")
match_results.fetch_data_failures.append(ca.path)
print("Network error while getting issue details. Save aborted", file=sys.stderr)
match_results.fetchDataFailures.append(filename)
return
if cv_md is None:
logger.error("No match for ID %s was found.", opts.issue_id)
match_results.no_matches.append(ca.path)
print("No match for ID {0} was found.".format(
opts.issue_id), file=sys.stderr)
match_results.noMatches.append(filename)
return
if settings.apply_cbl_transform_on_cv_import:
@ -347,21 +404,21 @@ def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults
else:
ii = IssueIdentifier(ca, settings)
if md is None or md.is_empty:
logger.error("No metadata given to search online with!")
match_results.no_matches.append(ca.path)
if md is None or md.isEmpty:
print("No metadata given to search online with!", file=sys.stderr)
match_results.noMatches.append(filename)
return
def myoutput(text):
if opts.verbose:
IssueIdentifier.default_write_output(text)
IssueIdentifier.defaultWriteOutput(text)
# use our overlayed MD struct to search
ii.set_additional_metadata(md)
ii.only_use_additional_meta_data = True
ii.wait_and_retry_on_rate_limit = opts.wait_and_retry_on_rate_limit
ii.set_output_function(myoutput)
ii.cover_page_index = md.get_cover_page_index_list()[0]
ii.setAdditionalMetadata(md)
ii.onlyUseAdditionalMetaData = True
ii.waitAndRetryOnRateLimit = opts.wait_and_retry_on_rate_limit
ii.setOutputFunction(myoutput)
ii.cover_page_index = md.getCoverPageIndexList()[0]
matches = ii.search()
result = ii.search_result
@ -370,37 +427,40 @@ def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults
choices = False
low_confidence = False
if result == ii.result_no_matches:
if result == ii.ResultNoMatches:
pass
elif result == ii.result_found_match_but_bad_cover_score:
elif result == ii.ResultFoundMatchButBadCoverScore:
low_confidence = True
found_match = True
elif result == ii.result_found_match_but_not_first_page:
elif result == ii.ResultFoundMatchButNotFirstPage:
found_match = True
elif result == ii.result_multiple_matches_with_bad_image_scores:
elif result == ii.ResultMultipleMatchesWithBadImageScores:
low_confidence = True
choices = True
elif result == ii.result_one_good_match:
elif result == ii.ResultOneGoodMatch:
found_match = True
elif result == ii.result_multiple_good_matches:
elif result == ii.ResultMultipleGoodMatches:
choices = True
if choices:
if low_confidence:
logger.error("Online search: Multiple low confidence matches. Save aborted")
match_results.low_confidence_matches.append(MultipleMatch(ca, matches))
print("Online search: Multiple low confidence matches. Save aborted", file=sys.stderr)
match_results.lowConfidenceMatches.append(
MultipleMatch(filename, matches))
return
else:
print("Online search: Multiple good matches. Save aborted", file=sys.stderr)
match_results.multipleMatches.append(
MultipleMatch(filename, matches))
return
logger.error("Online search: Multiple good matches. Save aborted")
match_results.multiple_matches.append(MultipleMatch(ca, matches))
return
if low_confidence and opts.abortOnLowConfidence:
logger.error("Online search: Low confidence match. Save aborted")
match_results.low_confidence_matches.append(MultipleMatch(ca, matches))
print("Online search: Low confidence match. Save aborted", file=sys.stderr)
match_results.lowConfidenceMatches.append(
MultipleMatch(filename, matches))
return
if not found_match:
logger.error("Online search: No match found. Save aborted")
match_results.no_matches.append(ca.path)
print("Online search: No match found. Save aborted", file=sys.stderr)
match_results.noMatches.append(filename)
return
# we got here, so we have a single match
@ -408,22 +468,22 @@ def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults
# now get the particular issue data
cv_md = actual_issue_data_fetch(matches[0], settings, opts)
if cv_md is None:
match_results.fetch_data_failures.append(ca.path)
match_results.fetchDataFailures.append(filename)
return
md.overlay(cv_md)
# ok, done building our metadata. time to save
if not actual_metadata_save(ca, opts, md):
match_results.write_failures.append(ca.path)
match_results.writeFailures.append(filename)
else:
match_results.good_matches.append(ca.path)
match_results.goodMatches.append(filename)
elif opts.rename_file:
msg_hdr = ""
if batch_mode:
msg_hdr = f"{ca.path}: "
msg_hdr = "{0}: ".format(filename)
if opts.data_style is not None:
use_tags = has[opts.data_style]
@ -433,55 +493,66 @@ def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults
md = create_local_metadata(opts, ca, use_tags)
if md.series is None:
logger.error(msg_hdr + "Can't rename without series name")
print(msg_hdr + "Can't rename without series name", file=sys.stderr)
return
new_ext = None # default
if settings.rename_extension_based_on_archive:
if ca.is_sevenzip():
new_ext = ".cb7"
elif ca.is_zip():
if ca.isZip():
new_ext = ".cbz"
elif ca.is_rar():
elif ca.isRar():
new_ext = ".cbr"
renamer = FileRenamer(md)
renamer.set_template(settings.rename_template)
renamer.set_issue_zero_padding(settings.rename_issue_number_padding)
renamer.set_smart_cleanup(settings.rename_use_smart_string_cleanup)
renamer.setTemplate(settings.rename_template)
renamer.setIssueZeroPadding(settings.rename_issue_number_padding)
renamer.setSmartCleanup(settings.rename_use_smart_string_cleanup)
renamer.move = settings.rename_move_dir
new_name = renamer.determine_name(ca.path, ext=new_ext)
if new_name == os.path.basename(ca.path):
logger.error(msg_hdr + "Filename is already good!")
try:
new_name = renamer.determineName(filename, ext=new_ext)
except Exception as e:
print(msg_hdr + "Invalid format string!\nYour rename template is invalid!\n\n"
"{}\n\nPlease consult the template help in the settings "
"and the documentation on the format at "
"https://docs.python.org/3/library/string.html#format-string-syntax", file=sys.stderr)
return
folder = os.path.dirname(os.path.abspath(ca.path))
folder = os.path.dirname(os.path.abspath(filename))
if settings.rename_move_dir and len(settings.rename_dir.strip()) > 3:
folder = settings.rename_dir.strip()
new_abs_path = utils.unique_file(os.path.join(folder, new_name))
if os.path.join(folder, new_name) == os.path.abspath(filename):
print(msg_hdr + "Filename is already good!", file=sys.stderr)
return
suffix = ""
if not opts.dryrun:
# rename the file
os.rename(ca.path, new_abs_path)
os.makedirs(os.path.dirname(new_abs_path), 0o777, True)
os.rename(filename, new_abs_path)
else:
suffix = " (dry-run, no change)"
print(f"renamed '{os.path.basename(ca.path)}' -> '{new_name}' {suffix}")
print((
"renamed '{0}' -> '{1}' {2}".format(os.path.basename(filename), new_name, suffix)))
elif opts.export_to_zip:
msg_hdr = ""
if batch_mode:
msg_hdr = f"{ca.path}: "
msg_hdr = "{0}: ".format(filename)
if not ca.is_rar():
logger.error(msg_hdr + "Archive is not a RAR.")
if not ca.isRar():
print(msg_hdr + "Archive is not a RAR.", file=sys.stderr)
return
rar_file = os.path.abspath(os.path.abspath(filename))
new_file = os.path.splitext(rar_file)[0] + ".cbz"
if opts.abort_export_on_conflict and os.path.lexists(new_file):
print(msg_hdr + f"{os.path.split(new_file)[1]} already exists in the that folder.")
print(msg_hdr + "{0} already exists in the that folder.".format(os.path.split(new_file)[1]))
return
new_file = utils.unique_file(os.path.join(new_file))
@ -489,13 +560,14 @@ def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults
delete_success = False
export_success = False
if not opts.dryrun:
if ca.export_as_zip(new_file):
if ca.exportAsZip(new_file):
export_success = True
if opts.delete_rar_after_export:
try:
os.unlink(rar_file)
except:
logger.exception(msg_hdr + "Error deleting original RAR after export")
print(msg_hdr + \
"Error deleting original RAR after export", file=sys.stderr)
delete_success = False
else:
delete_success = True
@ -504,15 +576,18 @@ def process_file_cli(filename, opts, settings, match_results: OnlineMatchResults
if os.path.lexists(new_file):
os.remove(new_file)
else:
msg = msg_hdr + f"Dry-run: Would try to create {os.path.split(new_file)[1]}"
if opts.delete_after_zip_export:
msg = msg_hdr + \
"Dry-run: Would try to create {0}".format(
os.path.split(new_file)[1])
if opts.delete_rar_after_export:
msg += " and delete orginal."
print(msg)
return
msg = msg_hdr
if export_success:
msg += f"Archive exported successfully to: {os.path.split(new_file)[1]}"
msg += "Archive exported successfully to: {0}".format(
os.path.split(new_file)[1])
if opts.delete_rar_after_export and delete_success:
msg += " (Original deleted) "
else:

1
comictaggerlib/comet.py Normal file
View File

@ -0,0 +1 @@
from comicapi.comet import *

View File

@ -0,0 +1 @@
from comicapi.comicarchive import *

View File

@ -0,0 +1 @@
from comicapi.comicbookinfo import *

View File

@ -0,0 +1 @@
from comicapi.comicinfoxml import *

View File

@ -14,39 +14,40 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
import logging
import os
import sqlite3 as lite
import os
import datetime
#import sys
#from pprint import pprint
from comicapi import utils
from comictaggerlib import ctversion
from comictaggerlib.settings import ComicTaggerSettings
logger = logging.getLogger(__name__)
from . import ctversion
from .settings import ComicTaggerSettings
from . import utils
class ComicVineCacher:
def __init__(self):
self.settings_folder = ComicTaggerSettings.get_settings_folder()
self.settings_folder = ComicTaggerSettings.getSettingsFolder()
self.db_file = os.path.join(self.settings_folder, "cv_cache.db")
self.version_file = os.path.join(self.settings_folder, "cache_version.txt")
self.version_file = os.path.join(
self.settings_folder, "cache_version.txt")
# verify that cache is from same version as this one
data = ""
try:
with open(self.version_file, "rb") as f:
data = f.read().decode("utf-8")
with open(self.version_file, 'rb') as f:
data = f.read().decode("utf-8")
f.close()
except:
pass
if data != ctversion.version:
self.clear_cache()
self.clearCache()
if not os.path.exists(self.db_file):
self.create_cache_db()
def clear_cache(self):
def clearCache(self):
try:
os.unlink(self.db_file)
except:
@ -59,64 +60,61 @@ class ComicVineCacher:
def create_cache_db(self):
# create the version file
with open(self.version_file, "w") as f:
with open(self.version_file, 'w') as f:
f.write(ctversion.version)
# this will wipe out any existing version
open(self.db_file, "w").close()
open(self.db_file, 'w').close()
con = lite.connect(self.db_file)
# create tables
with con:
cur = con.cursor()
# name,id,start_year,publisher,image,description,count_of_issues
cur.execute(
"CREATE TABLE VolumeSearchCache("
+ "search_term TEXT,"
+ "id INT,"
+ "name TEXT,"
+ "start_year INT,"
+ "publisher TEXT,"
+ "count_of_issues INT,"
+ "image_url TEXT,"
+ "description TEXT,"
+ "timestamp DATE DEFAULT (datetime('now','localtime'))) "
)
"CREATE TABLE VolumeSearchCache(" +
"search_term TEXT," +
"id INT," +
"name TEXT," +
"start_year INT," +
"publisher TEXT," +
"count_of_issues INT," +
"image_url TEXT," +
"description TEXT," +
"timestamp DATE DEFAULT (datetime('now','localtime'))) ")
cur.execute(
"CREATE TABLE Volumes("
+ "id INT,"
+ "name TEXT,"
+ "publisher TEXT,"
+ "count_of_issues INT,"
+ "start_year INT,"
+ "timestamp DATE DEFAULT (datetime('now','localtime')), "
+ "PRIMARY KEY (id))"
)
"CREATE TABLE Volumes(" +
"id INT," +
"name TEXT," +
"publisher TEXT," +
"count_of_issues INT," +
"start_year INT," +
"timestamp DATE DEFAULT (datetime('now','localtime')), " +
"PRIMARY KEY (id))")
cur.execute(
"CREATE TABLE AltCovers("
+ "issue_id INT,"
+ "url_list TEXT,"
+ "timestamp DATE DEFAULT (datetime('now','localtime')), "
+ "PRIMARY KEY (issue_id))"
)
"CREATE TABLE AltCovers(" +
"issue_id INT," +
"url_list TEXT," +
"timestamp DATE DEFAULT (datetime('now','localtime')), " +
"PRIMARY KEY (issue_id))")
cur.execute(
"CREATE TABLE Issues("
+ "id INT,"
+ "volume_id INT,"
+ "name TEXT,"
+ "issue_number TEXT,"
+ "super_url TEXT,"
+ "thumb_url TEXT,"
+ "cover_date TEXT,"
+ "site_detail_url TEXT,"
+ "description TEXT,"
+ "timestamp DATE DEFAULT (datetime('now','localtime')), "
+ "PRIMARY KEY (id))"
)
"CREATE TABLE Issues(" +
"id INT," +
"volume_id INT," +
"name TEXT," +
"issue_number TEXT," +
"super_url TEXT," +
"thumb_url TEXT," +
"cover_date TEXT," +
"site_detail_url TEXT," +
"description TEXT," +
"timestamp DATE DEFAULT (datetime('now','localtime')), " +
"PRIMARY KEY (id))")
def add_search_results(self, search_term, cv_search_results):
@ -127,40 +125,40 @@ class ComicVineCacher:
cur = con.cursor()
# remove all previous entries with this search term
cur.execute("DELETE FROM VolumeSearchCache WHERE search_term = ?", [search_term.lower()])
cur.execute(
"DELETE FROM VolumeSearchCache WHERE search_term = ?", [
search_term.lower()])
# now add in new results
for record in cv_search_results:
timestamp = datetime.datetime.now()
if record["publisher"] is None:
if record['publisher'] is None:
pub_name = ""
else:
pub_name = record["publisher"]["name"]
pub_name = record['publisher']['name']
if record["image"] is None:
if record['image'] is None:
url = ""
else:
url = record["image"]["super_url"]
url = record['image']['super_url']
cur.execute(
"INSERT INTO VolumeSearchCache "
+ "(search_term, id, name, start_year, publisher, count_of_issues, image_url, description) "
+ "VALUES(?, ?, ?, ?, ?, ?, ?, ?)",
(
search_term.lower(),
record["id"],
record["name"],
record["start_year"],
"INSERT INTO VolumeSearchCache " +
"(search_term, id, name, start_year, publisher, count_of_issues, image_url, description) " +
"VALUES(?, ?, ?, ?, ?, ?, ?, ?)",
(search_term.lower(),
record['id'],
record['name'],
record['start_year'],
pub_name,
record["count_of_issues"],
record['count_of_issues'],
url,
record["description"],
),
)
record['description']))
def get_search_results(self, search_term):
results = []
results = list()
con = lite.connect(self.db_file)
with con:
con.text_factory = str
@ -168,23 +166,27 @@ class ComicVineCacher:
# purge stale search results
a_day_ago = datetime.datetime.today() - datetime.timedelta(days=1)
cur.execute("DELETE FROM VolumeSearchCache WHERE timestamp < ?", [str(a_day_ago)])
cur.execute(
"DELETE FROM VolumeSearchCache WHERE timestamp < ?", [
str(a_day_ago)])
# fetch
cur.execute("SELECT * FROM VolumeSearchCache WHERE search_term=?", [search_term.lower()])
cur.execute(
"SELECT * FROM VolumeSearchCache WHERE search_term=?", [search_term.lower()])
rows = cur.fetchall()
# now process the results
for record in rows:
result = {}
result["id"] = record[1]
result["name"] = record[2]
result["start_year"] = record[3]
result["publisher"] = {}
result["publisher"]["name"] = record[4]
result["count_of_issues"] = record[5]
result["image"] = {}
result["image"]["super_url"] = record[6]
result["description"] = record[7]
result = dict()
result['id'] = record[1]
result['name'] = record[2]
result['start_year'] = record[3]
result['publisher'] = dict()
result['publisher']['name'] = record[4]
result['count_of_issues'] = record[5]
result['image'] = dict()
result['image']['super_url'] = record[6]
result['description'] = record[7]
results.append(result)
@ -201,9 +203,14 @@ class ComicVineCacher:
# remove all previous entries with this search term
cur.execute("DELETE FROM AltCovers WHERE issue_id = ?", [issue_id])
url_list_str = utils.list_to_string(url_list)
url_list_str = utils.listToString(url_list)
# now add in new record
cur.execute("INSERT INTO AltCovers (issue_id, url_list) VALUES(?, ?)", (issue_id, url_list_str))
cur.execute("INSERT INTO AltCovers " +
"(issue_id, url_list) " +
"VALUES(?, ?)",
(issue_id,
url_list_str)
)
def get_alt_covers(self, issue_id):
@ -214,22 +221,26 @@ class ComicVineCacher:
# purge stale issue info - probably issue data won't change
# much....
a_month_ago = datetime.datetime.today() - datetime.timedelta(days=30)
cur.execute("DELETE FROM AltCovers WHERE timestamp < ?", [str(a_month_ago)])
a_month_ago = datetime.datetime.today() - \
datetime.timedelta(days=30)
cur.execute(
"DELETE FROM AltCovers WHERE timestamp < ?", [
str(a_month_ago)])
cur.execute("SELECT url_list FROM AltCovers WHERE issue_id=?", [issue_id])
cur.execute(
"SELECT url_list FROM AltCovers WHERE issue_id=?", [issue_id])
row = cur.fetchone()
if row is None:
return None
url_list_str = row[0]
if len(url_list_str) == 0:
return []
raw_list = url_list_str.split(",")
url_list = []
for item in raw_list:
url_list.append(str(item).strip())
return url_list
else:
url_list_str = row[0]
if len(url_list_str) == 0:
return []
raw_list = url_list_str.split(",")
url_list = []
for item in raw_list:
url_list.append(str(item).strip())
return url_list
def add_volume_info(self, cv_volume_record):
@ -241,25 +252,26 @@ class ComicVineCacher:
timestamp = datetime.datetime.now()
if cv_volume_record["publisher"] is None:
if cv_volume_record['publisher'] is None:
pub_name = ""
else:
pub_name = cv_volume_record["publisher"]["name"]
pub_name = cv_volume_record['publisher']['name']
data = {
"name": cv_volume_record["name"],
"name": cv_volume_record['name'],
"publisher": pub_name,
"count_of_issues": cv_volume_record["count_of_issues"],
"start_year": cv_volume_record["start_year"],
"timestamp": timestamp,
"count_of_issues": cv_volume_record['count_of_issues'],
"start_year": cv_volume_record['start_year'],
"timestamp": timestamp
}
self.upsert(cur, "volumes", "id", cv_volume_record["id"], data)
self.upsert(cur, "volumes", "id", cv_volume_record['id'], data)
def add_volume_issues_info(self, volume_id, cv_volume_issues):
con = lite.connect(self.db_file)
with con:
cur = con.cursor()
timestamp = datetime.datetime.now()
@ -267,18 +279,19 @@ class ComicVineCacher:
# add in issues
for issue in cv_volume_issues:
data = {
"volume_id": volume_id,
"name": issue["name"],
"issue_number": issue["issue_number"],
"site_detail_url": issue["site_detail_url"],
"cover_date": issue["cover_date"],
"super_url": issue["image"]["super_url"],
"thumb_url": issue["image"]["thumb_url"],
"description": issue["description"],
"timestamp": timestamp,
"name": issue['name'],
"issue_number": issue['issue_number'],
"site_detail_url": issue['site_detail_url'],
"cover_date": issue['cover_date'],
"super_url": issue['image']['super_url'],
"thumb_url": issue['image']['thumb_url'],
"description": issue['description'],
"timestamp": timestamp
}
self.upsert(cur, "issues", "id", issue["id"], data)
self.upsert(cur, "issues", "id", issue['id'], data)
def get_volume_info(self, volume_id):
@ -291,31 +304,36 @@ class ComicVineCacher:
# purge stale volume info
a_week_ago = datetime.datetime.today() - datetime.timedelta(days=7)
cur.execute("DELETE FROM Volumes WHERE timestamp < ?", [str(a_week_ago)])
cur.execute(
"DELETE FROM Volumes WHERE timestamp < ?", [str(a_week_ago)])
# fetch
cur.execute("SELECT id,name,publisher,count_of_issues,start_year FROM Volumes WHERE id = ?", [volume_id])
cur.execute(
"SELECT id,name,publisher,count_of_issues,start_year FROM Volumes WHERE id = ?",
[volume_id])
row = cur.fetchone()
if row is None:
return result
result = {}
result = dict()
# since ID is primary key, there is only one row
result["id"] = row[0]
result["name"] = row[1]
result["publisher"] = {}
result["publisher"]["name"] = row[2]
result["count_of_issues"] = row[3]
result["start_year"] = row[4]
result["issues"] = []
result['id'] = row[0]
result['name'] = row[1]
result['publisher'] = dict()
result['publisher']['name'] = row[2]
result['count_of_issues'] = row[3]
result['start_year'] = row[4]
result['issues'] = list()
return result
def get_volume_issues_info(self, volume_id):
result = None
con = lite.connect(self.db_file)
with con:
cur = con.cursor()
@ -324,30 +342,30 @@ class ComicVineCacher:
# purge stale issue info - probably issue data won't change
# much....
a_week_ago = datetime.datetime.today() - datetime.timedelta(days=7)
cur.execute("DELETE FROM Issues WHERE timestamp < ?", [str(a_week_ago)])
cur.execute(
"DELETE FROM Issues WHERE timestamp < ?", [str(a_week_ago)])
# fetch
results = []
results = list()
cur.execute(
"SELECT id,name,issue_number,site_detail_url,cover_date,super_url,thumb_url,description FROM Issues WHERE volume_id = ?",
[volume_id],
)
[volume_id])
rows = cur.fetchall()
# now process the results
for row in rows:
record = {}
record = dict()
record["id"] = row[0]
record["name"] = row[1]
record["issue_number"] = row[2]
record["site_detail_url"] = row[3]
record["cover_date"] = row[4]
record["image"] = {}
record["image"]["super_url"] = row[5]
record["image"]["thumb_url"] = row[6]
record["description"] = row[7]
record['id'] = row[0]
record['name'] = row[1]
record['issue_number'] = row[2]
record['site_detail_url'] = row[3]
record['cover_date'] = row[4]
record['image'] = dict()
record['image']['super_url'] = row[5]
record['image']['thumb_url'] = row[6]
record['description'] = row[7]
results.append(record)
@ -356,7 +374,13 @@ class ComicVineCacher:
return results
def add_issue_select_details(self, issue_id, image_url, thumb_image_url, cover_date, site_detail_url):
def add_issue_select_details(
self,
issue_id,
image_url,
thumb_image_url,
cover_date,
site_detail_url):
con = lite.connect(self.db_file)
@ -370,7 +394,7 @@ class ComicVineCacher:
"thumb_url": thumb_image_url,
"cover_date": cover_date,
"site_detail_url": site_detail_url,
"timestamp": timestamp,
"timestamp": timestamp
}
self.upsert(cur, "issues", "id", issue_id, data)
@ -381,21 +405,23 @@ class ComicVineCacher:
cur = con.cursor()
con.text_factory = str
cur.execute("SELECT super_url,thumb_url,cover_date,site_detail_url FROM Issues WHERE id=?", [issue_id])
cur.execute(
"SELECT super_url,thumb_url,cover_date,site_detail_url FROM Issues WHERE id=?",
[issue_id])
row = cur.fetchone()
details = {}
details = dict()
if row is None or row[0] is None:
details["image_url"] = None
details["thumb_image_url"] = None
details["cover_date"] = None
details["site_detail_url"] = None
details['image_url'] = None
details['thumb_image_url'] = None
details['cover_date'] = None
details['site_detail_url'] = None
else:
details["image_url"] = row[0]
details["thumb_image_url"] = row[1]
details["cover_date"] = row[2]
details["site_detail_url"] = row[3]
details['image_url'] = row[0]
details['thumb_image_url'] = row[1]
details['cover_date'] = row[2]
details['site_detail_url'] = row[3]
return details
@ -407,8 +433,10 @@ class ComicVineCacher:
TODO: should the cursor be created here, and not up the stack?
"""
ins_count = len(data) + 1
keys = ""
vals = []
vals = list()
ins_slots = ""
set_slots = ""
@ -431,8 +459,11 @@ class ComicVineCacher:
ins_slots += ", ?"
condition = pkname + " = ?"
sql_ins = f"INSERT OR IGNORE INTO {tablename} ({keys}) VALUES ({ins_slots})"
sql_ins = ("INSERT OR IGNORE INTO " + tablename +
" (" + keys + ") " +
" VALUES (" + ins_slots + ")")
cur.execute(sql_ins, vals)
sql_upd = f"UPDATE {tablename} SET {set_slots} WHERE {condition}"
sql_upd = ("UPDATE " + tablename +
" SET " + set_slots + " WHERE " + condition)
cur.execute(sql_upd, vals)

File diff suppressed because it is too large Load Diff

View File

@ -18,113 +18,82 @@ TODO: This should be re-factored using subclasses!
# See the License for the specific language governing permissions and
# limitations under the License.
#import os
import logging
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5 import uic
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comicapi.comicarchive import ComicArchive
from comictaggerlib.comicvinetalker import ComicVineTalker
from comictaggerlib.imagefetcher import ImageFetcher
from comictaggerlib.imagepopup import ImagePopup
from comictaggerlib.pageloader import PageLoader
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.ui.qtutils import get_qimage_from_data, reduce_widget_font_size
logger = logging.getLogger(__name__)
from .settings import ComicTaggerSettings
from .comicvinetalker import ComicVineTalker, ComicVineTalkerException
from .imagefetcher import ImageFetcher
from .pageloader import PageLoader
from .imagepopup import ImagePopup
from comictaggerlib.ui.qtutils import reduceWidgetFontSize, getQImageFromData
#from genericmetadata import GenericMetadata, PageType
#from comicarchive import MetaDataStyle
#import utils
def clickable(widget):
"""Allow a label to be clickable"""
"""# Allow a label to be clickable"""
class Filter(QtCore.QObject):
class Filter(QObject):
dblclicked = QtCore.pyqtSignal()
dblclicked = pyqtSignal()
def eventFilter(self, obj, event):
if obj == widget:
if event.type() == QtCore.QEvent.Type.MouseButtonDblClick:
if event.type() == QEvent.MouseButtonDblClick:
self.dblclicked.emit()
return True
return False
flt = Filter(widget)
widget.installEventFilter(flt)
return flt.dblclicked
filter = Filter(widget)
widget.installEventFilter(filter)
return filter.dblclicked
class Signal(QtCore.QObject):
alt_url_list_fetch_complete = QtCore.pyqtSignal(list)
url_fetch_complete = QtCore.pyqtSignal(str, str)
image_fetch_complete = QtCore.pyqtSignal(QtCore.QByteArray)
class CoverImageWidget(QWidget):
def __init__(self, list_fetch, url_fetch, image_fetch):
super().__init__()
self.alt_url_list_fetch_complete.connect(list_fetch)
self.url_fetch_complete.connect(url_fetch)
self.image_fetch_complete.connect(image_fetch)
def emit_list(self, url_list: list):
self.alt_url_list_fetch_complete.emit(url_list)
def emit_url(self, image_url: str, thumb_url: str):
self.url_fetch_complete.emit(image_url, thumb_url)
def emit_image(self, image_data: QtCore.QByteArray):
self.image_fetch_complete.emit(image_data)
class CoverImageWidget(QtWidgets.QWidget):
ArchiveMode = 0
AltCoverMode = 1
URLMode = 1
DataMode = 3
def __init__(self, parent, mode, expand_on_click=True):
super().__init__(parent)
super(CoverImageWidget, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.get_ui_file("coverimagewidget.ui"), self)
uic.loadUi(ComicTaggerSettings.getUIFile('coverimagewidget.ui'), self)
reduce_widget_font_size(self.label)
self.sig = Signal(
self.alt_cover_url_list_fetch_complete, self.primary_url_fetch_complete, self.cover_remote_fetch_complete
)
reduceWidgetFontSize(self.label)
self.mode = mode
self.comicVine = ComicVineTalker()
self.page_loader = None
self.showControls = True
self.current_pixmap = QtGui.QPixmap()
self.btnLeft.setIcon(QIcon(ComicTaggerSettings.getGraphic('left.png')))
self.btnRight.setIcon(
QIcon(ComicTaggerSettings.getGraphic('right.png')))
self.comic_archive = None
self.issue_id = None
self.cover_fetcher = None
self.url_list = []
if self.page_loader is not None:
self.page_loader.abandoned = True
self.page_loader = None
self.imageIndex = -1
self.imageCount = 1
self.imageData = None
self.btnLeft.setIcon(QtGui.QIcon(ComicTaggerSettings.get_graphic("left.png")))
self.btnRight.setIcon(QtGui.QIcon(ComicTaggerSettings.get_graphic("right.png")))
self.btnLeft.clicked.connect(self.decrement_image)
self.btnRight.clicked.connect(self.increment_image)
self.btnLeft.clicked.connect(self.decrementImage)
self.btnRight.clicked.connect(self.incrementImage)
self.resetWidget()
if expand_on_click:
clickable(self.lblImage).connect(self.show_popup)
clickable(self.lblImage).connect(self.showPopup)
else:
self.lblImage.setToolTip("")
self.update_content()
self.updateContent()
def reset_widget(self):
def resetWidget(self):
self.comic_archive = None
self.issue_id = None
self.comicVine = None
self.cover_fetcher = None
self.url_list = []
if self.page_loader is not None:
@ -135,52 +104,54 @@ class CoverImageWidget(QtWidgets.QWidget):
self.imageData = None
def clear(self):
self.reset_widget()
self.update_content()
self.resetWidget()
self.updateContent()
def increment_image(self):
def incrementImage(self):
self.imageIndex += 1
if self.imageIndex == self.imageCount:
self.imageIndex = 0
self.update_content()
self.updateContent()
def decrement_image(self):
def decrementImage(self):
self.imageIndex -= 1
if self.imageIndex == -1:
self.imageIndex = self.imageCount - 1
self.update_content()
self.updateContent()
def set_archive(self, ca: ComicArchive, page=0):
def setArchive(self, ca, page=0):
if self.mode == CoverImageWidget.ArchiveMode:
self.reset_widget()
self.resetWidget()
self.comic_archive = ca
self.imageIndex = page
self.imageCount = ca.get_number_of_pages()
self.update_content()
self.imageCount = ca.getNumberOfPages()
self.updateContent()
def set_url(self, url):
def setURL(self, url):
if self.mode == CoverImageWidget.URLMode:
self.reset_widget()
self.update_content()
self.resetWidget()
self.updateContent()
self.url_list = [url]
self.imageIndex = 0
self.imageCount = 1
self.update_content()
self.updateContent()
def set_issue_id(self, issue_id):
def setIssueID(self, issue_id):
if self.mode == CoverImageWidget.AltCoverMode:
self.reset_widget()
self.update_content()
self.resetWidget()
self.updateContent()
self.issue_id = issue_id
comic_vine = ComicVineTalker()
comic_vine.url_fetch_complete = self.sig.emit_url
comic_vine.async_fetch_issue_cover_urls(int(self.issue_id))
self.comicVine = ComicVineTalker()
self.comicVine.urlFetchComplete.connect(
self.primaryUrlFetchComplete)
self.comicVine.asyncFetchIssueCoverURLs(int(self.issue_id))
def set_image_data(self, image_data):
def setImageData(self, image_data):
if self.mode == CoverImageWidget.DataMode:
self.reset_widget()
self.resetWidget()
if image_data is None:
self.imageIndex = -1
@ -188,54 +159,56 @@ class CoverImageWidget(QtWidgets.QWidget):
self.imageIndex = 0
self.imageData = image_data
self.update_content()
self.updateContent()
def primary_url_fetch_complete(self, primary_url, thumb_url):
def primaryUrlFetchComplete(self, primary_url, thumb_url, issue_id):
self.url_list.append(str(primary_url))
self.imageIndex = 0
self.imageCount = len(self.url_list)
self.update_content()
self.updateContent()
# defer the alt cover search
QtCore.QTimer.singleShot(1, self.start_alt_cover_search)
QTimer.singleShot(1, self.startAltCoverSearch)
def start_alt_cover_search(self):
def startAltCoverSearch(self):
# now we need to get the list of alt cover URLs
self.label.setText("Searching for alt. covers...")
# page URL should already be cached, so no need to defer
comic_vine = ComicVineTalker()
issue_page_url = comic_vine.fetch_issue_page_url(self.issue_id)
comic_vine.alt_url_list_fetch_complete = self.sig.emit_list
comic_vine.async_fetch_alternate_cover_urls(int(self.issue_id), issue_page_url)
self.comicVine = ComicVineTalker()
issue_page_url = self.comicVine.fetchIssuePageURL(self.issue_id)
self.comicVine.altUrlListFetchComplete.connect(
self.altCoverUrlListFetchComplete)
self.comicVine.asyncFetchAlternateCoverURLs(
int(self.issue_id), issue_page_url)
def alt_cover_url_list_fetch_complete(self, url_list):
def altCoverUrlListFetchComplete(self, url_list, issue_id):
if len(url_list) > 0:
self.url_list.extend(url_list)
self.imageCount = len(self.url_list)
self.update_controls()
self.updateControls()
def set_page(self, pagenum):
def setPage(self, pagenum):
if self.mode == CoverImageWidget.ArchiveMode:
self.imageIndex = pagenum
self.update_content()
self.updateContent()
def update_content(self):
self.update_image()
self.update_controls()
def updateContent(self):
self.updateImage()
self.updateControls()
def update_image(self):
def updateImage(self):
if self.imageIndex == -1:
self.load_default()
self.loadDefault()
elif self.mode in [CoverImageWidget.AltCoverMode, CoverImageWidget.URLMode]:
self.load_url()
self.loadURL()
elif self.mode == CoverImageWidget.DataMode:
self.cover_remote_fetch_complete(self.imageData)
self.coverRemoteFetchComplete(self.imageData, 0)
else:
self.load_page()
self.loadPage()
def update_controls(self):
def updateControls(self):
if not self.showControls or self.mode == CoverImageWidget.DataMode:
self.btnLeft.hide()
self.btnRight.hide()
@ -256,47 +229,71 @@ class CoverImageWidget(QtWidgets.QWidget):
if self.imageIndex == -1 or self.imageCount == 1:
self.label.setText("")
elif self.mode == CoverImageWidget.AltCoverMode:
self.label.setText(f"Cover {self.imageIndex + 1} (of {self.imageCount})")
self.label.setText(
"Cover {0} (of {1})".format(
self.imageIndex + 1,
self.imageCount))
else:
self.label.setText(f"Page {self.imageIndex + 1} (of {self.imageCount})")
self.label.setText(
"Page {0} (of {1})".format(
self.imageIndex + 1,
self.imageCount))
def load_url(self):
self.load_default()
def loadURL(self):
self.loadDefault()
self.cover_fetcher = ImageFetcher()
self.cover_fetcher.image_fetch_complete = self.sig.emit_image
self.cover_fetcher.fetchComplete.connect(self.coverRemoteFetchComplete)
self.cover_fetcher.fetch(self.url_list[self.imageIndex])
#print("ATB cover fetch started...")
# called when the image is done loading from internet
def cover_remote_fetch_complete(self, image_data):
img = get_qimage_from_data(image_data)
self.current_pixmap = QtGui.QPixmap.fromImage(img)
self.set_display_pixmap()
def coverRemoteFetchComplete(self, image_data, issue_id):
img = getQImageFromData(image_data)
self.current_pixmap = QPixmap(img)
self.setDisplayPixmap(0, 0)
#print("ATB cover fetch complete!")
def load_page(self):
def loadPage(self):
if self.comic_archive is not None:
if self.page_loader is not None:
self.page_loader.abandoned = True
self.page_loader = PageLoader(self.comic_archive, self.imageIndex)
self.page_loader.loadComplete.connect(self.page_load_complete)
self.page_loader.loadComplete.connect(self.pageLoadComplete)
self.page_loader.start()
def page_load_complete(self, image_data):
img = get_qimage_from_data(image_data)
self.current_pixmap = QtGui.QPixmap.fromImage(img)
self.set_display_pixmap()
def pageLoadComplete(self, img):
self.current_pixmap = QPixmap(img)
self.setDisplayPixmap(0, 0)
self.page_loader = None
def load_default(self):
self.current_pixmap = QtGui.QPixmap(ComicTaggerSettings.get_graphic("nocover.png"))
self.set_display_pixmap()
def loadDefault(self):
self.current_pixmap = QPixmap(
ComicTaggerSettings.getGraphic('nocover.png'))
#print("loadDefault called")
self.setDisplayPixmap(0, 0)
def resizeEvent(self, resize_event):
if self.current_pixmap is not None:
self.set_display_pixmap()
delta_w = resize_event.size().width() - \
resize_event.oldSize().width()
delta_h = resize_event.size().height() - \
resize_event.oldSize().height()
# print "ATB resizeEvent deltas", resize_event.size().width(),
# resize_event.size().height()
self.setDisplayPixmap(delta_w, delta_h)
def set_display_pixmap(self):
def setDisplayPixmap(self, delta_w, delta_h):
"""The deltas let us know what the new width and height of the label will be"""
#new_h = self.frame.height() + delta_h
#new_w = self.frame.width() + delta_w
# print "ATB setDisplayPixmap deltas", delta_w , delta_h
# print "ATB self.frame", self.frame.width(), self.frame.height()
# print "ATB self.", self.width(), self.height()
#frame_w = new_w
#frame_h = new_h
new_h = self.frame.height()
new_w = self.frame.width()
frame_w = self.frame.width()
@ -305,18 +302,25 @@ class CoverImageWidget(QtWidgets.QWidget):
new_h -= 4
new_w -= 4
new_h = max(new_h, 0)
new_w = max(new_w, 0)
if new_h < 0:
new_h = 0
if new_w < 0:
new_w = 0
# print "ATB setDisplayPixmap deltas", delta_w , delta_h
# print "ATB self.frame", frame_w, frame_h
# print "ATB new size", new_w, new_h
# scale the pixmap to fit in the frame
scaled_pixmap = self.current_pixmap.scaled(new_w, new_h, QtCore.Qt.AspectRatioMode.KeepAspectRatio)
scaled_pixmap = self.current_pixmap.scaled(
new_w, new_h, Qt.KeepAspectRatio)
self.lblImage.setPixmap(scaled_pixmap)
# move and resize the label to be centered in the fame
img_w = scaled_pixmap.width()
img_h = scaled_pixmap.height()
self.lblImage.resize(img_w, img_h)
self.lblImage.move(int((frame_w - img_w) / 2), int((frame_h - img_h) / 2))
self.lblImage.move((frame_w - img_w) / 2, (frame_h - img_h) / 2)
def show_popup(self):
ImagePopup(self, self.current_pixmap)
def showPopup(self):
self.popup = ImagePopup(self, self.current_pixmap)

View File

@ -14,24 +14,23 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#import os
import logging
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5 import QtCore, QtWidgets, uic
from comictaggerlib.settings import ComicTaggerSettings
logger = logging.getLogger(__name__)
from .settings import ComicTaggerSettings
class CreditEditorWindow(QtWidgets.QDialog):
ModeEdit = 0
ModeNew = 1
def __init__(self, parent, mode, role, name, primary):
super().__init__(parent)
super(CreditEditorWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.get_ui_file("crediteditorwindow.ui"), self)
uic.loadUi(
ComicTaggerSettings.getUIFile('crediteditorwindow.ui'), self)
self.mode = mode
@ -64,33 +63,34 @@ class CreditEditorWindow(QtWidgets.QDialog):
self.cbRole.setCurrentIndex(i)
if primary:
self.cbPrimary.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbPrimary.setCheckState(QtCore.Qt.Checked)
self.cbRole.currentIndexChanged.connect(self.role_changed)
self.cbRole.editTextChanged.connect(self.role_changed)
self.cbRole.currentIndexChanged.connect(self.roleChanged)
self.cbRole.editTextChanged.connect(self.roleChanged)
self.update_primary_button()
self.updatePrimaryButton()
def update_primary_button(self):
enabled = self.current_role_can_be_primary()
def updatePrimaryButton(self):
enabled = self.currentRoleCanBePrimary()
self.cbPrimary.setEnabled(enabled)
def current_role_can_be_primary(self):
def currentRoleCanBePrimary(self):
role = self.cbRole.currentText()
if str(role).lower() == "writer" or str(role).lower() == "artist":
return True
else:
return False
return False
def roleChanged(self, s):
self.updatePrimaryButton()
def role_changed(self, s):
self.update_primary_button()
def get_credits(self):
primary = self.current_role_can_be_primary() and self.cbPrimary.isChecked()
def getCredits(self):
primary = self.currentRoleCanBePrimary() and self.cbPrimary.isChecked()
return self.cbRole.currentText(), self.leName.text(), primary
def accept(self):
if self.cbRole.currentText() == "" or self.leName.text() == "":
QtWidgets.QMessageBox.warning(self, "Whoops", "You need to enter both role and name for a credit.")
QtWidgets.QMessageBox.warning(self, self.tr("Whoops"), self.tr(
"You need to enter both role and name for a credit."))
else:
QtWidgets.QDialog.accept(self)

View File

@ -14,14 +14,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#import os
import logging
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5 import QtCore, QtWidgets, uic
from comictaggerlib.settings import ComicTaggerSettings
logger = logging.getLogger(__name__)
from .settings import ComicTaggerSettings
#from settingswindow import SettingsWindow
#from filerenamer import FileRenamer
#import utils
class ExportConflictOpts:
@ -31,20 +31,20 @@ class ExportConflictOpts:
class ExportWindow(QtWidgets.QDialog):
def __init__(self, parent, settings, msg):
super().__init__(parent)
uic.loadUi(ComicTaggerSettings.get_ui_file("exportwindow.ui"), self)
def __init__(self, parent, settings, msg):
super(ExportWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile('exportwindow.ui'), self)
self.label.setText(msg)
self.setWindowFlags(
QtCore.Qt.WindowType(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint)
)
self.setWindowFlags(self.windowFlags() &
~QtCore.Qt.WindowContextHelpButtonHint)
self.settings = settings
self.cbxDeleteOriginal.setCheckState(QtCore.Qt.CheckState.Unchecked)
self.cbxAddToList.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxDeleteOriginal.setCheckState(QtCore.Qt.Unchecked)
self.cbxAddToList.setCheckState(QtCore.Qt.Checked)
self.radioDontCreate.setChecked(True)
self.deleteOriginal = False
@ -60,3 +60,5 @@ class ExportWindow(QtWidgets.QDialog):
self.fileConflictBehavior = ExportConflictOpts.dontCreate
elif self.radioCreateNew.isChecked():
self.fileConflictBehavior = ExportConflictOpts.createUnique
# else:
# self.fileConflictBehavior = ExportConflictOpts.overwrite

View File

@ -0,0 +1 @@
from comicapi.filenameparser import *

View File

@ -14,131 +14,143 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
import logging
import os
import re
import datetime
import sys
import string
from comicapi.genericmetadata import GenericMetadata
from comicapi.issuestring import IssueString
from pathvalidate import sanitize_filepath
logger = logging.getLogger(__name__)
from . import utils
from .issuestring import IssueString
class MetadataFormatter(string.Formatter):
def __init__(self, smart_cleanup=False):
super().__init__()
self.smart_cleanup = smart_cleanup
def format_field(self, value, format_spec):
if value is None or value == "":
return ""
return super().format_field(value, format_spec)
def _vformat(self, format_string, args, kwargs, used_args, recursion_depth,
auto_arg_index=0):
if recursion_depth < 0:
raise ValueError('Max string recursion exceeded')
result = []
lstrip = False
for literal_text, field_name, format_spec, conversion in \
self.parse(format_string):
# output the literal text
if literal_text:
if lstrip:
result.append(literal_text.lstrip("-_)}]#"))
else:
result.append(literal_text)
lstrip = False
# if there's a field, output it
if field_name is not None:
# this is some markup, find the object and do
# the formatting
# handle arg indexing when empty field_names are given.
if field_name == '':
if auto_arg_index is False:
raise ValueError('cannot switch from manual field '
'specification to automatic field '
'numbering')
field_name = str(auto_arg_index)
auto_arg_index += 1
elif field_name.isdigit():
if auto_arg_index:
raise ValueError('cannot switch from manual field '
'specification to automatic field '
'numbering')
# disable auto arg incrementing, if it gets
# used later on, then an exception will be raised
auto_arg_index = False
# given the field_name, find the object it references
# and the argument it came from
obj, arg_used = self.get_field(field_name, args, kwargs)
used_args.add(arg_used)
# do any conversion on the resulting object
obj = self.convert_field(obj, conversion)
# expand the format spec, if needed
format_spec, auto_arg_index = self._vformat(
format_spec, args, kwargs,
used_args, recursion_depth-1,
auto_arg_index=auto_arg_index)
# format the object and append to the result
fmtObj = self.format_field(obj, format_spec)
if fmtObj == "" and len(result) > 0 and self.smart_cleanup:
lstrip = True
result.pop()
result.append(fmtObj)
return ''.join(result), auto_arg_index
class FileRenamer:
def __init__(self, metadata):
self.template = "%series% v%volume% #%issue% (of %issuecount%) (%year%)"
self.setMetadata(metadata)
self.setTemplate(
"{publisher}/{series}/{series} v{volume} #{issue} (of {issueCount}) ({year})")
self.smart_cleanup = True
self.issue_zero_padding = 3
self.metadata = metadata
self.move = False
def set_metadata(self, metadata: GenericMetadata):
self.metadata = metadata
def setMetadata(self, metadata):
self.metdata = metadata
def set_issue_zero_padding(self, count):
def setIssueZeroPadding(self, count):
self.issue_zero_padding = count
def set_smart_cleanup(self, on):
def setSmartCleanup(self, on):
self.smart_cleanup = on
def set_template(self, template: str):
def setTemplate(self, template):
self.template = template
def replace_token(self, text, value, token):
# helper func
def is_token(word):
return word[0] == "%" and word[-1:] == "%"
def determineName(self, filename, ext=None):
class Default(dict):
def __missing__(self, key):
return "{" + key + "}"
md = self.metdata
if value is not None:
return text.replace(token, str(value))
if self.smart_cleanup:
# smart cleanup means we want to remove anything appended to token if it's empty (e.g "#%issue%" or "v%volume%")
# (TODO: This could fail if there is more than one token appended together, I guess)
text_list = text.split()
# padding for issue
md.issue = IssueString(md.issue).asString(pad=self.issue_zero_padding)
# special case for issuecount, remove preceding non-token word,
# as in "...(of %issuecount%)..."
if token == "%issuecount%":
for idx, word in enumerate(text_list):
if token in word and not is_token(text_list[idx - 1]):
text_list[idx - 1] = ""
template = self.template
text_list = [x for x in text_list if token not in x]
return " ".join(text_list)
pathComponents = template.split(os.sep)
new_name = ""
return text.replace(token, "")
fmt = MetadataFormatter(self.smart_cleanup)
for Component in pathComponents:
new_name = os.path.join(new_name, fmt.vformat(Component, args=[], kwargs=Default(vars(md))).replace("/", "-"))
def determine_name(self, filename, ext=None):
md = self.metadata
new_name = self.template
new_name = self.replace_token(new_name, md.series, "%series%")
new_name = self.replace_token(new_name, md.volume, "%volume%")
if md.issue is not None:
issue_str = IssueString(md.issue).as_string(pad=self.issue_zero_padding)
else:
issue_str = None
new_name = self.replace_token(new_name, issue_str, "%issue%")
new_name = self.replace_token(new_name, md.issue_count, "%issuecount%")
new_name = self.replace_token(new_name, md.year, "%year%")
new_name = self.replace_token(new_name, md.publisher, "%publisher%")
new_name = self.replace_token(new_name, md.title, "%title%")
new_name = self.replace_token(new_name, md.month, "%month%")
month_name = None
if md.month is not None:
if (isinstance(md.month, str) and md.month.isdigit()) or isinstance(md.month, int):
if int(md.month) in range(1, 13):
dt = datetime.datetime(1970, int(md.month), 1, 0, 0)
month_name = dt.strftime("%B")
new_name = self.replace_token(new_name, month_name, "%month_name%")
new_name = self.replace_token(new_name, md.genre, "%genre%")
new_name = self.replace_token(new_name, md.language, "%language_code%")
new_name = self.replace_token(new_name, md.critical_rating, "%criticalrating%")
new_name = self.replace_token(new_name, md.alternate_series, "%alternateseries%")
new_name = self.replace_token(new_name, md.alternate_number, "%alternatenumber%")
new_name = self.replace_token(new_name, md.alternate_count, "%alternatecount%")
new_name = self.replace_token(new_name, md.imprint, "%imprint%")
new_name = self.replace_token(new_name, md.format, "%format%")
new_name = self.replace_token(new_name, md.maturity_rating, "%maturityrating%")
new_name = self.replace_token(new_name, md.story_arc, "%storyarc%")
new_name = self.replace_token(new_name, md.series_group, "%seriesgroup%")
new_name = self.replace_token(new_name, md.scan_info, "%scaninfo%")
if self.smart_cleanup:
# remove empty braces,brackets, parentheses
new_name = re.sub(r"\(\s*[-:]*\s*\)", "", new_name)
new_name = re.sub(r"\[\s*[-:]*\s*]", "", new_name)
new_name = re.sub(r"{\s*[-:]*\s*}", "", new_name)
# remove duplicate spaces
new_name = " ".join(new_name.split())
# remove remove duplicate -, _,
new_name = re.sub(r"[-_]{2,}\s+", "-- ", new_name)
new_name = re.sub(r"(\s--)+", " --", new_name)
new_name = re.sub(r"(\s-)+", " -", new_name)
# remove dash or double dash at end of line
new_name = re.sub(r"[-]{1,2}\s*$", "", new_name)
# remove duplicate spaces (again!)
new_name = " ".join(new_name.split())
if ext is None:
if ext is None or ext == "":
ext = os.path.splitext(filename)[1]
new_name += ext
# some tweaks to keep various filesystems happy
new_name = new_name.replace("/", "-")
new_name = new_name.replace(" :", " -")
new_name = new_name.replace(": ", " - ")
new_name = new_name.replace(":", "-")
new_name = new_name.replace("?", "")
return new_name
# remove padding
md.issue = IssueString(md.issue).asString()
if self.move:
return sanitize_filepath(new_name.strip())
else:
return os.path.basename(sanitize_filepath(new_name.strip()))

View File

@ -1,3 +1,4 @@
# coding=utf-8
"""A PyQt5 widget for managing list of comic archive files"""
# Copyright 2012-2014 Anthony Beville
@ -14,33 +15,45 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import platform
import os
from typing import List
#import os
import sys
from PyQt5 import QtCore, QtWidgets, uic
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5 import uic
from PyQt5.QtCore import pyqtSignal
from comicapi import utils
from comicapi.comicarchive import ComicArchive
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.ui.qtutils import center_window_on_parent, reduce_widget_font_size
logger = logging.getLogger(__name__)
from .settings import ComicTaggerSettings
from .comicarchive import ComicArchive
from .optionalmsgdialog import OptionalMessageDialog
from comictaggerlib.ui.qtutils import reduceWidgetFontSize, centerWindowOnParent
from . import utils
#from comicarchive import MetaDataStyle
#from genericmetadata import GenericMetadata, PageType
class FileTableWidgetItem(QtWidgets.QTableWidgetItem):
class FileTableWidgetItem(QTableWidgetItem):
def __lt__(self, other):
return self.data(QtCore.Qt.ItemDataRole.UserRole) < other.data(QtCore.Qt.ItemDataRole.UserRole)
#return (self.data(Qt.UserRole).toBool() <
# other.data(Qt.UserRole).toBool())
return (self.data(Qt.UserRole) <
other.data(Qt.UserRole))
class FileInfo:
def __init__(self, ca: ComicArchive):
self.ca: ComicArchive = ca
class FileInfo():
def __init__(self, ca):
self.ca = ca
class FileSelectionList(QtWidgets.QWidget):
selectionChanged = QtCore.pyqtSignal(QtCore.QVariant)
listCleared = QtCore.pyqtSignal()
class FileSelectionList(QWidget):
selectionChanged = pyqtSignal(QVariant)
listCleared = pyqtSignal()
fileColNum = 0
CRFlagColNum = 1
@ -50,77 +63,92 @@ class FileSelectionList(QtWidgets.QWidget):
folderColNum = 5
dataColNum = fileColNum
def __init__(self, parent, settings, dirty_flag_verification):
super().__init__(parent)
def __init__(self, parent, settings):
super(FileSelectionList, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.get_ui_file("fileselectionlist.ui"), self)
uic.loadUi(ComicTaggerSettings.getUIFile('fileselectionlist.ui'), self)
self.settings = settings
reduce_widget_font_size(self.twList)
reduceWidgetFontSize(self.twList)
self.twList.setColumnCount(6)
self.twList.currentItemChanged.connect(self.current_item_changed_cb)
#self.twlist.setHorizontalHeaderLabels (["File", "Folder", "CR", "CBL", ""])
# self.twList.horizontalHeader().setStretchLastSection(True)
self.twList.currentItemChanged.connect(self.currentItemChangedCB)
self.currentItem = None
self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.ActionsContextMenu)
self.dirty_flag = False
self.setContextMenuPolicy(Qt.ActionsContextMenu)
self.modifiedFlag = False
select_all_action = QtWidgets.QAction("Select All", self)
remove_action = QtWidgets.QAction("Remove Selected Items", self)
self.separator = QtWidgets.QAction("", self)
selectAllAction = QAction("Select All", self)
removeAction = QAction("Remove Selected Items", self)
self.separator = QAction("", self)
self.separator.setSeparator(True)
select_all_action.setShortcut("Ctrl+A")
remove_action.setShortcut("Ctrl+X")
selectAllAction.setShortcut('Ctrl+A')
removeAction.setShortcut('Ctrl+X')
select_all_action.triggered.connect(self.select_all)
remove_action.triggered.connect(self.remove_selection)
selectAllAction.triggered.connect(self.selectAll)
removeAction.triggered.connect(self.removeSelection)
self.addAction(select_all_action)
self.addAction(remove_action)
self.addAction(selectAllAction)
self.addAction(removeAction)
self.addAction(self.separator)
self.dirty_flag_verification = dirty_flag_verification
def get_sorting(self) -> (int, int):
def getSorting(self):
col = self.twList.horizontalHeader().sortIndicatorSection()
order = self.twList.horizontalHeader().sortIndicatorOrder()
return int(col), int(order)
return col, order
def set_sorting(self, col: int, order: QtCore.Qt.SortOrder):
self.twList.horizontalHeader().setSortIndicator(col, order)
def setSorting(self, col, order):
col = self.twList.horizontalHeader().setSortIndicator(col, order)
def add_app_action(self, action):
self.insertAction(QtWidgets.QAction(), action)
def addAppAction(self, action):
self.insertAction(None, action)
def set_modified_flag(self, modified):
self.dirty_flag = modified
def setModifiedFlag(self, modified):
self.modifiedFlag = modified
def select_all(self):
self.twList.setRangeSelected(QtWidgets.QTableWidgetSelectionRange(0, 0, self.twList.rowCount() - 1, 5), True)
def selectAll(self):
self.twList.setRangeSelected(
QTableWidgetSelectionRange(
0,
0,
self.twList.rowCount() -
1,
5),
True)
def deselect_all(self):
self.twList.setRangeSelected(QtWidgets.QTableWidgetSelectionRange(0, 0, self.twList.rowCount() - 1, 5), False)
def deselectAll(self):
self.twList.setRangeSelected(
QTableWidgetSelectionRange(
0,
0,
self.twList.rowCount() -
1,
5),
False)
def remove_archive_list(self, ca_list):
def removeArchiveList(self, ca_list):
self.twList.setSortingEnabled(False)
for ca in ca_list:
for row in range(self.twList.rowCount()):
row_ca = self.get_archive_by_row(row)
row_ca = self.getArchiveByRow(row)
if row_ca == ca:
self.twList.removeRow(row)
break
self.twList.setSortingEnabled(True)
def get_archive_by_row(self, row):
fi = self.twList.item(row, FileSelectionList.dataColNum).data(QtCore.Qt.ItemDataRole.UserRole)
def getArchiveByRow(self, row):
fi = self.twList.item(row, FileSelectionList.dataColNum).data(
Qt.UserRole)
return fi.ca
def get_current_archive(self):
return self.get_archive_by_row(self.twList.currentRow())
def getCurrentArchive(self):
return self.getArchiveByRow(self.twList.currentRow())
def remove_selection(self):
def removeSelection(self):
row_list = []
for item in self.twList.selectedItems():
if item.column() == 0:
@ -130,71 +158,75 @@ class FileSelectionList(QtWidgets.QWidget):
return
if self.twList.currentRow() in row_list:
if not self.dirty_flag_verification(
"Remove Archive", "If you close this archive, data in the form will be lost. Are you sure?"
):
if not self.modifiedFlagVerification(
"Remove Archive",
"If you close this archive, data in the form will be lost. Are you sure?"):
return
row_list.sort()
row_list.reverse()
self.twList.currentItemChanged.disconnect(self.current_item_changed_cb)
self.twList.currentItemChanged.disconnect(self.currentItemChangedCB)
self.twList.setSortingEnabled(False)
for i in row_list:
self.twList.removeRow(i)
self.twList.setSortingEnabled(True)
self.twList.currentItemChanged.connect(self.current_item_changed_cb)
self.twList.currentItemChanged.connect(self.currentItemChangedCB)
if self.twList.rowCount() > 0:
# since on a removal, we select row 0, make sure callback occurs if
# we're already there
if self.twList.currentRow() == 0:
self.current_item_changed_cb(self.twList.currentItem(), None)
self.currentItemChangedCB(self.twList.currentItem(), None)
self.twList.selectRow(0)
else:
self.listCleared.emit()
def add_path_list(self, pathlist):
def addPathList(self, pathlist):
filelist = utils.get_recursive_filelist(pathlist)
# we now have a list of files to add
# Prog dialog on Linux flakes out for small range, so scale up
progdialog = QtWidgets.QProgressDialog("", "Cancel", 0, len(filelist), parent=self)
progdialog = QProgressDialog("", "Cancel", 0, len(filelist), parent=self)
progdialog.setWindowTitle("Adding Files")
progdialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
progdialog.setWindowModality(Qt.ApplicationModal)
progdialog.setMinimumDuration(300)
center_window_on_parent(progdialog)
QtCore.QCoreApplication.processEvents()
first_added = None
centerWindowOnParent(progdialog)
#QCoreApplication.processEvents()
#progdialog.show()
QCoreApplication.processEvents()
firstAdded = None
self.twList.setSortingEnabled(False)
for idx, f in enumerate(filelist):
QtCore.QCoreApplication.processEvents()
QCoreApplication.processEvents()
if progdialog.wasCanceled():
break
progdialog.setValue(idx + 1)
progdialog.setValue(idx+1)
progdialog.setLabelText(f)
center_window_on_parent(progdialog)
QtCore.QCoreApplication.processEvents()
row = self.add_path_item(f)
if first_added is None and row is not None:
first_added = row
centerWindowOnParent(progdialog)
QCoreApplication.processEvents()
row = self.addPathItem(f)
if firstAdded is None and row is not None:
firstAdded = row
progdialog.hide()
QtCore.QCoreApplication.processEvents()
QCoreApplication.processEvents()
if first_added is not None:
self.twList.selectRow(first_added)
if firstAdded is not None:
self.twList.selectRow(firstAdded)
else:
if len(pathlist) == 1 and os.path.isfile(pathlist[0]):
QtWidgets.QMessageBox.information(
self, "File Open", "Selected file doesn't seem to be a comic archive."
)
QMessageBox.information(self, self.tr("File Open"), self.tr(
"Selected file doesn't seem to be a comic archive."))
else:
QtWidgets.QMessageBox.information(self, "File/Folder Open", "No readable comic archives were found.")
QMessageBox.information(
self,
self.tr("File/Folder Open"),
self.tr("No readable comic archives were found."))
self.twList.setSortingEnabled(True)
@ -209,78 +241,84 @@ class FileSelectionList(QtWidgets.QWidget):
if self.twList.columnWidth(FileSelectionList.folderColNum) > 200:
self.twList.setColumnWidth(FileSelectionList.folderColNum, 200)
def is_list_dupe(self, path):
def isListDupe(self, path):
r = 0
while r < self.twList.rowCount():
ca = self.get_archive_by_row(r)
ca = self.getArchiveByRow(r)
if ca.path == path:
return True
r = r + 1
return False
def get_current_list_row(self, path):
def getCurrentListRow(self, path):
r = 0
while r < self.twList.rowCount():
ca = self.get_archive_by_row(r)
ca = self.getArchiveByRow(r)
if ca.path == path:
return r
r = r + 1
return -1
def add_path_item(self, path):
def addPathItem(self, path):
path = str(path)
path = os.path.abspath(path)
# print "processing", path
if self.is_list_dupe(path):
return self.get_current_list_row(path)
if self.isListDupe(path):
return self.getCurrentListRow(path)
ca = ComicArchive(path, self.settings.rar_exe_path, ComicTaggerSettings.get_graphic("nocover.png"))
ca = ComicArchive(
path,
self.settings.rar_exe_path,
ComicTaggerSettings.getGraphic('nocover.png'))
if ca.seems_to_be_a_comic_archive():
if ca.seemsToBeAComicArchive():
row = self.twList.rowCount()
self.twList.insertRow(row)
fi = FileInfo(ca)
filename_item = QtWidgets.QTableWidgetItem()
folder_item = QtWidgets.QTableWidgetItem()
filename_item = QTableWidgetItem()
folder_item = QTableWidgetItem()
cix_item = FileTableWidgetItem()
cbi_item = FileTableWidgetItem()
readonly_item = FileTableWidgetItem()
type_item = QtWidgets.QTableWidgetItem()
type_item = QTableWidgetItem()
filename_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
filename_item.setData(QtCore.Qt.ItemDataRole.UserRole, fi)
self.twList.setItem(row, FileSelectionList.fileColNum, filename_item)
filename_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
filename_item.setData(Qt.UserRole, fi)
self.twList.setItem(
row, FileSelectionList.fileColNum, filename_item)
folder_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
self.twList.setItem(row, FileSelectionList.folderColNum, folder_item)
folder_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
self.twList.setItem(
row, FileSelectionList.folderColNum, folder_item)
type_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
type_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
self.twList.setItem(row, FileSelectionList.typeColNum, type_item)
cix_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
cix_item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter)
cix_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
cix_item.setTextAlignment(Qt.AlignHCenter)
self.twList.setItem(row, FileSelectionList.CRFlagColNum, cix_item)
cbi_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
cbi_item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter)
cbi_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
cbi_item.setTextAlignment(Qt.AlignHCenter)
self.twList.setItem(row, FileSelectionList.CBLFlagColNum, cbi_item)
readonly_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
readonly_item.setTextAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter)
self.twList.setItem(row, FileSelectionList.readonlyColNum, readonly_item)
readonly_item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
readonly_item.setTextAlignment(Qt.AlignHCenter)
self.twList.setItem(
row, FileSelectionList.readonlyColNum, readonly_item)
self.update_row(row)
self.updateRow(row)
return row
return -1
def update_row(self, row):
fi: FileInfo = self.twList.item(row, FileSelectionList.dataColNum).data(QtCore.Qt.ItemDataRole.UserRole)
def updateRow(self, row):
fi = self.twList.item(row, FileSelectionList.dataColNum).data(
Qt.UserRole) #.toPyObject()
filename_item = self.twList.item(row, FileSelectionList.fileColNum)
folder_item = self.twList.item(row, FileSelectionList.folderColNum)
@ -291,93 +329,119 @@ class FileSelectionList(QtWidgets.QWidget):
item_text = os.path.split(fi.ca.path)[0]
folder_item.setText(item_text)
folder_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
folder_item.setData(Qt.ToolTipRole, item_text)
item_text = os.path.split(fi.ca.path)[1]
filename_item.setText(item_text)
filename_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
filename_item.setData(Qt.ToolTipRole, item_text)
if fi.ca.is_sevenzip():
item_text = "7Z"
elif fi.ca.is_zip():
if fi.ca.isZip():
item_text = "ZIP"
elif fi.ca.is_rar():
elif fi.ca.isRar():
item_text = "RAR"
else:
item_text = ""
type_item.setText(item_text)
type_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
type_item.setData(Qt.ToolTipRole, item_text)
if fi.ca.has_cix():
cix_item.setCheckState(QtCore.Qt.CheckState.Checked)
cix_item.setData(QtCore.Qt.ItemDataRole.UserRole, True)
if fi.ca.hasCIX():
cix_item.setCheckState(Qt.Checked)
cix_item.setData(Qt.UserRole, True)
else:
cix_item.setData(QtCore.Qt.ItemDataRole.UserRole, False)
cix_item.setCheckState(QtCore.Qt.CheckState.Unchecked)
cix_item.setData(Qt.UserRole, False)
cix_item.setCheckState(Qt.Unchecked)
if fi.ca.has_cbi():
cbi_item.setCheckState(QtCore.Qt.CheckState.Checked)
cbi_item.setData(QtCore.Qt.ItemDataRole.UserRole, True)
if fi.ca.hasCBI():
cbi_item.setCheckState(Qt.Checked)
cbi_item.setData(Qt.UserRole, True)
else:
cbi_item.setData(QtCore.Qt.ItemDataRole.UserRole, False)
cbi_item.setCheckState(QtCore.Qt.CheckState.Unchecked)
cbi_item.setData(Qt.UserRole, False)
cbi_item.setCheckState(Qt.Unchecked)
if not fi.ca.is_writable():
readonly_item.setCheckState(QtCore.Qt.CheckState.Checked)
readonly_item.setData(QtCore.Qt.ItemDataRole.UserRole, True)
if not fi.ca.isWritable():
readonly_item.setCheckState(Qt.Checked)
readonly_item.setData(Qt.UserRole, True)
else:
readonly_item.setData(QtCore.Qt.ItemDataRole.UserRole, False)
readonly_item.setCheckState(QtCore.Qt.CheckState.Unchecked)
readonly_item.setData(Qt.UserRole, False)
readonly_item.setCheckState(Qt.Unchecked)
# Reading these will force them into the ComicArchive's cache
fi.ca.read_cix()
fi.ca.has_cbi()
fi.ca.readCIX()
fi.ca.hasCBI()
def get_selected_archive_list(self) -> List[ComicArchive]:
ca_list: List[ComicArchive] = []
def getSelectedArchiveList(self):
ca_list = []
for r in range(self.twList.rowCount()):
item = self.twList.item(r, FileSelectionList.dataColNum)
if item.isSelected():
fi: FileInfo = item.data(QtCore.Qt.ItemDataRole.UserRole)
fi = item.data(Qt.UserRole)
ca_list.append(fi.ca)
return ca_list
def update_current_row(self):
self.update_row(self.twList.currentRow())
def updateCurrentRow(self):
self.updateRow(self.twList.currentRow())
def update_selected_rows(self):
def updateSelectedRows(self):
self.twList.setSortingEnabled(False)
for r in range(self.twList.rowCount()):
item = self.twList.item(r, FileSelectionList.dataColNum)
if item.isSelected():
self.update_row(r)
self.updateRow(r)
self.twList.setSortingEnabled(True)
def current_item_changed_cb(self, curr, prev):
def currentItemChangedCB(self, curr, prev):
new_idx = curr.row()
old_idx = -1
if prev is not None:
old_idx = prev.row()
#print("old {0} new {1}".format(old_idx, new_idx))
if old_idx == new_idx:
return
# don't allow change if modified
if prev is not None and new_idx != old_idx:
if not self.dirty_flag_verification(
"Change Archive", "If you change archives now, data in the form will be lost. Are you sure?"
):
self.twList.currentItemChanged.disconnect(self.current_item_changed_cb)
if not self.modifiedFlagVerification(
"Change Archive",
"If you change archives now, data in the form will be lost. Are you sure?"):
self.twList.currentItemChanged.disconnect(
self.currentItemChangedCB)
self.twList.setCurrentItem(prev)
self.twList.currentItemChanged.connect(self.current_item_changed_cb)
self.twList.currentItemChanged.connect(
self.currentItemChangedCB)
# Need to defer this revert selection, for some reason
QtCore.QTimer.singleShot(1, self.revert_selection)
QTimer.singleShot(1, self.revertSelection)
return
fi = self.twList.item(new_idx, FileSelectionList.dataColNum).data(QtCore.Qt.ItemDataRole.UserRole)
self.selectionChanged.emit(QtCore.QVariant(fi))
fi = self.twList.item(new_idx, FileSelectionList.dataColNum).data(
Qt.UserRole) #.toPyObject()
self.selectionChanged.emit(QVariant(fi))
def revert_selection(self):
def revertSelection(self):
self.twList.selectRow(self.twList.currentRow())
def modifiedFlagVerification(self, title, desc):
if self.modifiedFlag:
reply = QMessageBox.question(self,
self.tr(title),
self.tr(desc),
QMessageBox.Yes, QMessageBox.No)
if reply != QMessageBox.Yes:
return False
return True
# Attempt to use a special checkbox widget in the cell.
# Couldn't figure out how to disable it with "enabled" colors
#w = QWidget()
#cb = QCheckBox(w)
# cb.setCheckState(Qt.Checked)
#layout = QHBoxLayout()
# layout.addWidget(cb)
# layout.setAlignment(Qt.AlignHCenter)
# layout.setMargin(2)
# w.setLayout(layout)
#self.twList.setCellWidget(row, 2, w)

View File

@ -0,0 +1 @@
from comicapi.genericmetadata import *

View File

@ -14,63 +14,63 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
import os
import shutil
import sqlite3 as lite
import os
import datetime
import shutil
import tempfile
import requests
try:
from PyQt5 import QtCore, QtNetwork
qt_available = True
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
from PyQt5.QtCore import QUrl, pyqtSignal, QObject, QByteArray
from PyQt5 import QtGui
except ImportError:
qt_available = False
# No Qt, so define a few dummy QObjects to help us compile
class QObject():
def __init__(self, *args):
pass
import logging
class QByteArray():
pass
from comictaggerlib import ctversion
from comictaggerlib.settings import ComicTaggerSettings
class pyqtSignal():
logger = logging.getLogger(__name__)
def __init__(self, *args):
pass
def emit(a, b, c):
pass
from .settings import ComicTaggerSettings
from . import ctversion
class ImageFetcherException(Exception):
pass
def fetch_complete(this, image_data):
...
class ImageFetcher(QObject):
class ImageFetcher:
image_fetch_complete = fetch_complete
fetchComplete = pyqtSignal(QByteArray, int)
def __init__(self):
QObject.__init__(self)
self.settings_folder = ComicTaggerSettings.get_settings_folder()
self.settings_folder = ComicTaggerSettings.getSettingsFolder()
self.db_file = os.path.join(self.settings_folder, "image_url_cache.db")
self.cache_folder = os.path.join(self.settings_folder, "image_cache")
self.user_data = None
self.fetched_url = ""
if not os.path.exists(self.db_file):
self.create_image_db()
if qt_available:
self.nam = QtNetwork.QNetworkAccessManager()
def clear_cache(self):
def clearCache(self):
os.unlink(self.db_file)
if os.path.isdir(self.cache_folder):
shutil.rmtree(self.cache_folder)
def fetch(self, url, blocking=False):
def fetch(self, url, user_data=None, blocking=False):
"""
If called with blocking=True, this will block until the image is
fetched.
@ -78,49 +78,52 @@ class ImageFetcher:
background, and emit a signal when done
"""
self.user_data = user_data
self.fetched_url = url
# first look in the DB
image_data = self.get_image_from_cache(url)
if blocking or not qt_available:
if blocking:
if image_data is None:
try:
image_data = requests.get(url, headers={"user-agent": "comictagger/" + ctversion.version}).content
print(url)
image_data = requests.get(url, headers={'user-agent': 'comictagger/' + ctversion.version}).content
except Exception as e:
logger.exception("Fetching url failed: %s")
raise ImageFetcherException("Network Error!") from e
print(e)
raise ImageFetcherException("Network Error!")
# save the image to the cache
self.add_image_to_cache(self.fetched_url, image_data)
return image_data
if qt_available:
else:
# if we found it, just emit the signal asap
if image_data is not None:
self.image_fetch_complete(QtCore.QByteArray(image_data))
return bytes()
self.fetchComplete.emit(QByteArray(image_data), self.user_data)
return
# didn't find it. look online
self.nam.finished.connect(self.finish_request)
self.nam.get(QtNetwork.QNetworkRequest(QtCore.QUrl(url)))
self.nam = QNetworkAccessManager()
self.nam.finished.connect(self.finishRequest)
self.nam.get(QNetworkRequest(QUrl(url)))
# we'll get called back when done...
return bytes()
def finish_request(self, reply):
def finishRequest(self, reply):
# read in the image data
logger.debug("request finished")
image_data = reply.readAll()
# save the image to the cache
self.add_image_to_cache(self.fetched_url, image_data)
self.image_fetch_complete(image_data)
self.fetchComplete.emit(QByteArray(image_data), self.user_data)
def create_image_db(self):
# this will wipe out any existing version
open(self.db_file, "w").close()
open(self.db_file, 'w').close()
# wipe any existing image cache folder too
if os.path.isdir(self.cache_folder):
@ -131,25 +134,37 @@ class ImageFetcher:
# create tables
with con:
cur = con.cursor()
cur.execute("CREATE TABLE Images(url TEXT,filename TEXT,timestamp TEXT,PRIMARY KEY (url))")
cur.execute("CREATE TABLE Images(" +
"url TEXT," +
"filename TEXT," +
"timestamp TEXT," +
"PRIMARY KEY (url))"
)
def add_image_to_cache(self, url, image_data):
con = lite.connect(self.db_file)
with con:
cur = con.cursor()
timestamp = datetime.datetime.now()
tmp_fd, filename = tempfile.mkstemp(dir=self.cache_folder, prefix="img")
f = os.fdopen(tmp_fd, "w+b")
tmp_fd, filename = tempfile.mkstemp(
dir=self.cache_folder, prefix="img")
f = os.fdopen(tmp_fd, 'w+b')
f.write(image_data)
f.close()
cur.execute("INSERT or REPLACE INTO Images VALUES(?, ?, ?)", (url, filename, timestamp))
cur.execute("INSERT or REPLACE INTO Images VALUES(?, ?, ?)",
(url,
filename,
timestamp)
)
def get_image_from_cache(self, url):
@ -162,15 +177,15 @@ class ImageFetcher:
if row is None:
return None
else:
filename = row[0]
image_data = None
filename = row[0]
image_data = None
try:
with open(filename, 'rb') as f:
image_data = f.read()
f.close()
except IOError as e:
pass
try:
with open(filename, "rb") as f:
image_data = f.read()
f.close()
except IOError:
pass
return image_data
return image_data

View File

@ -15,55 +15,57 @@
# limitations under the License.
import io
import logging
import sys
from functools import reduce
try:
from PIL import Image
from PIL import WebPImagePlugin
pil_available = True
except ImportError:
pil_available = False
logger = logging.getLogger(__name__)
class ImageHasher:
class ImageHasher(object):
def __init__(self, path=None, data=None, width=8, height=8):
#self.hash_size = size
self.width = width
self.height = height
if path is None and data is None:
raise IOError
try:
if path is not None:
self.image = Image.open(path)
else:
self.image = Image.open(io.BytesIO(data))
except Exception:
logger.exception("Image data seems corrupted!")
# just generate a bogus image
self.image = Image.new("L", (1, 1))
else:
try:
if path is not None:
self.image = Image.open(path)
else:
self.image = Image.open(io.BytesIO(data))
except Exception as e:
print("Image data seems corrupted! [{}]".format(e))
# just generate a bogus image
self.image = Image.new("L", (1, 1))
def average_hash(self):
try:
image = self.image.resize((self.width, self.height), Image.ANTIALIAS).convert("L")
except Exception:
logger.exception("average_hash error")
image = self.image.resize(
(self.width, self.height), Image.ANTIALIAS).convert("L")
except Exception as e:
print("average_hash error:", e)
return int(0)
pixels = list(image.getdata())
avg = sum(pixels) / len(pixels)
def compare_value_to_avg(i):
return 1 if i > avg else 0
return (1 if i > avg else 0)
bitlist = list(map(compare_value_to_avg, pixels))
# build up an int value from the bit list, one bit at a time
def set_bit(x, idx_val):
(idx, val) = idx_val
return x | (val << idx)
return (x | (val << idx))
result = reduce(set_bit, enumerate(bitlist), 0)
@ -71,6 +73,7 @@ class ImageHasher:
return result
def average_hash2(self):
pass
"""
# Got this one from somewhere on the net. Not a clue how the 'convolve2d'
# works!
@ -92,6 +95,7 @@ class ImageHasher:
"""
def dct_average_hash(self):
pass
"""
# Algorithm source: http://syntaxcandy.blogspot.com/2012/08/perceptual-hash.html
@ -129,8 +133,8 @@ class ImageHasher:
7. Construct the hash. Set the 64 bits into a 64-bit integer. The order does not
matter, just as long as you are consistent.
"""
"""
import numpy
import scipy.fftpack
numpy.set_printoptions(threshold=10000, linewidth=200, precision=2, suppress=True)
@ -173,7 +177,7 @@ class ImageHasher:
@staticmethod
def hamming_distance(h1, h2):
if isinstance(h1, int) or isinstance(h2, int):
if isinstance(h1, int) or isinstance(h1, int):
n1 = h1
n2 = h2
else:
@ -185,4 +189,4 @@ class ImageHasher:
n = n1 ^ n2
# count up the 1's in the binary string
return sum(b == "1" for b in bin(n)[2:])
return sum(b == '1' for b in bin(n)[2:])

View File

@ -14,62 +14,70 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
#import sys
#import os
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comictaggerlib.settings import ComicTaggerSettings
logger = logging.getLogger(__name__)
from .settings import ComicTaggerSettings
class ImagePopup(QtWidgets.QDialog):
def __init__(self, parent, image_pixmap):
super(ImagePopup, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.get_ui_file("imagepopup.ui"), self)
uic.loadUi(ComicTaggerSettings.getUIFile('imagepopup.ui'), self)
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
QtWidgets.QApplication.setOverrideCursor(
QtGui.QCursor(QtCore.Qt.WaitCursor))
self.setWindowFlags(QtCore.Qt.WindowType.Popup)
self.setWindowState(QtCore.Qt.WindowState.WindowFullScreen)
# self.setWindowModality(QtCore.Qt.WindowModal)
self.setWindowFlags(QtCore.Qt.Popup)
self.setWindowState(QtCore.Qt.WindowFullScreen)
self.imagePixmap = image_pixmap
screen_size = QtGui.QGuiApplication.primaryScreen().geometry()
QtWidgets.QApplication.primaryScreen()
screen_size = QtWidgets.QDesktopWidget().screenGeometry()
self.resize(screen_size.width(), screen_size.height())
self.move(0, 0)
# This is a total hack. Uses a snapshot of the desktop, and overlays a
# translucent screen over it. Probably can do it better by setting opacity of a widget
# TODO: macOS denies this
# translucent screen over it. Probably can do it better by setting opacity of a
# widget
screen = QtWidgets.QApplication.primaryScreen()
self.desktopBg = screen.grabWindow(0, 0, 0, screen_size.width(), screen_size.height())
bg = QtGui.QPixmap(ComicTaggerSettings.get_graphic("popup_bg.png"))
self.clientBgPixmap = bg.scaled(screen_size.width(), screen_size.height())
self.desktopBg = screen.grabWindow(
QtWidgets.QApplication.desktop().winId(),
0,
0,
screen_size.width(),
screen_size.height())
bg = QtGui.QPixmap(ComicTaggerSettings.getGraphic('popup_bg.png'))
self.clientBgPixmap = bg.scaled(
screen_size.width(), screen_size.height())
self.setMask(self.clientBgPixmap.mask())
self.apply_image_pixmap()
self.applyImagePixmap()
self.showFullScreen()
self.raise_()
QtWidgets.QApplication.restoreOverrideCursor()
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
painter.drawPixmap(0, 0, self.desktopBg)
painter.drawPixmap(0, 0, self.clientBgPixmap)
painter.end()
self.painter = QtGui.QPainter(self)
self.painter.setRenderHint(QtGui.QPainter.Antialiasing)
self.painter.drawPixmap(0, 0, self.desktopBg)
self.painter.drawPixmap(0, 0, self.clientBgPixmap)
self.painter.end()
def apply_image_pixmap(self):
def applyImagePixmap(self):
win_h = self.height()
win_w = self.width()
if self.imagePixmap.width() > win_w or self.imagePixmap.height() > win_h:
if self.imagePixmap.width(
) > win_w or self.imagePixmap.height() > win_h:
# scale the pixmap to fit in the frame
display_pixmap = self.imagePixmap.scaled(win_w, win_h, QtCore.Qt.AspectRatioMode.KeepAspectRatio)
display_pixmap = self.imagePixmap.scaled(
win_w, win_h, QtCore.Qt.KeepAspectRatio)
self.lblImage.setPixmap(display_pixmap)
else:
display_pixmap = self.imagePixmap
@ -79,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(int((win_w - img_w) / 2), int((win_h - img_h) / 2))
self.lblImage.move((win_w - img_w) / 2, (win_h - img_h) / 2)
def mousePressEvent(self, event):
self.close()

View File

@ -14,36 +14,24 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import io
import logging
import sys
from typing import List, TypedDict
from comicapi import utils
from comicapi.comicarchive import ComicArchive
from comicapi.genericmetadata import GenericMetadata
from comicapi.issuestring import IssueString
from comictaggerlib.comicvinetalker import ComicVineTalker, ComicVineTalkerException
from comictaggerlib.imagefetcher import ImageFetcher, ImageFetcherException
from comictaggerlib.imagehasher import ImageHasher
from comictaggerlib.resulttypes import IssueResult
logger = logging.getLogger(__name__)
import io
try:
from PIL import Image
from PIL import WebPImagePlugin
pil_available = True
except ImportError:
pil_available = False
class SearchKeys(TypedDict):
series: str
issue_number: str
month: int
year: int
issue_count: int
from .genericmetadata import GenericMetadata
from .comicvinetalker import ComicVineTalker, ComicVineTalkerException
from .imagehasher import ImageHasher
from .imagefetcher import ImageFetcher, ImageFetcherException
from .issuestring import IssueString
from . import utils
#from settings import ComicTaggerSettings
#from comicvinecacher import ComicVineCacher
class IssueIdentifierNetworkError(Exception):
@ -55,21 +43,22 @@ class IssueIdentifierCancelled(Exception):
class IssueIdentifier:
result_no_matches = 0
result_found_match_but_bad_cover_score = 1
result_found_match_but_not_first_page = 2
result_multiple_matches_with_bad_image_scores = 3
result_one_good_match = 4
result_multiple_good_matches = 5
def __init__(self, comic_archive: ComicArchive, settings):
self.comic_archive: ComicArchive = comic_archive
ResultNoMatches = 0
ResultFoundMatchButBadCoverScore = 1
ResultFoundMatchButNotFirstPage = 2
ResultMultipleMatchesWithBadImageScores = 3
ResultOneGoodMatch = 4
ResultMultipleGoodMatches = 5
def __init__(self, comic_archive, settings):
self.comic_archive = comic_archive
self.image_hasher = 1
self.only_use_additional_meta_data = False
self.onlyUseAdditionalMetaData = False
# a decent hamming score, good enough to call it a match
self.min_score_thresh: int = 16
self.min_score_thresh = 16
# for alternate covers, be more stringent, since we're a bit more
# scattershot in comparisons
@ -87,111 +76,112 @@ class IssueIdentifier:
self.length_delta_thresh = settings.id_length_delta_thresh
# used to eliminate unlikely publishers
self.publisher_filter = [s.strip().lower() for s in settings.id_publisher_filter.split(",")]
self.publisher_blacklist = [
s.strip().lower() for s in settings.id_publisher_blacklist.split(',')]
self.additional_metadata = GenericMetadata()
self.output_function = IssueIdentifier.default_write_output
self.output_function = IssueIdentifier.defaultWriteOutput
self.callback = None
self.cover_url_callback = None
self.search_result = self.result_no_matches
self.coverUrlCallback = None
self.search_result = self.ResultNoMatches
self.cover_page_index = 0
self.cancel = False
self.wait_and_retry_on_rate_limit = False
self.waitAndRetryOnRateLimit = False
self.match_list = []
def set_score_min_threshold(self, thresh: int):
def setScoreMinThreshold(self, thresh):
self.min_score_thresh = thresh
def set_score_min_distance(self, distance):
def setScoreMinDistance(self, distance):
self.min_score_distance = distance
def set_additional_metadata(self, md):
def setAdditionalMetadata(self, md):
self.additional_metadata = md
def set_name_length_delta_threshold(self, delta):
def setNameLengthDeltaThreshold(self, delta):
self.length_delta_thresh = delta
def set_publisher_filter(self, filter):
self.publisher_filter = filter
def setPublisherBlackList(self, blacklist):
self.publisher_blacklist = blacklist
def set_hasher_algorithm(self, algo):
def setHasherAlgorithm(self, algo):
self.image_hasher = algo
pass
def set_output_function(self, func):
def setOutputFunction(self, func):
self.output_function = func
pass
def calculate_hash(self, image_data):
if self.image_hasher == "3":
def calculateHash(self, image_data):
if self.image_hasher == '3':
return ImageHasher(data=image_data).dct_average_hash()
if self.image_hasher == "2":
elif self.image_hasher == '2':
return ImageHasher(data=image_data).average_hash2()
else:
return ImageHasher(data=image_data).average_hash()
return ImageHasher(data=image_data).average_hash()
def get_aspect_ratio(self, image_data):
def getAspectRatio(self, image_data):
try:
im = Image.open(io.BytesIO(image_data))
im = Image.open(io.StringIO(image_data))
w, h = im.size
return float(h) / float(w)
except:
return 1.5
def crop_cover(self, image_data):
def cropCover(self, image_data):
im = Image.open(io.BytesIO(image_data))
im = Image.open(io.StringIO(image_data))
w, h = im.size
try:
cropped_im = im.crop((int(w / 2), 0, w, h))
except:
logger.exception("cropCover() error")
except Exception as e:
print("cropCover() error:", e)
return None
output = io.BytesIO()
output = io.StringIO()
cropped_im.save(output, format="PNG")
cropped_image_data = output.getvalue()
output.close()
return cropped_image_data
def set_progress_callback(self, cb_func):
def setProgressCallback(self, cb_func):
self.callback = cb_func
def set_cover_url_callback(self, cb_func):
self.cover_url_callback = cb_func
def setCoverURLCallback(self, cb_func):
self.coverUrlCallback = cb_func
def get_search_keys(self):
def getSearchKeys(self):
ca = self.comic_archive
search_keys: SearchKeys = {}
search_keys["series"] = None
search_keys["issue_number"] = None
search_keys["month"] = None
search_keys["year"] = None
search_keys["issue_count"] = None
search_keys = dict()
search_keys['series'] = None
search_keys['issue_number'] = None
search_keys['month'] = None
search_keys['year'] = None
search_keys['issue_count'] = None
if ca is None:
return None
return
if self.only_use_additional_meta_data:
search_keys["series"] = self.additional_metadata.series
search_keys["issue_number"] = self.additional_metadata.issue
search_keys["year"] = self.additional_metadata.year
search_keys["month"] = self.additional_metadata.month
search_keys["issue_count"] = self.additional_metadata.issue_count
if self.onlyUseAdditionalMetaData:
search_keys['series'] = self.additional_metadata.series
search_keys['issue_number'] = self.additional_metadata.issue
search_keys['year'] = self.additional_metadata.year
search_keys['month'] = self.additional_metadata.month
search_keys['issue_count'] = self.additional_metadata.issueCount
return search_keys
# see if the archive has any useful meta data for searching with
if ca.has_cix():
internal_metadata = ca.read_cix()
elif ca.has_cbi():
internal_metadata = ca.read_cbi()
if ca.hasCIX():
internal_metadata = ca.readCIX()
elif ca.hasCBI():
internal_metadata = ca.readCBI()
else:
internal_metadata = ca.read_cbi()
internal_metadata = ca.readCBI()
# try to get some metadata from filename
md_from_filename = ca.metadata_from_filename()
md_from_filename = ca.metadataFromFilename()
# preference order:
# 1. Additional metadata
@ -199,131 +189,136 @@ class IssueIdentifier:
# 1. Filename metadata
if self.additional_metadata.series is not None:
search_keys["series"] = self.additional_metadata.series
search_keys['series'] = self.additional_metadata.series
elif internal_metadata.series is not None:
search_keys["series"] = internal_metadata.series
search_keys['series'] = internal_metadata.series
else:
search_keys["series"] = md_from_filename.series
search_keys['series'] = md_from_filename.series
if self.additional_metadata.issue is not None:
search_keys["issue_number"] = self.additional_metadata.issue
search_keys['issue_number'] = self.additional_metadata.issue
elif internal_metadata.issue is not None:
search_keys["issue_number"] = internal_metadata.issue
search_keys['issue_number'] = internal_metadata.issue
else:
search_keys["issue_number"] = md_from_filename.issue
search_keys['issue_number'] = md_from_filename.issue
if self.additional_metadata.year is not None:
search_keys["year"] = self.additional_metadata.year
search_keys['year'] = self.additional_metadata.year
elif internal_metadata.year is not None:
search_keys["year"] = internal_metadata.year
search_keys['year'] = internal_metadata.year
else:
search_keys["year"] = md_from_filename.year
search_keys['year'] = md_from_filename.year
if self.additional_metadata.month is not None:
search_keys["month"] = self.additional_metadata.month
search_keys['month'] = self.additional_metadata.month
elif internal_metadata.month is not None:
search_keys["month"] = internal_metadata.month
search_keys['month'] = internal_metadata.month
else:
search_keys["month"] = md_from_filename.month
search_keys['month'] = md_from_filename.month
if self.additional_metadata.issue_count is not None:
search_keys["issue_count"] = self.additional_metadata.issue_count
elif internal_metadata.issue_count is not None:
search_keys["issue_count"] = internal_metadata.issue_count
if self.additional_metadata.issueCount is not None:
search_keys['issue_count'] = self.additional_metadata.issueCount
elif internal_metadata.issueCount is not None:
search_keys['issue_count'] = internal_metadata.issueCount
else:
search_keys["issue_count"] = md_from_filename.issue_count
search_keys['issue_count'] = md_from_filename.issueCount
return search_keys
@staticmethod
def default_write_output(text):
def defaultWriteOutput(text):
sys.stdout.write(text)
sys.stdout.flush()
def log_msg(self, msg: str, newline=True):
msg = str(msg)
if newline:
msg += "\n"
def log_msg(self, msg, newline=True):
self.output_function(msg)
if newline:
self.output_function("\n")
def get_issue_cover_match_score(
self,
comic_vine,
issue_id,
primary_img_url,
primary_thumb_url,
page_url,
local_cover_hash_list,
use_remote_alternates=False,
use_log=True,
):
# local_cover_hash_list is a list of pre-calculated hashs.
# use_remote_alternates - indicates to use alternate covers from CV
def getIssueCoverMatchScore(
self,
comicVine,
issue_id,
primary_img_url,
primary_thumb_url,
page_url,
localCoverHashList,
useRemoteAlternates=False,
useLog=True):
# localHashes is a list of pre-calculated hashs.
# useRemoteAlternates - indicates to use alternate covers from CV
try:
url_image_data = ImageFetcher().fetch(primary_thumb_url, blocking=True)
except ImageFetcherException as e:
self.log_msg("Network issue while fetching cover image from Comic Vine. Aborting...")
raise IssueIdentifierNetworkError from e
url_image_data = ImageFetcher().fetch(
primary_thumb_url, blocking=True)
except ImageFetcherException:
self.log_msg(
"Network issue while fetching cover image from Comic Vine. Aborting...")
raise IssueIdentifierNetworkError
if self.cancel:
raise IssueIdentifierCancelled
# alert the GUI, if needed
if self.cover_url_callback is not None:
self.cover_url_callback(url_image_data)
if self.coverUrlCallback is not None:
self.coverUrlCallback(url_image_data)
remote_cover_list = []
item = {}
item["url"] = primary_img_url
item = dict()
item['url'] = primary_img_url
item["hash"] = self.calculate_hash(url_image_data)
item['hash'] = self.calculateHash(url_image_data)
remote_cover_list.append(item)
if self.cancel:
raise IssueIdentifierCancelled
if use_remote_alternates:
alt_img_url_list = comic_vine.fetch_alternate_cover_urls(issue_id, page_url)
if useRemoteAlternates:
alt_img_url_list = comicVine.fetchAlternateCoverURLs(
issue_id, page_url)
for alt_url in alt_img_url_list:
try:
alt_url_image_data = ImageFetcher().fetch(alt_url, blocking=True)
except ImageFetcherException as e:
self.log_msg("Network issue while fetching alt. cover image from Comic Vine. Aborting...")
raise IssueIdentifierNetworkError from e
alt_url_image_data = ImageFetcher().fetch(
alt_url, blocking=True)
except ImageFetcherException:
self.log_msg(
"Network issue while fetching alt. cover image from Comic Vine. Aborting...")
raise IssueIdentifierNetworkError
if self.cancel:
raise IssueIdentifierCancelled
# alert the GUI, if needed
if self.cover_url_callback is not None:
self.cover_url_callback(alt_url_image_data)
if self.coverUrlCallback is not None:
self.coverUrlCallback(alt_url_image_data)
item = {}
item["url"] = alt_url
item["hash"] = self.calculate_hash(alt_url_image_data)
item = dict()
item['url'] = alt_url
item['hash'] = self.calculateHash(alt_url_image_data)
remote_cover_list.append(item)
if self.cancel:
raise IssueIdentifierCancelled
if use_log and use_remote_alternates:
self.log_msg(f"[{len(remote_cover_list) - 1} alt. covers]", False)
if use_log:
if useLog and useRemoteAlternates:
self.log_msg(
"[{0} alt. covers]".format(len(remote_cover_list) - 1), False)
if useLog:
self.log_msg("[ ", False)
score_list = []
done = False
for local_cover_hash in local_cover_hash_list:
for local_cover_hash in localCoverHashList:
for remote_cover_item in remote_cover_list:
score = ImageHasher.hamming_distance(local_cover_hash, remote_cover_item["hash"])
score_item = {}
score_item["score"] = score
score_item["url"] = remote_cover_item["url"]
score_item["hash"] = remote_cover_item["hash"]
score = ImageHasher.hamming_distance(
local_cover_hash, remote_cover_item['hash'])
score_item = dict()
score_item['score'] = score
score_item['url'] = remote_cover_item['url']
score_item['hash'] = remote_cover_item['hash']
score_list.append(score_item)
if use_log:
self.log_msg(score, False)
if useLog:
self.log_msg("{0}".format(score), False)
if score <= self.strong_score_thresh:
# such a good score, we can quit now, since for sure we
@ -333,71 +328,89 @@ class IssueIdentifier:
if done:
break
if use_log:
if useLog:
self.log_msg(" ]", False)
best_score_item = min(score_list, key=lambda x: x["score"])
best_score_item = min(score_list, key=lambda x: x['score'])
return best_score_item
def search(self) -> List[IssueResult]:
# def validate(self, issue_id):
# create hash list
# score = self.getIssueMatchScore(issue_id, hash_list, useRemoteAlternates = True)
# if score < 20:
# return True
# else:
# return False
def search(self):
ca = self.comic_archive
self.match_list: List[IssueResult] = []
self.match_list = []
self.cancel = False
self.search_result = self.result_no_matches
self.search_result = self.ResultNoMatches
if not pil_available:
self.log_msg("Python Imaging Library (PIL) is not available and is needed for issue identification.")
self.log_msg(
"Python Imaging Library (PIL) is not available and is needed for issue identification.")
return self.match_list
if not ca.seems_to_be_a_comic_archive():
self.log_msg("Sorry, but " + ca.path + " is not a comic archive!")
if not ca.seemsToBeAComicArchive():
self.log_msg(
"Sorry, but " + opts.filename + " is not a comic archive!")
return self.match_list
cover_image_data = ca.get_page(self.cover_page_index)
cover_hash = self.calculate_hash(cover_image_data)
cover_image_data = ca.getPage(self.cover_page_index)
cover_hash = self.calculateHash(cover_image_data)
# check the aspect ratio
# if it's wider than it is high, it's probably a two page spread
# if so, crop it and calculate a second hash
narrow_cover_hash = None
aspect_ratio = self.get_aspect_ratio(cover_image_data)
aspect_ratio = self.getAspectRatio(cover_image_data)
if aspect_ratio < 1.0:
right_side_image_data = self.crop_cover(cover_image_data)
right_side_image_data = self.cropCover(cover_image_data)
if right_side_image_data is not None:
narrow_cover_hash = self.calculate_hash(right_side_image_data)
narrow_cover_hash = self.calculateHash(right_side_image_data)
keys = self.get_search_keys()
#self.log_msg("Cover hash = {0:016x}".format(cover_hash))
keys = self.getSearchKeys()
# normalize the issue number
keys["issue_number"] = IssueString(keys["issue_number"]).as_string()
keys['issue_number'] = IssueString(keys['issue_number']).asString()
# we need, at minimum, a series and issue number
if keys["series"] is None or keys["issue_number"] is None:
if keys['series'] is None or keys['issue_number'] is None:
self.log_msg("Not enough info for a search!")
return []
self.log_msg("Going to search for:")
self.log_msg("\tSeries: " + keys["series"])
self.log_msg("\tIssue: " + keys["issue_number"])
if keys["issue_count"] is not None:
self.log_msg("\tCount: " + str(keys["issue_count"]))
if keys["year"] is not None:
self.log_msg("\tYear: " + str(keys["year"]))
if keys["month"] is not None:
self.log_msg("\tMonth: " + str(keys["month"]))
self.log_msg("\tSeries: " + keys['series'])
self.log_msg("\tIssue: " + keys['issue_number'])
if keys['issue_count'] is not None:
self.log_msg("\tCount: " + str(keys['issue_count']))
if keys['year'] is not None:
self.log_msg("\tYear: " + str(keys['year']))
if keys['month'] is not None:
self.log_msg("\tMonth: " + str(keys['month']))
comic_vine = ComicVineTalker()
comic_vine.wait_for_rate_limit = self.wait_and_retry_on_rate_limit
#self.log_msg("Publisher Blacklist: " + str(self.publisher_blacklist))
comicVine = ComicVineTalker()
comicVine.wait_for_rate_limit = self.waitAndRetryOnRateLimit
comic_vine.set_log_func(self.output_function)
comicVine.setLogFunc(self.output_function)
self.log_msg(f"Searching for {keys['series']} #{keys['issue_number']} ...")
# self.log_msg(("Searching for " + keys['series'] + "...")
self.log_msg("Searching for {0} #{1} ...".format(
keys['series'], keys['issue_number']))
try:
cv_search_results = comic_vine.search_for_series(keys["series"])
cv_search_results = comicVine.searchForSeries(keys['series'])
except ComicVineTalkerException:
self.log_msg("Network issue while searching for series. Aborting...")
self.log_msg(
"Network issue while searching for series. Aborting...")
return []
#self.log_msg("Found " + str(len(cv_search_results)) + " initial results")
if self.cancel:
return []
@ -406,80 +419,85 @@ class IssueIdentifier:
series_second_round_list = []
#self.log_msg("Removing results with too long names, banned publishers, or future start dates")
for item in cv_search_results:
length_approved = False
publisher_approved = True
date_approved = True
# remove any series that starts after the issue year
if (
keys["year"] is not None
and str(keys["year"]).isdigit()
and item["start_year"] is not None
and str(item["start_year"]).isdigit()
):
if int(keys["year"]) < int(item["start_year"]):
if keys['year'] is not None and str(
keys['year']).isdigit() and item['start_year'] is not None and str(
item['start_year']).isdigit():
if int(keys['year']) < int(item['start_year']):
date_approved = False
# assume that our search name is close to the actual name, say
# within ,e.g. 5 chars
# 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):
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 filter
if item["publisher"] is not None:
publisher = item["publisher"]["name"]
if publisher is not None and publisher.lower() in self.publisher_filter:
# remove any series from publishers on the blacklist
if item['publisher'] is not None:
publisher = item['publisher']['name']
if publisher is not None and publisher.lower(
) in self.publisher_blacklist:
publisher_approved = False
if length_approved and publisher_approved and date_approved:
series_second_round_list.append(item)
self.log_msg("Searching in " + str(len(series_second_round_list)) + " series")
self.log_msg(
"Searching in " + str(len(series_second_round_list)) + " series")
if self.callback is not None:
self.callback(0, len(series_second_round_list))
# now sort the list by name length
series_second_round_list.sort(key=lambda x: len(x["name"]), reverse=False)
series_second_round_list.sort(
key=lambda x: len(x['name']), reverse=False)
# build a list of volume IDs
volume_id_list = []
volume_id_list = list()
for series in series_second_round_list:
volume_id_list.append(series["id"])
volume_id_list.append(series['id'])
issue_list = None
try:
if len(volume_id_list) > 0:
issue_list = comic_vine.fetch_issues_by_volume_issue_num_and_year(
volume_id_list, keys["issue_number"], keys["year"]
)
issue_list = comicVine.fetchIssuesByVolumeIssueNumAndYear(
volume_id_list,
keys['issue_number'],
keys['year'])
except ComicVineTalkerException:
self.log_msg("Network issue while searching for series details. Aborting...")
self.log_msg(
"Network issue while searching for series details. Aborting...")
return []
if issue_list is None:
return []
shortlist = []
shortlist = list()
# now re-associate the issues and volumes
for issue in issue_list:
for series in series_second_round_list:
if series["id"] == issue["volume"]["id"]:
if series['id'] == issue['volume']['id']:
shortlist.append((series, issue))
break
if keys["year"] is None:
self.log_msg(f"Found {len(shortlist)} series that have an issue #{keys['issue_number']}")
if keys['year'] is None:
self.log_msg("Found {0} series that have an issue #{1}".format(
len(shortlist), keys['issue_number']))
else:
self.log_msg(
f"Found {len(shortlist)} series that have an issue #{keys['issue_number']} from {keys['year']}"
)
"Found {0} series that have an issue #{1} from {2}".format(
len(shortlist),
keys['issue_number'],
keys['year']))
# now we have a shortlist of volumes with the desired issue number
# Do first round of cover matching
@ -489,13 +507,13 @@ class IssueIdentifier:
self.callback(counter, len(shortlist) * 3)
counter += 1
self.log_msg(
f"Examining covers for ID: {series['id']} {series['name']} ({series['start_year']}) ...",
newline=False,
)
self.log_msg("Examining covers for ID: {0} {1} ({2}) ...".format(
series['id'],
series['name'],
series['start_year']), newline=False)
# parse out the cover date
_, month, year = comic_vine.parse_date_str(issue["cover_date"])
day, month, year = comicVine.parseDateStr(issue['cover_date'])
# Now check the cover match against the primary image
hash_list = [cover_hash]
@ -503,87 +521,87 @@ class IssueIdentifier:
hash_list.append(narrow_cover_hash)
try:
image_url = issue["image"]["super_url"]
thumb_url = issue["image"]["thumb_url"]
page_url = issue["site_detail_url"]
image_url = issue['image']['super_url']
thumb_url = issue['image']['thumb_url']
page_url = issue['site_detail_url']
score_item = self.get_issue_cover_match_score(
comic_vine,
issue["id"],
score_item = self.getIssueCoverMatchScore(
comicVine,
issue['id'],
image_url,
thumb_url,
page_url,
hash_list,
use_remote_alternates=False,
)
useRemoteAlternates=False)
except:
self.match_list = []
return self.match_list
match: IssueResult = {}
match["series"] = f"{series['name']} ({series['start_year']})"
match["distance"] = score_item["score"]
match["issue_number"] = keys["issue_number"]
match["cv_issue_count"] = series["count_of_issues"]
match["url_image_hash"] = score_item["hash"]
match["issue_title"] = issue["name"]
match["issue_id"] = issue["id"]
match["volume_id"] = series["id"]
match["month"] = month
match["year"] = year
match["publisher"] = None
if series["publisher"] is not None:
match["publisher"] = series["publisher"]["name"]
match["image_url"] = image_url
match["thumb_url"] = thumb_url
match["page_url"] = page_url
match["description"] = issue["description"]
match = dict()
match['series'] = "{0} ({1})".format(
series['name'], series['start_year'])
match['distance'] = score_item['score']
match['issue_number'] = keys['issue_number']
match['cv_issue_count'] = series['count_of_issues']
match['url_image_hash'] = score_item['hash']
match['issue_title'] = issue['name']
match['issue_id'] = issue['id']
match['volume_id'] = series['id']
match['month'] = month
match['year'] = year
match['publisher'] = None
if series['publisher'] is not None:
match['publisher'] = series['publisher']['name']
match['image_url'] = image_url
match['thumb_url'] = thumb_url
match['page_url'] = page_url
match['description'] = issue['description']
self.match_list.append(match)
self.log_msg(f" --> {match['distance']}", newline=False)
self.log_msg(" --> {0}".format(match['distance']), newline=False)
self.log_msg("")
if len(self.match_list) == 0:
self.log_msg(":-(no matches!")
self.search_result = self.result_no_matches
self.search_result = self.ResultNoMatches
return self.match_list
# sort list by image match scores
self.match_list.sort(key=lambda k: k["distance"])
self.match_list.sort(key=lambda k: k['distance'])
lst = []
l = []
for i in self.match_list:
lst.append(i["distance"])
l.append(i['distance'])
self.log_msg(f"Compared to covers in {len(self.match_list)} issue(s):", newline=False)
self.log_msg(str(lst))
self.log_msg("Compared to covers in {0} issue(s):".format(
len(self.match_list)), newline=False)
self.log_msg(str(l))
def print_match(item):
self.log_msg(
"-----> {0} #{1} {2} ({3}/{4}) -- score: {5}".format(
item["series"],
item["issue_number"],
item["issue_title"],
item["month"],
item["year"],
item["distance"],
)
)
self.log_msg("-----> {0} #{1} {2} ({3}/{4}) -- score: {5}".format(
item['series'],
item['issue_number'],
item['issue_title'],
item['month'],
item['year'],
item['distance']))
best_score: int = self.match_list[0]["distance"]
best_score = self.match_list[0]['distance']
if best_score >= self.min_score_thresh:
# we have 1 or more low-confidence matches (all bad cover scores)
# look at a few more pages in the archive, and also alternate covers online
self.log_msg("Very weak scores for the cover. Analyzing alternate pages and covers...")
# look at a few more pages in the archive, and also alternate
# covers online
self.log_msg(
"Very weak scores for the cover. Analyzing alternate pages and covers...")
hash_list = [cover_hash]
if narrow_cover_hash is not None:
hash_list.append(narrow_cover_hash)
for i in range(1, min(3, ca.get_number_of_pages())):
image_data = ca.get_page(i)
page_hash = self.calculate_hash(image_data)
for i in range(1, min(3, ca.getNumberOfPages())):
image_data = ca.getPage(i)
page_hash = self.calculateHash(image_data)
hash_list.append(page_hash)
second_match_list = []
@ -592,92 +610,111 @@ class IssueIdentifier:
if self.callback is not None:
self.callback(counter, len(self.match_list) * 3)
counter += 1
self.log_msg(f"Examining alternate covers for ID: {m['volume_id']} {m['series']} ...", newline=False)
self.log_msg(
"Examining alternate covers for ID: {0} {1} ...".format(
m['volume_id'],
m['series']),
newline=False)
try:
score_item = self.get_issue_cover_match_score(
comic_vine,
m["issue_id"],
m["image_url"],
m["thumb_url"],
m["page_url"],
score_item = self.getIssueCoverMatchScore(
comicVine,
m['issue_id'],
m['image_url'],
m['thumb_url'],
m['page_url'],
hash_list,
use_remote_alternates=True,
)
useRemoteAlternates=True)
except:
self.match_list = []
return self.match_list
self.log_msg(f"--->{score_item['score']}")
self.log_msg("--->{0}".format(score_item['score']))
self.log_msg("")
if score_item["score"] < self.min_alternate_score_thresh:
if score_item['score'] < self.min_alternate_score_thresh:
second_match_list.append(m)
m["distance"] = score_item["score"]
m['distance'] = score_item['score']
if len(second_match_list) == 0:
if len(self.match_list) == 1:
self.log_msg("No matching pages in the issue.")
self.log_msg("--------------------------------------------------------------------------")
self.log_msg(
"--------------------------------------------------------------------------")
print_match(self.match_list[0])
self.log_msg("--------------------------------------------------------------------------")
self.search_result = self.result_found_match_but_bad_cover_score
self.log_msg(
"--------------------------------------------------------------------------")
self.search_result = self.ResultFoundMatchButBadCoverScore
else:
self.log_msg("--------------------------------------------------------------------------")
self.log_msg("Multiple bad cover matches! Need to use other info...")
self.log_msg("--------------------------------------------------------------------------")
self.search_result = self.result_multiple_matches_with_bad_image_scores
self.log_msg(
"--------------------------------------------------------------------------")
self.log_msg(
"Multiple bad cover matches! Need to use other info...")
self.log_msg(
"--------------------------------------------------------------------------")
self.search_result = self.ResultMultipleMatchesWithBadImageScores
return self.match_list
else:
# We did good, found something!
self.log_msg("Success in secondary/alternate cover matching!")
# We did good, found something!
self.log_msg("Success in secondary/alternate cover matching!")
self.match_list = second_match_list
# sort new list by image match scores
self.match_list.sort(key=lambda k: k["distance"])
best_score = self.match_list[0]["distance"]
self.log_msg("[Second round cover matching: best score = {best_score}]")
# now drop down into the rest of the processing
self.match_list = second_match_list
# sort new list by image match scores
self.match_list.sort(key=lambda k: k['distance'])
best_score = self.match_list[0]['distance']
self.log_msg(
"[Second round cover matching: best score = {0}]".format(best_score))
# now drop down into the rest of the processing
if self.callback is not None:
self.callback(99, 100)
# now pare down list, remove any item more than specified distant from the top scores
# now pare down list, remove any item more than specified distant from
# the top scores
for item in reversed(self.match_list):
if item["distance"] > best_score + self.min_score_distance:
if item['distance'] > best_score + self.min_score_distance:
self.match_list.remove(item)
# One more test for the case choosing limited series first issue vs a trade with the same cover:
# if we have a given issue count > 1 and the volume from CV has count==1, remove it from match list
if len(self.match_list) >= 2 and keys["issue_count"] is not None and keys["issue_count"] != 1:
new_list = []
# if we have a given issue count > 1 and the volume from CV has
# count==1, remove it from match list
if len(self.match_list) >= 2 and keys[
'issue_count'] is not None and keys['issue_count'] != 1:
new_list = list()
for match in self.match_list:
if match["cv_issue_count"] != 1:
if match['cv_issue_count'] != 1:
new_list.append(match)
else:
self.log_msg(
f"Removing volume {match['series']} [{match['volume_id']}] from consideration (only 1 issue)"
)
"Removing volume {0} [{1}] from consideration (only 1 issue)".format(
match['series'],
match['volume_id']))
if len(new_list) > 0:
self.match_list = new_list
if len(self.match_list) == 1:
self.log_msg("--------------------------------------------------------------------------")
self.log_msg(
"--------------------------------------------------------------------------")
print_match(self.match_list[0])
self.log_msg("--------------------------------------------------------------------------")
self.search_result = self.result_one_good_match
self.log_msg(
"--------------------------------------------------------------------------")
self.search_result = self.ResultOneGoodMatch
elif len(self.match_list) == 0:
self.log_msg("--------------------------------------------------------------------------")
self.log_msg(
"--------------------------------------------------------------------------")
self.log_msg("No matches found :(")
self.log_msg("--------------------------------------------------------------------------")
self.search_result = self.result_no_matches
self.log_msg(
"--------------------------------------------------------------------------")
self.search_result = self.ResultNoMatches
else:
# we've got multiple good matches:
self.log_msg("More than one likely candidate.")
self.search_result = self.result_multiple_good_matches
self.log_msg("--------------------------------------------------------------------------")
self.search_result = self.ResultMultipleGoodMatches
self.log_msg(
"--------------------------------------------------------------------------")
for item in self.match_list:
print_match(item)
self.log_msg("--------------------------------------------------------------------------")
self.log_msg(
"--------------------------------------------------------------------------")
return self.match_list

View File

@ -14,56 +14,58 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
#import sys
#import os
#import re
from PyQt5 import QtCore, QtGui, QtWidgets, uic
#from PyQt5.QtCore import QUrl, pyqtSignal, QByteArray
#from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
from comicapi.issuestring import IssueString
from comictaggerlib.comicvinetalker import ComicVineTalker, ComicVineTalkerException
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.ui.qtutils import reduce_widget_font_size
logger = logging.getLogger(__name__)
from .comicvinetalker import ComicVineTalker, ComicVineTalkerException
from .settings import ComicTaggerSettings
from .issuestring import IssueString
from .coverimagewidget import CoverImageWidget
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
#from imagefetcher import ImageFetcher
#import utils
class IssueNumberTableWidgetItem(QtWidgets.QTableWidgetItem):
def __lt__(self, other):
self_str = self.data(QtCore.Qt.ItemDataRole.DisplayRole)
other_str = other.data(QtCore.Qt.ItemDataRole.DisplayRole)
return IssueString(self_str).as_float() < IssueString(other_str).as_float()
selfStr = self.data(QtCore.Qt.DisplayRole)
otherStr = other.data(QtCore.Qt.DisplayRole)
return (IssueString(selfStr).asFloat() <
IssueString(otherStr).asFloat())
class IssueSelectionWindow(QtWidgets.QDialog):
volume_id = 0
def __init__(self, parent, settings, series_id, issue_number):
super().__init__(parent)
super(IssueSelectionWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.get_ui_file("issueselectionwindow.ui"), self)
uic.loadUi(
ComicTaggerSettings.getUIFile('issueselectionwindow.ui'), self)
self.coverWidget = CoverImageWidget(self.coverImageContainer, CoverImageWidget.AltCoverMode)
self.coverWidget = CoverImageWidget(
self.coverImageContainer, CoverImageWidget.AltCoverMode)
gridlayout = QtWidgets.QGridLayout(self.coverImageContainer)
gridlayout.addWidget(self.coverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
reduce_widget_font_size(self.twList)
reduce_widget_font_size(self.teDescription, 1)
reduceWidgetFontSize(self.twList)
reduceWidgetFontSize(self.teDescription, 1)
self.setWindowFlags(
QtCore.Qt.WindowType(
self.windowFlags()
| QtCore.Qt.WindowType.WindowSystemMenuHint
| QtCore.Qt.WindowType.WindowMaximizeButtonHint
)
)
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
self.series_id = series_id
self.issue_id = None
self.settings = settings
self.url_fetch_thread = None
self.issue_list = []
if issue_number is None or issue_number == "":
self.issue_number = 1
@ -71,11 +73,11 @@ class IssueSelectionWindow(QtWidgets.QDialog):
self.issue_number = issue_number
self.initial_id = None
self.perform_query()
self.performQuery()
self.twList.resizeColumnsToContents()
self.twList.currentItemChanged.connect(self.current_item_changed)
self.twList.cellDoubleClicked.connect(self.cell_double_clicked)
self.twList.currentItemChanged.connect(self.currentItemChanged)
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
# now that the list has been sorted, find the initial record, and
# select it
@ -83,25 +85,32 @@ class IssueSelectionWindow(QtWidgets.QDialog):
self.twList.selectRow(0)
else:
for r in range(0, self.twList.rowCount()):
issue_id = self.twList.item(r, 0).data(QtCore.Qt.ItemDataRole.UserRole)
if issue_id == self.initial_id:
issue_id = self.twList.item(r, 0).data(QtCore.Qt.UserRole)
if (issue_id == self.initial_id):
self.twList.selectRow(r)
break
def perform_query(self):
def performQuery(self):
QtWidgets.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.CursorShape.WaitCursor))
QtWidgets.QApplication.setOverrideCursor(
QtGui.QCursor(QtCore.Qt.WaitCursor))
try:
comic_vine = ComicVineTalker()
comic_vine.fetch_volume_data(self.series_id)
self.issue_list = comic_vine.fetch_issues_by_volume(self.series_id)
comicVine = ComicVineTalker()
volume_data = comicVine.fetchVolumeData(self.series_id)
self.issue_list = comicVine.fetchIssuesByVolume(self.series_id)
except ComicVineTalkerException as e:
QtWidgets.QApplication.restoreOverrideCursor()
if e.code == ComicVineTalkerException.RateLimit:
QtWidgets.QMessageBox.critical(self, "Comic Vine Error", ComicVineTalker.get_rate_limit_message())
QtWidgets.QMessageBox.critical(
self,
self.tr("Comic Vine Error"),
ComicVineTalker.getRateLimitMessage())
else:
QtWidgets.QMessageBox.critical(self, "Network Issue", "Could not connect to Comic Vine to list issues!")
QtWidgets.QMessageBox.critical(
self,
self.tr("Network Issue"),
self.tr("Could not connect to Comic Vine to list issues!"))
return
while self.twList.rowCount() > 0:
@ -113,15 +122,15 @@ class IssueSelectionWindow(QtWidgets.QDialog):
for record in self.issue_list:
self.twList.insertRow(row)
item_text = record["issue_number"]
item_text = record['issue_number']
item = IssueNumberTableWidgetItem(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setData(QtCore.Qt.ItemDataRole.UserRole, record["id"])
item.setData(QtCore.Qt.ItemDataRole.DisplayRole, item_text)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setData(QtCore.Qt.UserRole, record['id'])
item.setData(QtCore.Qt.DisplayRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 0, item)
item_text = record["cover_date"]
item_text = record['cover_date']
if item_text is None:
item_text = ""
# remove the day of "YYYY-MM-DD"
@ -130,51 +139,50 @@ class IssueSelectionWindow(QtWidgets.QDialog):
item_text = parts[0] + "-" + parts[1]
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 1, item)
item_text = record["name"]
item_text = record['name']
if item_text is None:
item_text = ""
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 2, item)
if (
IssueString(record["issue_number"]).as_string().lower()
== IssueString(self.issue_number).as_string().lower()
):
self.initial_id = record["id"]
if IssueString(
record['issue_number']).asString().lower() == IssueString(
self.issue_number).asString().lower():
self.initial_id = record['id']
row += 1
self.twList.setSortingEnabled(True)
self.twList.sortItems(0, QtCore.Qt.SortOrder.AscendingOrder)
self.twList.sortItems(0, QtCore.Qt.AscendingOrder)
QtWidgets.QApplication.restoreOverrideCursor()
def cell_double_clicked(self, r, c):
def cellDoubleClicked(self, r, c):
self.accept()
def current_item_changed(self, curr, prev):
def currentItemChanged(self, curr, prev):
if curr is None:
return
if prev is not None and prev.row() == curr.row():
return
self.issue_id = self.twList.item(curr.row(), 0).data(QtCore.Qt.ItemDataRole.UserRole)
self.issue_id = self.twList.item(curr.row(), 0).data(QtCore.Qt.UserRole)
# list selection was changed, update the the issue cover
for record in self.issue_list:
if record["id"] == self.issue_id:
self.issue_number = record["issue_number"]
self.coverWidget.set_issue_id(int(self.issue_id))
if record["description"] is None:
if record['id'] == self.issue_id:
self.issue_number = record['issue_number']
self.coverWidget.setIssueID(int(self.issue_id))
if record['description'] is None:
self.teDescription.setText("")
else:
self.teDescription.setText(record["description"])
self.teDescription.setText(record['description'])
break

View File

@ -0,0 +1 @@
from comicapi.issuestring import *

View File

@ -14,31 +14,26 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#import sys
#import os
import logging
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5 import QtCore, QtWidgets, uic
from comictaggerlib.settings import ComicTaggerSettings
logger = logging.getLogger(__name__)
from .settings import ComicTaggerSettings
class LogWindow(QtWidgets.QDialog):
def __init__(self, parent):
super().__init__(parent)
super(LogWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.get_ui_file("logwindow.ui"), self)
uic.loadUi(ComicTaggerSettings.getUIFile('logwindow.ui'), self)
self.setWindowFlags(
QtCore.Qt.WindowType(
self.windowFlags()
| QtCore.Qt.WindowType.WindowSystemMenuHint
| QtCore.Qt.WindowType.WindowMaximizeButtonHint
)
)
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
def set_text(self, text):
def setText(self, text):
try:
text = text.decode()
except:

View File

@ -14,64 +14,32 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import logging.handlers
import os
import pathlib
import platform
import signal
import sys
import signal
import traceback
import platform
import pkg_resources
from comictaggerlib import cli
from comictaggerlib.comicvinetalker import ComicVineTalker
from comictaggerlib.ctversion import version
from comictaggerlib.options import Options
from comictaggerlib.settings import ComicTaggerSettings
logger = logging.getLogger("comictagger")
logging.getLogger("comicapi").setLevel(logging.DEBUG)
logger.setLevel(logging.DEBUG)
from .settings import ComicTaggerSettings
# Need to load setting before anything else
SETTINGS = ComicTaggerSettings()
try:
qt_available = True
from PyQt5 import QtGui, QtWidgets
from comictaggerlib.taggerwindow import TaggerWindow
from PyQt5 import QtCore, QtGui, QtWidgets
from .taggerwindow import TaggerWindow
except ImportError as e:
logging.debug(e)
qt_available = False
def rotate(handler: logging.handlers.RotatingFileHandler, filename: pathlib.Path):
if filename.is_file() and filename.stat().st_size > 0:
handler.doRollover()
from . import utils
from . import cli
from .options import Options
from .comicvinetalker import ComicVineTalker
def ctmain():
os.makedirs(ComicTaggerSettings.get_settings_folder() / "logs", exist_ok=True)
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.WARNING)
file_handler = logging.handlers.RotatingFileHandler(
ComicTaggerSettings.get_settings_folder() / "logs" / "ComicTagger.log", encoding="utf-8", backupCount=10
)
rotate(file_handler, ComicTaggerSettings.get_settings_folder() / "logs" / "ComicTagger.log")
logging.basicConfig(
handlers=[
stream_handler,
file_handler,
],
level=logging.WARNING,
format="%(asctime)s | %(name)s | %(levelname)s | %(message)s",
datefmt="%Y-%m-%dT%H:%M:%S",
)
opts = Options()
opts.parse_cmd_line_args()
# Need to load setting before anything else
SETTINGS = ComicTaggerSettings()
opts.parseCmdLineArgs()
# manage the CV API key
if opts.cv_api_key:
@ -86,53 +54,34 @@ def ctmain():
signal.signal(signal.SIGINT, signal.SIG_DFL)
logger.info(
"ComicTagger Version: %s running on: %s PyInstaller: %s",
version,
platform.system(),
"Yes" if getattr(sys, "frozen", None) else "No",
)
logger.debug("Installed Packages")
for pkg in sorted(pkg_resources.working_set, key=lambda x: x.project_name):
logger.debug("%s\t%s", pkg.project_name, pkg.version)
if not qt_available and not opts.no_gui:
opts.no_gui = True
print("PyQt5 is not available. ComicTagger is limited to command-line mode.")
logger.info("PyQt5 is not available. ComicTagger is limited to command-line mode.")
print("PyQt5 is not available. ComicTagger is limited to command-line mode.", file=sys.stderr)
if opts.no_gui:
try:
cli.cli_mode(opts, SETTINGS)
except:
logger.exception()
cli.cli_mode(opts, SETTINGS)
else:
os.environ["QtWidgets.QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
args = []
if opts.darkmode:
args.extend(["-platform", "windows:darkmode=2"])
args.extend(sys.argv)
app = QtWidgets.QApplication(args)
os.environ['QT_AUTO_SCREEN_SCALE_FACTOR'] = '1'
app = QtWidgets.QApplication(sys.argv)
if platform.system() == "Darwin":
# Set the MacOS dock icon
app.setWindowIcon(QtGui.QIcon(ComicTaggerSettings.get_graphic("app.png")))
app.setWindowIcon(
QtGui.QIcon(ComicTaggerSettings.getGraphic('app.png')))
if platform.system() == "Windows":
# For pure python, tell windows that we're not python,
# so we can have our own taskbar icon
import ctypes
myappid = "comictagger" # arbitrary string
myappid = u'comictagger' # arbitrary string
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
# force close of console window
swp_hidewindow = 0x0080
console_wnd = ctypes.windll.kernel32.GetConsoleWindow()
if console_wnd != 0:
ctypes.windll.user32.SetWindowPos(console_wnd, None, 0, 0, 0, 0, swp_hidewindow)
SWP_HIDEWINDOW = 0x0080
consoleWnd = ctypes.windll.kernel32.GetConsoleWindow()
if consoleWnd != 0:
ctypes.windll.user32.SetWindowPos(consoleWnd, None, 0, 0, 0, 0, SWP_HIDEWINDOW)
if platform.system() != "Linux":
img = QtGui.QPixmap(ComicTaggerSettings.get_graphic("tags.png"))
img = QtGui.QPixmap(ComicTaggerSettings.getGraphic('tags.png'))
splash = QtWidgets.QSplashScreen(img)
splash.show()
@ -141,15 +90,17 @@ def ctmain():
try:
tagger_window = TaggerWindow(opts.file_list, SETTINGS, opts=opts)
tagger_window.setWindowIcon(QtGui.QIcon(ComicTaggerSettings.get_graphic("app.png")))
tagger_window.setWindowIcon(
QtGui.QIcon(ComicTaggerSettings.getGraphic('app.png')))
tagger_window.show()
if platform.system() != "Linux":
splash.finish(tagger_window)
sys.exit(app.exec())
except Exception:
logger.exception()
sys.exit(app.exec_())
except Exception as e:
QtWidgets.QMessageBox.critical(
QtWidgets.QMainWindow(), "Error", "Unhandled exception in app:\n" + traceback.format_exc()
)
QtWidgets.QMainWindow(),
"Error",
"Unhandled exception in app:\n" +
traceback.format_exc())

View File

@ -14,66 +14,70 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import os
#import sys
from PyQt5 import QtCore, QtWidgets, uic
from PyQt5 import QtCore, QtGui, QtWidgets, uic
#from PyQt5.QtCore import QUrl, pyqtSignal, QByteArray
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.ui.qtutils import reduce_widget_font_size
logger = logging.getLogger(__name__)
from .settings import ComicTaggerSettings
from .coverimagewidget import CoverImageWidget
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
#from imagefetcher import ImageFetcher
#from comicarchive import MetaDataStyle
#from comicvinetalker import ComicVineTalker
#import utils
class MatchSelectionWindow(QtWidgets.QDialog):
volume_id = 0
def __init__(self, parent, matches, comic_archive):
super().__init__(parent)
super(MatchSelectionWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.get_ui_file("matchselectionwindow.ui"), self)
uic.loadUi(
ComicTaggerSettings.getUIFile('matchselectionwindow.ui'), self)
self.altCoverWidget = CoverImageWidget(self.altCoverContainer, CoverImageWidget.AltCoverMode)
self.altCoverWidget = CoverImageWidget(
self.altCoverContainer, CoverImageWidget.AltCoverMode)
gridlayout = QtWidgets.QGridLayout(self.altCoverContainer)
gridlayout.addWidget(self.altCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.archiveCoverWidget = CoverImageWidget(self.archiveCoverContainer, CoverImageWidget.ArchiveMode)
self.archiveCoverWidget = CoverImageWidget(
self.archiveCoverContainer, CoverImageWidget.ArchiveMode)
gridlayout = QtWidgets.QGridLayout(self.archiveCoverContainer)
gridlayout.addWidget(self.archiveCoverWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
reduce_widget_font_size(self.twList)
reduce_widget_font_size(self.teDescription, 1)
reduceWidgetFontSize(self.twList)
reduceWidgetFontSize(self.teDescription, 1)
self.setWindowFlags(
QtCore.Qt.WindowType(
self.windowFlags()
| QtCore.Qt.WindowType.WindowSystemMenuHint
| QtCore.Qt.WindowType.WindowMaximizeButtonHint
)
)
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
self.matches = matches
self.comic_archive = comic_archive
self.twList.currentItemChanged.connect(self.current_item_changed)
self.twList.cellDoubleClicked.connect(self.cell_double_clicked)
self.twList.currentItemChanged.connect(self.currentItemChanged)
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
self.update_data()
self.updateData()
def update_data(self):
def updateData(self):
self.set_cover_image()
self.populate_table()
self.setCoverImage()
self.populateTable()
self.twList.resizeColumnsToContents()
self.twList.selectRow(0)
path = self.comic_archive.path
self.setWindowTitle(f"Select correct match: {os.path.split(path)[1]}")
self.setWindowTitle("Select correct match: {0}".format(
os.path.split(path)[1]))
def populate_table(self):
def populateTable(self):
while self.twList.rowCount() > 0:
self.twList.removeRow(0)
@ -84,72 +88,73 @@ class MatchSelectionWindow(QtWidgets.QDialog):
for match in self.matches:
self.twList.insertRow(row)
item_text = match["series"]
item_text = match['series']
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setData(QtCore.Qt.ItemDataRole.UserRole, (match,))
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setData(QtCore.Qt.UserRole, (match,))
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 0, item)
if match["publisher"] is not None:
item_text = str(match["publisher"])
if match['publisher'] is not None:
item_text = "{0}".format(match['publisher'])
else:
item_text = "Unknown"
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 1, item)
month_str = ""
year_str = "????"
if match["month"] is not None:
month_str = f"-{int(match['month']):02d}"
if match["year"] is not None:
year_str = str(match["year"])
if match['month'] is not None:
month_str = "-{0:02d}".format(int(match['month']))
if match['year'] is not None:
year_str = "{0}".format(match['year'])
item_text = year_str + month_str
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 2, item)
item_text = match["issue_title"]
item_text = match['issue_title']
if item_text is None:
item_text = ""
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 3, item)
row += 1
self.twList.resizeColumnsToContents()
self.twList.setSortingEnabled(True)
self.twList.sortItems(2, QtCore.Qt.SortOrder.AscendingOrder)
self.twList.sortItems(2, QtCore.Qt.AscendingOrder)
self.twList.selectRow(0)
self.twList.resizeColumnsToContents()
self.twList.horizontalHeader().setStretchLastSection(True)
def cell_double_clicked(self, r, c):
def cellDoubleClicked(self, r, c):
self.accept()
def current_item_changed(self, curr, prev):
def currentItemChanged(self, curr, prev):
if curr is None:
return
if prev is not None and prev.row() == curr.row():
return
self.altCoverWidget.set_issue_id(self.current_match()["issue_id"])
if self.current_match()["description"] is None:
self.altCoverWidget.setIssueID(self.currentMatch()['issue_id'])
if self.currentMatch()['description'] is None:
self.teDescription.setText("")
else:
self.teDescription.setText(self.current_match()["description"])
self.teDescription.setText(self.currentMatch()['description'])
def set_cover_image(self):
self.archiveCoverWidget.set_archive(self.comic_archive)
def setCoverImage(self):
self.archiveCoverWidget.setArchive(self.comic_archive)
def current_match(self):
def currentMatch(self):
row = self.twList.currentRow()
match = self.twList.item(row, 0).data(QtCore.Qt.ItemDataRole.UserRole)[0]
match = self.twList.item(row, 0).data(
QtCore.Qt.UserRole)[0]
return match

View File

@ -6,7 +6,7 @@ checked = OptionalMessageDialog.msg(self, "Disclaimer",
"This is beta software, and you are using it at your own risk!",
)
said_yes, checked = OptionalMessageDialog.question(self, "QtWidgets.Question",
said_yes, checked = OptionalMessageDialog.question(self, "Question",
"Are you sure you wish to do this?",
)
"""
@ -25,36 +25,35 @@ said_yes, checked = OptionalMessageDialog.question(self, "QtWidgets.Question",
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5 import QtCore, QtWidgets
logger = logging.getLogger(__name__)
StyleMessage = 0
StyleQuestion = 1
class OptionalMessageDialog(QtWidgets.QDialog):
def __init__(self, parent, style, title, msg, check_state=QtCore.Qt.CheckState.Unchecked, check_text=None):
super().__init__(parent)
class OptionalMessageDialog(QDialog):
def __init__(self, parent, style, title, msg,
check_state=Qt.Unchecked, check_text=None):
QDialog.__init__(self, parent)
self.setWindowTitle(title)
self.was_accepted = False
layout = QtWidgets.QVBoxLayout(self)
self.theLabel = QtWidgets.QLabel(msg)
l = QVBoxLayout(self)
self.theLabel = QLabel(msg)
self.theLabel.setWordWrap(True)
self.theLabel.setTextFormat(QtCore.Qt.TextFormat.RichText)
self.theLabel.setTextFormat(Qt.RichText)
self.theLabel.setOpenExternalLinks(True)
self.theLabel.setTextInteractionFlags(
QtCore.Qt.TextInteractionFlag.TextSelectableByMouse
| QtCore.Qt.TextInteractionFlag.LinksAccessibleByMouse
| QtCore.Qt.TextInteractionFlag.LinksAccessibleByKeyboard
)
Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
layout.addWidget(self.theLabel)
layout.insertSpacing(-1, 10)
l.addWidget(self.theLabel)
l.insertSpacing(-1, 10)
if check_text is None:
if style == StyleQuestion:
@ -62,46 +61,58 @@ class OptionalMessageDialog(QtWidgets.QDialog):
else:
check_text = "Don't show this message again"
self.theCheckBox = QtWidgets.QCheckBox(check_text)
self.theCheckBox = QCheckBox(check_text)
self.theCheckBox.setCheckState(check_state)
layout.addWidget(self.theCheckBox)
l.addWidget(self.theCheckBox)
btnbox_style = QtWidgets.QDialogButtonBox.StandardButton.Ok
btnbox_style = QDialogButtonBox.Ok
if style == StyleQuestion:
btnbox_style = QtWidgets.QDialogButtonBox.StandardButton.Yes | QtWidgets.QDialogButtonBox.StandardButton.No
btnbox_style = QDialogButtonBox.Yes | QDialogButtonBox.No
self.theButtonBox = QtWidgets.QDialogButtonBox(
self.theButtonBox = QDialogButtonBox(
btnbox_style,
parent=self,
accepted=self.accept,
rejected=self.reject,
)
rejected=self.reject)
layout.addWidget(self.theButtonBox)
l.addWidget(self.theButtonBox)
def accept(self):
self.was_accepted = True
QtWidgets.QDialog.accept(self)
QDialog.accept(self)
def reject(self):
self.was_accepted = False
QtWidgets.QDialog.reject(self)
QDialog.reject(self)
@staticmethod
def msg(parent, title, msg, check_state=QtCore.Qt.CheckState.Unchecked, check_text=None):
def msg(parent, title, msg, check_state=Qt.Unchecked, check_text=None):
d = OptionalMessageDialog(parent, StyleMessage, title, msg, check_state=check_state, check_text=check_text)
d = OptionalMessageDialog(
parent,
StyleMessage,
title,
msg,
check_state=check_state,
check_text=check_text)
d.exec()
d.exec_()
return d.theCheckBox.isChecked()
@staticmethod
def question(parent, title, msg, check_state=QtCore.Qt.CheckState.Unchecked, check_text=None):
def question(
parent, title, msg, check_state=Qt.Unchecked, check_text=None):
d = OptionalMessageDialog(parent, StyleQuestion, title, msg, check_state=check_state, check_text=check_text)
d = OptionalMessageDialog(
parent,
StyleQuestion,
title,
msg,
check_state=check_state,
check_text=check_text)
d.exec()
d.exec_()
return d.was_accepted, d.theCheckBox.isChecked()

View File

@ -14,18 +14,23 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import getopt
import logging
import os
import platform
import sys
import getopt
import platform
import os
import traceback
from comicapi import utils
from comicapi.comicarchive import MetaDataStyle
from comicapi.genericmetadata import GenericMetadata
from comictaggerlib import ctversion
try:
import argparse
except ImportError:
pass
logger = logging.getLogger(__name__)
from datetime import datetime
from .genericmetadata import GenericMetadata
from .comicarchive import MetaDataStyle
from .versionchecker import VersionChecker
from . import ctversion
from . import utils
class Options:
@ -96,13 +101,11 @@ If no options are given, {0} will run in windowed mode.
error, wait and retry query.
-v, --verbose Be noisy when doing what it does.
--terse Don't say much (for print mode).
--darkmode Windows only. Force a dark pallet
--config=CONFIG_DIR Config directory defaults to ~/.ComicTagger
--version Display version.
-h, --help Display this message.
For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
"""
"""
def __init__(self):
self.data_style = None
@ -136,9 +139,6 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
self.wait_and_retry_on_rate_limit = False
self.assume_issue_is_one_if_not_set = False
self.file_list = []
self.darkmode = False
self.copy_source = None
self.config_path = ""
def display_msg_and_quit(self, msg, code, show_help=False):
appname = os.path.basename(sys.argv[0])
@ -150,7 +150,7 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
print("For more help, run with '--help'")
sys.exit(code)
def parse_metadata_from_string(self, mdstr):
def parseMetadataFromString(self, mdstr):
"""The metadata string is a comma separated list of name-value pairs
The names match the attributes of the internal metadata struct (for now)
The caret is the special "escape character", since it's not common in
@ -165,7 +165,8 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
md = GenericMetadata()
# First, replace escaped commas with with a unique token (to be changed back later)
# First, replace escaped commas with with a unique token (to be changed
# back later)
mdstr = mdstr.replace(escaped_comma, replacement_token)
tmp_list = mdstr.split(",")
md_list = []
@ -174,7 +175,7 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
md_list.append(item)
# Now build a nice dict from the list
md_dict = {}
md_dict = dict()
for item in md_list:
# Make sure to fix any escaped equal signs
i = item.replace(escaped_equals, replacement_token)
@ -184,38 +185,44 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
if key.lower() == "credit":
cred_attribs = value.split(":")
role = cred_attribs[0]
person = cred_attribs[1] if len(cred_attribs) > 1 else ""
primary = len(cred_attribs) > 2
md.add_credit(person.strip(), role.strip(), primary)
person = (cred_attribs[1] if len(cred_attribs) > 1 else "")
primary = (cred_attribs[2] if len(cred_attribs) > 2 else None)
md.addCredit(
person.strip(),
role.strip(),
True if primary is not None else False)
else:
md_dict[key] = value
# Map the dict to the metadata object
for key, value in md_dict.items():
for key in md_dict:
if not hasattr(md, key):
logger.warning("'%s' is not a valid tag name", key)
print(("Warning: '{0}' is not a valid tag name".format(key)))
else:
md.is_empty = False
setattr(md, key, value)
md.isEmpty = False
setattr(md, key, md_dict[key])
# print(md)
return md
def launch_script(self, scriptfile):
# we were given a script. special case for the args:
# 1. ignore everything before the -S,
# 2. pass all the ones that follow (including script name) to the script
script_args = []
# 2. pass all the ones that follow (including script name) to the
# script
script_args = list()
for idx, arg in enumerate(sys.argv):
if arg in ["-S", "--script"]:
if arg in ['-S', '--script']:
# found script!
script_args = sys.argv[idx + 1 :]
script_args = sys.argv[idx + 1:]
break
sys.argv = script_args
if not os.path.exists(scriptfile):
logger.error("Can't find %s", scriptfile)
print(("Can't find {0}".format(scriptfile)))
else:
# I *think* this makes sense:
# assume the base name of the file is the module name
# add the folder of the given file to the python path import module
# assume the base name of the file is the module name
# add the folder of the given file to the python path
# import module
dirname = os.path.dirname(scriptfile)
module_name = os.path.splitext(os.path.basename(scriptfile))[0]
sys.path = [dirname] + sys.path
@ -226,23 +233,27 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
if "main" in dir(script):
script.main()
else:
logger.error("Can't find entry point 'main()' in module '%s'", module_name)
except Exception:
logger.exception("Script: %s raised an unhandled exception: ", module_name)
print((
"Can't find entry point \"main()\" in module \"{0}\"".format(module_name)))
except Exception as e:
print("Script raised an unhandled exception: ", e)
print((traceback.format_exc()))
sys.exit(0)
def parse_cmd_line_args(self):
def parseCmdLineArgs(self):
if platform.system() == "Darwin" and hasattr(sys, "frozen") and sys.frozen == 1:
# remove the PSN (process serial number) argument from OS/X
if platform.system() == "Darwin" and hasattr(
sys, "frozen") and sys.frozen == 1:
# 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:
input_args = sys.argv[1:]
# first check if we're launching a script:
for n in range(len(input_args)):
if input_args[n] in ["-S", "--script"] and n + 1 < len(input_args):
if (input_args[n] in ["-S", "--script"] and
n + 1 < len(input_args)):
# insert a "--" which will cause getopt to ignore the remaining args
# so they will be passed to the script
input_args.insert(n + 2, "--")
@ -250,43 +261,15 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
# parse command line options
try:
opts, args = getopt.getopt(
input_args,
"hpdt:fm:vownsrc:ieRS:1",
[
"help",
"print",
"delete",
"type=",
"copy=",
"parsefilename",
"metadata=",
"verbose",
"online",
"dryrun",
"save",
"rename",
"raw",
"noabort",
"terse",
"nooverwrite",
"interactive",
"nosummary",
"version",
"id=",
"recursive",
"script=",
"export-to-zip",
"delete-rar",
"abort-on-conflict",
"assume-issue-one",
"cv-api-key=",
"only-set-cv-key",
"wait-on-cv-rate-limit",
"darkmode",
"config=",
],
)
opts, args = getopt.getopt(input_args,
"hpdt:fm:vownsrc:ieRS:1",
["help", "print", "delete", "type=", "copy=", "parsefilename",
"metadata=", "verbose", "online", "dryrun", "save", "rename",
"raw", "noabort", "terse", "nooverwrite", "interactive",
"nosummary", "version", "id=", "recursive", "script=",
"export-to-zip", "delete-rar", "abort-on-conflict",
"assume-issue-one", "cv-api-key=", "only-set-cv-key",
"wait-on-cv-rate-limit"])
except getopt.GetoptError as err:
self.display_msg_and_quit(str(err), 2)
@ -310,7 +293,6 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
self.interactive = True
if o in ("-c", "--copy"):
self.copy_tags = True
if a.lower() == "cr":
self.copy_source = MetaDataStyle.CIX
elif a.lower() == "cbl":
@ -318,13 +300,14 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
elif a.lower() == "comet":
self.copy_source = MetaDataStyle.COMET
else:
self.display_msg_and_quit("Invalid copy tag source type", 1)
self.display_msg_and_quit(
"Invalid copy tag source type", 1)
if o in ("-o", "--online"):
self.search_online = True
if o in ("-n", "--dryrun"):
self.dryrun = True
if o in ("-m", "--metadata"):
self.metadata = self.parse_metadata_from_string(a)
self.metadata = self.parseMetadataFromString(a)
if o in ("-s", "--save"):
self.save_tags = True
if o in ("-r", "--rename"):
@ -339,8 +322,6 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
self.parse_filename = True
if o in ("-w", "--wait-on-cv-rate-limit"):
self.wait_and_retry_on_rate_limit = True
if o == "--config":
self.config_path = os.path.abspath(a)
if o == "--id":
self.issue_id = a
if o == "--raw":
@ -360,8 +341,10 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
if o == "--only-set-cv-key":
self.only_set_key = True
if o == "--version":
print(f"ComicTagger {ctversion.version}: Copyright (c) 2012-2022 ComicTagger Team")
print("Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)")
print((
"ComicTagger {}: Copyright (c) 2012-{:%Y} ComicTagger Team".format(ctversion.version, datetime.today())))
print(
"Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)")
sys.exit(0)
if o in ("-t", "--type"):
if a.lower() == "cr":
@ -372,20 +355,8 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
self.data_style = MetaDataStyle.COMET
else:
self.display_msg_and_quit("Invalid tag type", 1)
if o == "--darkmode":
self.darkmode = True
if any(
[
self.print_tags,
self.delete_tags,
self.save_tags,
self.copy_tags,
self.rename_file,
self.export_to_zip,
self.only_set_key,
]
):
if self.print_tags or self.delete_tags or self.save_tags or self.copy_tags or self.rename_file or self.export_to_zip or self.only_set_key:
self.no_gui = True
count = 0
@ -408,8 +379,8 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
if count > 1:
self.display_msg_and_quit(
"Must choose only one action of print, delete, save, copy, rename, export, set key, or run script", 1
)
"Must choose only one action of print, delete, save, copy, rename, export, set key, or run script",
1)
if self.script is not None:
self.launch_script(self.script)
@ -418,7 +389,6 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
if platform.system() == "Windows":
# no globbing on windows shell, so do it for them
import glob
self.file_list = []
for item in args:
self.file_list.extend(glob.glob(item))
@ -431,17 +401,25 @@ For more help visit the wiki at: https://github.com/comictagger/comictagger/wiki
if self.only_set_key and self.cv_api_key is None:
self.display_msg_and_quit("Key not given!", 1)
if not self.only_set_key and self.no_gui and self.filename is None:
self.display_msg_and_quit("Command requires at least one filename!", 1)
if (self.only_set_key == False) and self.no_gui and (
self.filename is None):
self.display_msg_and_quit(
"Command requires at least one filename!", 1)
if self.delete_tags and self.data_style is None:
self.display_msg_and_quit("Please specify the type to delete with -t", 1)
self.display_msg_and_quit(
"Please specify the type to delete with -t", 1)
if self.save_tags and self.data_style is None:
self.display_msg_and_quit("Please specify the type to save with -t", 1)
self.display_msg_and_quit(
"Please specify the type to save with -t", 1)
if self.copy_tags and self.data_style is None:
self.display_msg_and_quit("Please specify the type to copy to with -t", 1)
self.display_msg_and_quit(
"Please specify the type to copy to with -t", 1)
# if self.rename_file and self.data_style is None:
# self.display_msg_and_quit("Please specify the type to use for renaming with -t", 1)
if self.recursive:
self.file_list = utils.get_recursive_filelist(self.file_list)

View File

@ -14,53 +14,51 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import platform
#import sys
#import os
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comicapi.comicarchive import ComicArchive
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.settings import ComicTaggerSettings
logger = logging.getLogger(__name__)
from .settings import ComicTaggerSettings
from .coverimagewidget import CoverImageWidget
class PageBrowserWindow(QtWidgets.QDialog):
def __init__(self, parent, metadata):
super().__init__(parent)
super(PageBrowserWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.get_ui_file("pagebrowser.ui"), self)
uic.loadUi(ComicTaggerSettings.getUIFile('pagebrowser.ui'), self)
self.pageWidget = CoverImageWidget(self.pageContainer, CoverImageWidget.ArchiveMode)
self.pageWidget = CoverImageWidget(
self.pageContainer, CoverImageWidget.ArchiveMode)
gridlayout = QtWidgets.QGridLayout(self.pageContainer)
gridlayout.addWidget(self.pageWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.pageWidget.showControls = False
self.setWindowFlags(
QtCore.Qt.WindowType(
self.windowFlags()
| QtCore.Qt.WindowType.WindowSystemMenuHint
| QtCore.Qt.WindowType.WindowMaximizeButtonHint
)
)
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
self.comic_archive = None
self.page_count = 0
self.current_page_num = 0
self.metadata = metadata
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Close).setDefault(True)
self.buttonBox.button(QtWidgets.QDialogButtonBox.Close).setDefault(True)
if platform.system() == "Darwin":
self.btnPrev.setText("<<")
self.btnNext.setText(">>")
else:
self.btnPrev.setIcon(QtGui.QIcon(ComicTaggerSettings.get_graphic("left.png")))
self.btnNext.setIcon(QtGui.QIcon(ComicTaggerSettings.get_graphic("right.png")))
self.btnPrev.setIcon(
QtGui.QIcon(ComicTaggerSettings.getGraphic('left.png')))
self.btnNext.setIcon(
QtGui.QIcon(ComicTaggerSettings.getGraphic('right.png')))
self.btnNext.clicked.connect(self.next_page)
self.btnPrev.clicked.connect(self.prev_page)
self.btnNext.clicked.connect(self.nextPage)
self.btnPrev.clicked.connect(self.prevPage)
self.show()
self.btnNext.setEnabled(False)
@ -76,39 +74,41 @@ class PageBrowserWindow(QtWidgets.QDialog):
self.btnPrev.setEnabled(False)
self.pageWidget.clear()
def set_comic_archive(self, ca: ComicArchive):
def setComicArchive(self, ca):
self.comic_archive = ca
self.page_count = ca.get_number_of_pages()
self.page_count = ca.getNumberOfPages()
self.current_page_num = 0
self.pageWidget.set_archive(self.comic_archive)
self.set_page()
self.pageWidget.setArchive(self.comic_archive)
self.setPage()
if self.page_count > 1:
self.btnNext.setEnabled(True)
self.btnPrev.setEnabled(True)
def next_page(self):
def nextPage(self):
if self.current_page_num + 1 < self.page_count:
self.current_page_num += 1
else:
self.current_page_num = 0
self.set_page()
self.setPage()
def prev_page(self):
def prevPage(self):
if self.current_page_num - 1 >= 0:
self.current_page_num -= 1
else:
self.current_page_num = self.page_count - 1
self.set_page()
self.setPage()
def set_page(self):
def setPage(self):
if self.metadata is not None:
archive_page_index = self.metadata.get_archive_page_index(self.current_page_num)
archive_page_index = self.metadata.getArchivePageIndex(
self.current_page_num)
else:
archive_page_index = self.current_page_num
self.pageWidget.set_page(archive_page_index)
self.setWindowTitle(f"Page Browser - Page {self.current_page_num + 1} (of {self.page_count}) ")
self.pageWidget.setPage(archive_page_index)
self.setWindowTitle(
"Page Browser - Page {0} (of {1}) ".format(self.current_page_num + 1, self.page_count))

View File

@ -14,47 +14,50 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from typing import Optional
#import os
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5 import uic
from comicapi.comicarchive import ComicArchive, MetaDataStyle
from comicapi.genericmetadata import PageType
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.settings import ComicTaggerSettings
logger = logging.getLogger(__name__)
from .settings import ComicTaggerSettings
from .genericmetadata import GenericMetadata, PageType
from .comicarchive import MetaDataStyle
from .coverimagewidget import CoverImageWidget
#from pageloader import PageLoader
def item_move_events(widget):
class Filter(QtCore.QObject):
def itemMoveEvents(widget):
mysignal = QtCore.pyqtSignal(str)
class Filter(QObject):
mysignal = pyqtSignal(str)
def eventFilter(self, obj, event):
if obj == widget:
# print(event.type())
if event.type() == QtCore.QEvent.Type.ChildRemoved:
if event.type() == QEvent.ChildRemoved:
# print("ChildRemoved")
self.mysignal.emit("finish")
if event.type() == QtCore.QEvent.Type.ChildAdded:
if event.type() == QEvent.ChildAdded:
# print("ChildAdded")
self.mysignal.emit("start")
return True
return False
filt = Filter(widget)
widget.installEventFilter(filt)
return filt.mysignal
filter = Filter(widget)
widget.installEventFilter(filter)
return filter.mysignal
class PageListEditor(QtWidgets.QWidget):
firstFrontCoverChanged = QtCore.pyqtSignal(int)
listOrderChanged = QtCore.pyqtSignal()
modified = QtCore.pyqtSignal()
class PageListEditor(QWidget):
firstFrontCoverChanged = pyqtSignal(int)
listOrderChanged = pyqtSignal()
modified = pyqtSignal()
pageTypeNames = {
PageType.FrontCover: "Front Cover",
@ -71,286 +74,209 @@ class PageListEditor(QtWidgets.QWidget):
}
def __init__(self, parent):
super().__init__(parent)
super(PageListEditor, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.get_ui_file("pagelisteditor.ui"), self)
uic.loadUi(ComicTaggerSettings.getUIFile('pagelisteditor.ui'), self)
self.pageWidget = CoverImageWidget(self.pageContainer, CoverImageWidget.ArchiveMode)
gridlayout = QtWidgets.QGridLayout(self.pageContainer)
self.pageWidget = CoverImageWidget(
self.pageContainer, CoverImageWidget.ArchiveMode)
gridlayout = QGridLayout(self.pageContainer)
gridlayout.addWidget(self.pageWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
self.pageWidget.showControls = False
self.reset_page()
self.resetPage()
# Add the entries to the manga combobox
self.comboBox.addItem("", "")
self.comboBox.addItem(self.pageTypeNames[PageType.FrontCover], PageType.FrontCover)
self.comboBox.addItem(self.pageTypeNames[PageType.InnerCover], PageType.InnerCover)
self.comboBox.addItem(self.pageTypeNames[PageType.Advertisement], PageType.Advertisement)
self.comboBox.addItem(self.pageTypeNames[PageType.Roundup], PageType.Roundup)
self.comboBox.addItem(self.pageTypeNames[PageType.Story], PageType.Story)
self.comboBox.addItem(self.pageTypeNames[PageType.Editorial], PageType.Editorial)
self.comboBox.addItem(self.pageTypeNames[PageType.Letters], PageType.Letters)
self.comboBox.addItem(self.pageTypeNames[PageType.Preview], PageType.Preview)
self.comboBox.addItem(self.pageTypeNames[PageType.BackCover], PageType.BackCover)
self.comboBox.addItem(self.pageTypeNames[PageType.Other], PageType.Other)
self.comboBox.addItem(self.pageTypeNames[PageType.Deleted], PageType.Deleted)
self.comboBox.addItem(
self.pageTypeNames[PageType.FrontCover], PageType.FrontCover)
self.comboBox.addItem(
self.pageTypeNames[PageType.InnerCover], PageType.InnerCover)
self.comboBox.addItem(
self.pageTypeNames[PageType.Advertisement], PageType.Advertisement)
self.comboBox.addItem(
self.pageTypeNames[PageType.Roundup], PageType.Roundup)
self.comboBox.addItem(
self.pageTypeNames[PageType.Story], PageType.Story)
self.comboBox.addItem(
self.pageTypeNames[PageType.Editorial], PageType.Editorial)
self.comboBox.addItem(
self.pageTypeNames[PageType.Letters], PageType.Letters)
self.comboBox.addItem(
self.pageTypeNames[PageType.Preview], PageType.Preview)
self.comboBox.addItem(
self.pageTypeNames[PageType.BackCover], PageType.BackCover)
self.comboBox.addItem(
self.pageTypeNames[PageType.Other], PageType.Other)
self.comboBox.addItem(
self.pageTypeNames[PageType.Deleted], PageType.Deleted)
self.listWidget.itemSelectionChanged.connect(self.change_page)
item_move_events(self.listWidget).connect(self.item_move_event)
self.comboBox.activated.connect(self.change_page_type)
self.leBookmark.editingFinished.connect(self.save_bookmark)
self.btnUp.clicked.connect(self.move_current_up)
self.btnDown.clicked.connect(self.move_current_down)
self.listWidget.itemSelectionChanged.connect(self.changePage)
itemMoveEvents(self.listWidget).connect(self.itemMoveEvent)
self.comboBox.activated.connect(self.changePageType)
self.btnUp.clicked.connect(self.moveCurrentUp)
self.btnDown.clicked.connect(self.moveCurrentDown)
self.pre_move_row = -1
self.first_front_page = None
self.comic_archive: Optional[ComicArchive] = None
self.pages_list = []
def reset_page(self):
def resetPage(self):
self.pageWidget.clear()
self.comboBox.setDisabled(True)
self.leBookmark.setDisabled(True)
self.comic_archive = None
self.pages_list = []
self.pages_list = None
def get_new_indexes(self, movement):
selection = self.listWidget.selectionModel().selectedRows()
selection.sort(reverse=movement > 0)
newindexes = []
oldindexes = []
for x in selection:
current = x.row()
oldindexes.append(current)
if 0 <= current + movement <= self.listWidget.count() - 1:
if len(newindexes) < 1 or current + movement != newindexes[-1]:
current += movement
newindexes.append(current)
oldindexes.sort()
newindexes.sort()
return list(zip(newindexes, oldindexes))
def set_selection(self, indexes):
selection_ranges = []
first = 0
for i, selection in enumerate(indexes):
if i == 0:
first = selection[0]
continue
if selection != indexes[i - 1][0] + 1:
selection_ranges.append((first, indexes[i - 1][0]))
first = selection[0]
selection_ranges.append((first, indexes[-1][0]))
selection = QtCore.QItemSelection()
for x in selection_ranges:
selection.merge(
QtCore.QItemSelection(self.listWidget.model().index(x[0], 0), self.listWidget.model().index(x[1], 0)),
QtCore.QItemSelectionModel.SelectionFlag.Select,
)
self.listWidget.selectionModel().select(selection, QtCore.QItemSelectionModel.SelectionFlag.ClearAndSelect)
return selection_ranges
def move_current_up(self):
def moveCurrentUp(self):
row = self.listWidget.currentRow()
selection = self.get_new_indexes(-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.set_selection(selection)
self.listOrderChanged.emit()
self.emit_front_cover_change()
self.modified.emit()
self.listOrderChanged.emit()
self.emitFrontCoverChange()
self.modified.emit()
def move_current_down(self):
def moveCurrentDown(self):
row = self.listWidget.currentRow()
selection = self.get_new_indexes(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.emit_front_cover_change()
self.set_selection(selection)
self.modified.emit()
self.listOrderChanged.emit()
self.emitFrontCoverChange()
self.modified.emit()
def item_move_event(self, s):
def itemMoveEvent(self, s):
# print "move event: ", s, self.listWidget.currentRow()
if s == "start":
self.pre_move_row = self.listWidget.currentRow()
if s == "finish":
if self.pre_move_row != self.listWidget.currentRow():
self.listOrderChanged.emit()
self.emit_front_cover_change()
self.emitFrontCoverChange()
self.modified.emit()
def change_page_type(self, i):
def changePageType(self, i):
new_type = self.comboBox.itemData(i)
if self.get_current_page_type() != new_type:
self.set_current_page_type(new_type)
self.emit_front_cover_change()
if self.getCurrentPageType() != new_type:
self.setCurrentPageType(new_type)
self.emitFrontCoverChange()
self.modified.emit()
def change_page(self):
def changePage(self):
row = self.listWidget.currentRow()
pagetype = self.get_current_page_type()
pagetype = self.getCurrentPageType()
i = self.comboBox.findData(pagetype)
self.comboBox.setCurrentIndex(i)
if "Bookmark" in self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]:
self.leBookmark.setText(self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]["Bookmark"])
else:
self.leBookmark.setText("")
idx = int(self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole)[0]["Image"])
#idx = int(str (self.listWidget.item(row).text()))
idx = int(self.listWidget.item(row).data(
Qt.UserRole)[0]['Image'])
if self.comic_archive is not None:
self.pageWidget.set_archive(self.comic_archive, idx)
self.pageWidget.setArchive(self.comic_archive, idx)
def get_first_front_cover(self):
front_cover = 0
def getFirstFrontCover(self):
frontCover = 0
for i in range(self.listWidget.count()):
item = self.listWidget.item(i)
page_dict = item.data(QtCore.Qt.ItemDataRole.UserRole)[0] # .toPyObject()[0]
if "Type" in page_dict and page_dict["Type"] == PageType.FrontCover:
front_cover = int(page_dict["Image"])
page_dict = item.data(Qt.UserRole)[0] #.toPyObject()[0]
if 'Type' in page_dict and page_dict[
'Type'] == PageType.FrontCover:
frontCover = int(page_dict['Image'])
break
return front_cover
return frontCover
def get_current_page_type(self):
def getCurrentPageType(self):
row = self.listWidget.currentRow()
page_dict = self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole)[0] # .toPyObject()[0]
if "Type" in page_dict:
return page_dict["Type"]
page_dict = self.listWidget.item(row).data(Qt.UserRole)[0] #.toPyObject()[0]
if 'Type' in page_dict:
return page_dict['Type']
else:
return ""
return ""
def set_current_page_type(self, t):
def setCurrentPageType(self, t):
row = self.listWidget.currentRow()
page_dict = self.listWidget.item(row).data(QtCore.Qt.ItemDataRole.UserRole)[0] # .toPyObject()[0]
page_dict = self.listWidget.item(row).data(Qt.UserRole)[0] #.toPyObject()[0]
if t == "":
if "Type" in page_dict:
del page_dict["Type"]
if 'Type' in page_dict:
del(page_dict['Type'])
else:
page_dict["Type"] = str(t)
item = self.listWidget.item(row)
# wrap the dict in a tuple to keep from being converted to QtWidgets.QStrings
item.setData(QtCore.Qt.ItemDataRole.UserRole, (page_dict,))
item.setText(self.list_entry_text(page_dict))
def save_bookmark(self):
row = self.listWidget.currentRow()
page_dict = self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]
current_bookmark = ""
if "Bookmark" in page_dict:
current_bookmark = page_dict["Bookmark"]
if self.leBookmark.text().strip():
new_bookmark = str(self.leBookmark.text().strip())
if current_bookmark != new_bookmark:
page_dict["Bookmark"] = new_bookmark
self.modified.emit()
elif current_bookmark != "":
del page_dict["Bookmark"]
self.modified.emit()
page_dict['Type'] = str(t)
item = self.listWidget.item(row)
# wrap the dict in a tuple to keep from being converted to QStrings
item.setData(QtCore.Qt.UserRole, (page_dict,))
item.setText(self.list_entry_text(page_dict))
item.setData(Qt.UserRole, (page_dict,))
item.setText(self.listEntryText(page_dict))
self.listWidget.setFocus()
def set_data(self, comic_archive: ComicArchive, pages_list: list):
def setData(self, comic_archive, pages_list):
self.comic_archive = comic_archive
self.pages_list = pages_list
if pages_list is not None and len(pages_list) > 0:
self.comboBox.setDisabled(False)
self.leBookmark.setDisabled(False)
self.listWidget.itemSelectionChanged.disconnect(self.change_page)
self.listWidget.itemSelectionChanged.disconnect(self.changePage)
self.listWidget.clear()
for p in pages_list:
item = QtWidgets.QListWidgetItem(self.list_entry_text(p))
# wrap the dict in a tuple to keep from being converted to QtWidgets.QStrings
item.setData(QtCore.Qt.ItemDataRole.UserRole, (p,))
item = QListWidgetItem(self.listEntryText(p))
# wrap the dict in a tuple to keep from being converted to QStrings
item.setData(Qt.UserRole, (p,))
self.listWidget.addItem(item)
self.first_front_page = self.get_first_front_cover()
self.listWidget.itemSelectionChanged.connect(self.change_page)
self.first_front_page = self.getFirstFrontCover()
self.listWidget.itemSelectionChanged.connect(self.changePage)
self.listWidget.setCurrentRow(0)
def list_entry_text(self, page_dict):
text = str(int(page_dict["Image"]) + 1)
if "Type" in page_dict:
if page_dict["Type"] in self.pageTypeNames.keys():
text += " (" + self.pageTypeNames[page_dict["Type"]] + ")"
else:
text += " (Error: " + page_dict["Type"] + ")"
if "Bookmark" in page_dict:
text += " " + "\U0001F516"
def listEntryText(self, page_dict):
text = str(int(page_dict['Image']) + 1)
if 'Type' in page_dict:
text += " (" + self.pageTypeNames[page_dict['Type']] + ")"
return text
def get_page_list(self):
def getPageList(self):
page_list = []
for i in range(self.listWidget.count()):
item = self.listWidget.item(i)
page_list.append(item.data(QtCore.Qt.ItemDataRole.UserRole)[0]) # .toPyObject()[0]
page_list.append(item.data(Qt.UserRole)[0]) #.toPyObject()[0]
return page_list
def emit_front_cover_change(self):
if self.first_front_page != self.get_first_front_cover():
self.first_front_page = self.get_first_front_cover()
def emitFrontCoverChange(self):
if self.first_front_page != self.getFirstFrontCover():
self.first_front_page = self.getFirstFrontCover()
self.firstFrontCoverChanged.emit(self.first_front_page)
def set_metadata_style(self, data_style):
def setMetadataStyle(self, data_style):
# depending on the current data style, certain fields are disabled
inactive_color = QtGui.QColor(255, 170, 150)
inactive_color = QColor(255, 170, 150)
active_palette = self.comboBox.palette()
inactive_palette3 = self.comboBox.palette()
inactive_palette3.setColor(QtGui.QPalette.ColorRole.Base, inactive_color)
inactive_palette3.setColor(QPalette.Base, inactive_color)
if data_style == MetaDataStyle.CIX:
self.btnUp.setEnabled(True)
self.btnDown.setEnabled(True)
self.comboBox.setEnabled(True)
self.leBookmark.setEnabled(True)
self.listWidget.setEnabled(True)
self.leBookmark.setPalette(active_palette)
self.listWidget.setPalette(active_palette)
elif data_style == MetaDataStyle.CBI:
self.btnUp.setEnabled(False)
self.btnDown.setEnabled(False)
self.comboBox.setEnabled(False)
self.leBookmark.setEnabled(False)
self.listWidget.setEnabled(False)
self.leBookmark.setPalette(inactive_palette3)
self.listWidget.setPalette(inactive_palette3)
elif data_style == MetaDataStyle.COMET:
elif data_style == MetaDataStyle.CoMet:
pass
# make sure combo is disabled when no list
if self.comic_archive is None:
self.comboBox.setEnabled(False)
self.leBookmark.setEnabled(False)

View File

@ -14,16 +14,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from PyQt5 import QtCore, QtGui, uic
from PyQt5.QtCore import pyqtSignal
from PyQt5 import QtCore
from comicapi.comicarchive import ComicArchive
logger = logging.getLogger(__name__)
from comictaggerlib.ui.qtutils import getQImageFromData
#from comicarchive import ComicArchive
#import utils
class PageLoader(QtCore.QThread):
"""
This class holds onto a reference of each instance in a list since
problems occur if the ref count goes to zero and the GC tries to reap
@ -32,36 +32,39 @@ class PageLoader(QtCore.QThread):
"abandoned", and no signals will be issued.
"""
loadComplete = QtCore.pyqtSignal(bytes)
loadComplete = pyqtSignal(QtGui.QImage)
instanceList = []
mutex = QtCore.QMutex()
# Remove all finished threads from the list
@staticmethod
def reap_instances():
def reapInstances():
for obj in reversed(PageLoader.instanceList):
if obj.isFinished():
PageLoader.instanceList.remove(obj)
def __init__(self, ca: ComicArchive, page_num):
def __init__(self, ca, page_num):
QtCore.QThread.__init__(self)
self.ca: ComicArchive = ca
self.page_num: int = page_num
self.ca = ca
self.page_num = page_num
self.abandoned = False
# remove any old instances, and then add ourself
PageLoader.mutex.lock()
PageLoader.reap_instances()
PageLoader.reapInstances()
PageLoader.instanceList.append(self)
PageLoader.mutex.unlock()
def run(self):
image_data = self.ca.get_page(self.page_num)
image_data = self.ca.getPage(self.page_num)
if self.abandoned:
return
if image_data is not None:
img = getQImageFromData(image_data)
if self.abandoned:
return
self.loadComplete.emit(image_data)
self.loadComplete.emit(img)

View File

@ -1,4 +1,4 @@
"""A PyQt5 dialog to show ID log and progress"""
"""A PyQT5 dialog to show ID log and progress"""
# Copyright 2012-2014 Anthony Beville
@ -14,29 +14,25 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#import sys
#import os
import logging
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5 import QtCore, QtWidgets, uic
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.ui.qtutils import reduce_widget_font_size
logger = logging.getLogger(__name__)
from comictaggerlib.ui.qtutils import reduceWidgetFontSize
from .settings import ComicTaggerSettings
#import utils
class IDProgressWindow(QtWidgets.QDialog):
def __init__(self, parent):
super().__init__(parent)
super(IDProgressWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.get_ui_file("progresswindow.ui"), self)
uic.loadUi(ComicTaggerSettings.getUIFile('progresswindow.ui'), self)
self.setWindowFlags(
QtCore.Qt.WindowType(
self.windowFlags()
| QtCore.Qt.WindowType.WindowSystemMenuHint
| QtCore.Qt.WindowType.WindowMaximizeButtonHint
)
)
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
reduce_widget_font_size(self.textEdit)
reduceWidgetFontSize(self.textEdit)

View File

@ -14,54 +14,50 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import os
from typing import List
from PyQt5 import QtCore, QtWidgets, uic
from PyQt5 import QtCore, QtGui, QtWidgets, uic
import comicapi.comicarchive
from comicapi import utils
from comicapi.comicarchive import MetaDataStyle
from comictaggerlib.filerenamer import FileRenamer
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.settingswindow import SettingsWindow
from comictaggerlib.ui.qtutils import center_window_on_parent
logger = logging.getLogger(__name__)
from .settings import ComicTaggerSettings
from .settingswindow import SettingsWindow
from .filerenamer import FileRenamer
from .comicarchive import MetaDataStyle
from comictaggerlib.ui.qtutils import centerWindowOnParent
from . import utils
class RenameWindow(QtWidgets.QDialog):
def __init__(self, parent, comic_archive_list: List[comicapi.comicarchive.ComicArchive], data_style, settings):
super().__init__(parent)
uic.loadUi(ComicTaggerSettings.get_ui_file("renamewindow.ui"), self)
self.label.setText(f"Preview (based on {MetaDataStyle.name[data_style]} tags):")
def __init__(self, parent, comic_archive_list, data_style, settings):
super(RenameWindow, self).__init__(parent)
self.setWindowFlags(
QtCore.Qt.WindowType(
self.windowFlags()
| QtCore.Qt.WindowType.WindowSystemMenuHint
| QtCore.Qt.WindowType.WindowMaximizeButtonHint
)
)
uic.loadUi(ComicTaggerSettings.getUIFile('renamewindow.ui'), self)
self.label.setText(
"Preview (based on {0} tags):".format(
MetaDataStyle.name[data_style]))
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
self.settings = settings
self.comic_archive_list = comic_archive_list
self.data_style = data_style
self.rename_list = []
self.btnSettings.clicked.connect(self.modify_settings)
self.btnSettings.clicked.connect(self.modifySettings)
self.configRenamer()
self.doPreview()
def configRenamer(self):
self.renamer = FileRenamer(None)
self.config_renamer()
self.do_preview()
self.renamer.setTemplate(self.settings.rename_template)
self.renamer.setIssueZeroPadding(
self.settings.rename_issue_number_padding)
self.renamer.setSmartCleanup(
self.settings.rename_use_smart_string_cleanup)
def config_renamer(self):
self.renamer.set_template(self.settings.rename_template)
self.renamer.set_issue_zero_padding(self.settings.rename_issue_number_padding)
self.renamer.set_smart_cleanup(self.settings.rename_use_smart_string_cleanup)
def do_preview(self):
def doPreview(self):
self.rename_list = []
while self.twList.rowCount() > 0:
self.twList.removeRow(0)
@ -71,18 +67,29 @@ class RenameWindow(QtWidgets.QDialog):
new_ext = None # default
if self.settings.rename_extension_based_on_archive:
if ca.is_sevenzip():
new_ext = ".cb7"
elif ca.is_zip():
if ca.isZip():
new_ext = ".cbz"
elif ca.is_rar():
elif ca.isRar():
new_ext = ".cbr"
md = ca.read_metadata(self.data_style)
if md.is_empty:
md = ca.metadata_from_filename(self.settings.parse_scan_info)
self.renamer.set_metadata(md)
new_name = self.renamer.determine_name(ca.path, ext=new_ext)
md = ca.readMetadata(self.data_style)
if md.isEmpty:
md = ca.metadataFromFilename(self.settings.parse_scan_info)
self.renamer.setMetadata(md)
self.renamer.move = self.settings.rename_move_dir
try:
new_name = self.renamer.determineName(ca.path, ext=new_ext)
except Exception as e:
QtWidgets.QMessageBox.critical(self, 'Invalid format string!',
'Your rename template is invalid!'
'<br/><br/>{}<br/><br/>'
'Please consult the template help in the '
'settings and the documentation on the format at '
'<a href=\'https://docs.python.org/3/library/string.html#format-string-syntax\'>'
'https://docs.python.org/3/library/string.html#format-string-syntax</a>'.format(e))
return
row = self.twList.rowCount()
self.twList.insertRow(row)
@ -91,25 +98,28 @@ class RenameWindow(QtWidgets.QDialog):
new_name_item = QtWidgets.QTableWidgetItem()
item_text = os.path.split(ca.path)[0]
folder_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
folder_item.setFlags(
QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 0, folder_item)
folder_item.setText(item_text)
folder_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
folder_item.setData(QtCore.Qt.ToolTipRole, item_text)
item_text = os.path.split(ca.path)[1]
old_name_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
old_name_item.setFlags(
QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 1, old_name_item)
old_name_item.setText(item_text)
old_name_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
old_name_item.setData(QtCore.Qt.ToolTipRole, item_text)
new_name_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
new_name_item.setFlags(
QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 2, new_name_item)
new_name_item.setText(new_name)
new_name_item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, new_name)
new_name_item.setData(QtCore.Qt.ToolTipRole, new_name)
dict_item = {}
dict_item["archive"] = ca
dict_item["new_name"] = new_name
dict_item = dict()
dict_item['archive'] = ca
dict_item['new_name'] = new_name
self.rename_list.append(dict_item)
# Adjust column sizes
@ -121,51 +131,56 @@ class RenameWindow(QtWidgets.QDialog):
self.twList.setSortingEnabled(True)
def modify_settings(self):
def modifySettings(self):
settingswin = SettingsWindow(self, self.settings)
settingswin.setModal(True)
settingswin.show_rename_tab()
settingswin.exec()
settingswin.showRenameTab()
settingswin.exec_()
if settingswin.result():
self.config_renamer()
self.do_preview()
self.configRenamer()
self.doPreview()
def accept(self):
prog_dialog = QtWidgets.QProgressDialog("", "Cancel", 0, len(self.rename_list), self)
prog_dialog.setWindowTitle("Renaming Archives")
prog_dialog.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
prog_dialog.setMinimumDuration(100)
center_window_on_parent(prog_dialog)
progdialog = QtWidgets.QProgressDialog(
"", "Cancel", 0, len(self.rename_list), self)
progdialog.setWindowTitle("Renaming Archives")
progdialog.setWindowModality(QtCore.Qt.WindowModal)
progdialog.setMinimumDuration(100)
centerWindowOnParent(progdialog)
#progdialog.show()
QtCore.QCoreApplication.processEvents()
for idx, item in enumerate(self.rename_list):
QtCore.QCoreApplication.processEvents()
if prog_dialog.wasCanceled():
if progdialog.wasCanceled():
break
idx += 1
prog_dialog.setValue(idx)
prog_dialog.setLabelText(item["new_name"])
center_window_on_parent(prog_dialog)
progdialog.setValue(idx)
progdialog.setLabelText(item['new_name'])
centerWindowOnParent(progdialog)
QtCore.QCoreApplication.processEvents()
if item["new_name"] == os.path.basename(item["archive"].path):
print(item["new_name"], "Filename is already good!")
logger.info(item["new_name"], "Filename is already good!")
folder = os.path.dirname(os.path.abspath(item['archive'].path))
if self.settings.rename_move_dir and len(self.settings.rename_dir.strip()) > 3:
folder = self.settings.rename_dir.strip()
new_abs_path = utils.unique_file(os.path.join(folder, item['new_name']))
if os.path.join(folder, item['new_name']) == item['archive'].path:
print(item['new_name'], "Filename is already good!")
continue
if not item["archive"].is_writable(check_rar_status=False):
if not item['archive'].isWritable(check_rar_status=False):
continue
folder = os.path.dirname(os.path.abspath(item["archive"].path))
new_abs_path = utils.unique_file(os.path.join(folder, item["new_name"]))
os.makedirs(os.path.dirname(new_abs_path), 0o777, True)
os.rename(item['archive'].path, new_abs_path)
os.rename(item["archive"].path, new_abs_path)
item['archive'].rename(new_abs_path)
item["archive"].rename(new_abs_path)
prog_dialog.hide()
progdialog.hide()
QtCore.QCoreApplication.processEvents()
QtWidgets.QDialog.accept(self)

View File

@ -1,37 +0,0 @@
from typing import List, TypedDict
from comicapi.comicarchive import ComicArchive
class IssueResult(TypedDict):
series: str
distance: int
issue_number: str
cv_issue_count: int
url_image_hash: str
issue_title: str
issue_id: str # int?
volume_id: str # int?
month: int
year: int
publisher: str
image_url: str
thumb_url: str
page_url: str
description: str
class OnlineMatchResults:
def __init__(self):
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]):
self.ca: ComicArchive = ca
self.matches = match_list

View File

@ -14,46 +14,48 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import configparser
import logging
import os
import pathlib
import platform
import sys
import configparser
import platform
import codecs
import uuid
from comicapi import utils
logger = logging.getLogger(__name__)
from . import utils
class ComicTaggerSettings:
@staticmethod
def get_settings_folder():
def getSettingsFolder():
filename_encoding = sys.getfilesystemencoding()
if platform.system() == "Windows":
folder = os.path.join(os.environ["APPDATA"], "ComicTagger")
folder = os.path.join(os.environ['APPDATA'], 'ComicTagger')
else:
folder = os.path.join(os.path.expanduser("~"), ".ComicTagger")
return pathlib.Path(folder)
folder = os.path.join(os.path.expanduser('~'), '.ComicTagger')
if folder is not None:
folder = folder
return folder
@staticmethod
def base_dir():
if getattr(sys, "frozen", None):
def baseDir():
if getattr(sys, 'frozen', None):
return sys._MEIPASS
return pathlib.Path(__file__).parent
else:
return os.path.dirname(os.path.abspath(__file__))
@staticmethod
def get_graphic(filename):
graphic_folder = pathlib.Path(os.path.join(ComicTaggerSettings.base_dir(), "graphics"))
def getGraphic(filename):
graphic_folder = os.path.join(
ComicTaggerSettings.baseDir(), 'graphics')
return os.path.join(graphic_folder, filename)
@staticmethod
def get_ui_file(filename):
ui_folder = os.path.join(ComicTaggerSettings.base_dir(), "ui")
def getUIFile(filename):
ui_folder = os.path.join(ComicTaggerSettings.baseDir(), 'ui')
return os.path.join(ui_folder, filename)
def set_default_values(self):
def setDefaultValues(self):
# General Settings
self.rar_exe_path = ""
self.allow_cbi_in_rar = True
@ -76,7 +78,7 @@ class ComicTaggerSettings:
# identifier settings
self.id_length_delta_thresh = 5
self.id_publisher_filter = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa, Dino Comics"
self.id_publisher_blacklist = "Panini Comics, Abril, Planeta DeAgostini, Editorial Televisa, Dino Comics"
# Show/ask dialog flags
self.ask_about_cbi_in_rar = True
@ -93,10 +95,6 @@ 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
@ -110,10 +108,12 @@ class ComicTaggerSettings:
self.apply_cbl_transform_on_bulk_operation = False
# Rename settings
self.rename_template = "%series% #%issue% (%year%)"
self.rename_template = "{publisher}/{series}/{series} #{issue} - {title} ({year})"
self.rename_issue_number_padding = 3
self.rename_use_smart_string_cleanup = True
self.rename_extension_based_on_archive = True
self.rename_dir = ""
self.rename_move_dir = False
# Auto-tag stickies
self.save_on_low_confidence = False
@ -127,77 +127,10 @@ class ComicTaggerSettings:
self.settings_file = ""
self.folder = ""
# General Settings
self.rar_exe_path = ""
self.allow_cbi_in_rar = True
self.check_for_new_version = False
self.send_usage_stats = False
# automatic settings
self.install_id = uuid.uuid4().hex
self.last_selected_save_data_style = 0
self.last_selected_load_data_style = 0
self.last_opened_folder = ""
self.last_main_window_width = 0
self.last_main_window_height = 0
self.last_main_window_x = 0
self.last_main_window_y = 0
self.last_form_side_width = -1
self.last_list_side_width = -1
self.last_filelist_sorted_column = -1
self.last_filelist_sorted_order = 0
# identifier settings
self.id_length_delta_thresh = 5
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
self.dont_notify_about_this_version = ""
self.ask_about_usage_stats = True
# filename parsing settings
self.parse_scan_info = True
# Comic Vine settings
self.use_series_start_as_volume = False
self.clear_form_before_populating_from_cv = False
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
self.copy_characters_to_tags = False
self.copy_teams_to_tags = False
self.copy_locations_to_tags = False
self.copy_storyarcs_to_tags = False
self.copy_notes_to_comments = False
self.copy_weblink_to_comments = False
self.apply_cbl_transform_on_cv_import = False
self.apply_cbl_transform_on_bulk_operation = False
# Rename settings
self.rename_template = "%series% #%issue% (%year%)"
self.rename_issue_number_padding = 3
self.rename_use_smart_string_cleanup = True
self.rename_extension_based_on_archive = True
# Auto-tag stickies
self.save_on_low_confidence = False
self.dont_use_year_when_identifying = False
self.assume_1_if_no_issue_num = False
self.ignore_leading_numbers_in_filename = False
self.remove_archive_after_successful_match = False
self.wait_and_retry_on_rate_limit = False
self.setDefaultValues()
self.config = configparser.RawConfigParser()
self.folder = ComicTaggerSettings.get_settings_folder()
self.folder = ComicTaggerSettings.getSettingsFolder()
if not os.path.exists(self.folder):
os.makedirs(self.folder)
@ -214,10 +147,10 @@ class ComicTaggerSettings:
if self.rar_exe_path == "":
if platform.system() == "Windows":
# look in some likely places for Windows machines
if os.path.exists(r"C:\Program Files\WinRAR\Rar.exe"):
self.rar_exe_path = r"C:\Program Files\WinRAR\Rar.exe"
elif os.path.exists(r"C:\Program Files (x86)\WinRAR\Rar.exe"):
self.rar_exe_path = r"C:\Program Files (x86)\WinRAR\Rar.exe"
if os.path.exists("C:\Program Files\WinRAR\Rar.exe"):
self.rar_exe_path = "C:\Program Files\WinRAR\Rar.exe"
elif os.path.exists("C:\Program Files (x86)\WinRAR\Rar.exe"):
self.rar_exe_path = "C:\Program Files (x86)\WinRAR\Rar.exe"
else:
# see if it's in the path of unix user
if utils.which("rar") is not None:
@ -225,231 +158,343 @@ class ComicTaggerSettings:
if self.rar_exe_path != "":
self.save()
if self.rar_exe_path != "":
# make sure rar program is now in the path for the rar class
utils.add_to_path(os.path.dirname(self.rar_exe_path))
# make sure rar program is now in the path for the rar class
utils.addtopath(os.path.dirname(self.rar_exe_path))
def reset(self):
os.unlink(self.settings_file)
self.__init__()
def load(self):
def readline_generator(f):
line = f.readline()
while line:
yield line
line = f.readline()
with open(self.settings_file, "r") as f:
self.config.read_file(readline_generator(f))
#self.config.readfp(codecs.open(self.settings_file, "r", "utf8"))
self.config.read_file(
readline_generator(codecs.open(self.settings_file, "r", "utf8")))
self.rar_exe_path = self.config.get("settings", "rar_exe_path")
if self.config.has_option("settings", "check_for_new_version"):
self.check_for_new_version = self.config.getboolean("settings", "check_for_new_version")
if self.config.has_option("settings", "send_usage_stats"):
self.send_usage_stats = self.config.getboolean("settings", "send_usage_stats")
self.rar_exe_path = self.config.get('settings', 'rar_exe_path')
if self.config.has_option('settings', 'check_for_new_version'):
self.check_for_new_version = self.config.getboolean(
'settings', 'check_for_new_version')
if self.config.has_option('settings', 'send_usage_stats'):
self.send_usage_stats = self.config.getboolean(
'settings', 'send_usage_stats')
if self.config.has_option("auto", "install_id"):
self.install_id = self.config.get("auto", "install_id")
if self.config.has_option("auto", "last_selected_load_data_style"):
self.last_selected_load_data_style = self.config.getint("auto", "last_selected_load_data_style")
if self.config.has_option("auto", "last_selected_save_data_style"):
self.last_selected_save_data_style = self.config.getint("auto", "last_selected_save_data_style")
if self.config.has_option("auto", "last_opened_folder"):
self.last_opened_folder = self.config.get("auto", "last_opened_folder")
if self.config.has_option("auto", "last_main_window_width"):
self.last_main_window_width = self.config.getint("auto", "last_main_window_width")
if self.config.has_option("auto", "last_main_window_height"):
self.last_main_window_height = self.config.getint("auto", "last_main_window_height")
if self.config.has_option("auto", "last_main_window_x"):
self.last_main_window_x = self.config.getint("auto", "last_main_window_x")
if self.config.has_option("auto", "last_main_window_y"):
self.last_main_window_y = self.config.getint("auto", "last_main_window_y")
if self.config.has_option("auto", "last_form_side_width"):
self.last_form_side_width = self.config.getint("auto", "last_form_side_width")
if self.config.has_option("auto", "last_list_side_width"):
self.last_list_side_width = self.config.getint("auto", "last_list_side_width")
if self.config.has_option("auto", "last_filelist_sorted_column"):
self.last_filelist_sorted_column = self.config.getint("auto", "last_filelist_sorted_column")
if self.config.has_option("auto", "last_filelist_sorted_order"):
self.last_filelist_sorted_order = self.config.getint("auto", "last_filelist_sorted_order")
if self.config.has_option('auto', 'install_id'):
self.install_id = self.config.get('auto', 'install_id')
if self.config.has_option('auto', 'last_selected_load_data_style'):
self.last_selected_load_data_style = self.config.getint(
'auto', 'last_selected_load_data_style')
if self.config.has_option('auto', 'last_selected_save_data_style'):
self.last_selected_save_data_style = self.config.getint(
'auto', 'last_selected_save_data_style')
if self.config.has_option('auto', 'last_opened_folder'):
self.last_opened_folder = self.config.get(
'auto', 'last_opened_folder')
if self.config.has_option('auto', 'last_main_window_width'):
self.last_main_window_width = self.config.getint(
'auto', 'last_main_window_width')
if self.config.has_option('auto', 'last_main_window_height'):
self.last_main_window_height = self.config.getint(
'auto', 'last_main_window_height')
if self.config.has_option('auto', 'last_main_window_x'):
self.last_main_window_x = self.config.getint(
'auto', 'last_main_window_x')
if self.config.has_option('auto', 'last_main_window_y'):
self.last_main_window_y = self.config.getint(
'auto', 'last_main_window_y')
if self.config.has_option('auto', 'last_form_side_width'):
self.last_form_side_width = self.config.getint(
'auto', 'last_form_side_width')
if self.config.has_option('auto', 'last_list_side_width'):
self.last_list_side_width = self.config.getint(
'auto', 'last_list_side_width')
if self.config.has_option('auto', 'last_filelist_sorted_column'):
self.last_filelist_sorted_column = self.config.getint(
'auto', 'last_filelist_sorted_column')
if self.config.has_option('auto', 'last_filelist_sorted_order'):
self.last_filelist_sorted_order = self.config.getint(
'auto', 'last_filelist_sorted_order')
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_filter"):
self.id_publisher_filter = self.config.get("identifier", "id_publisher_filter")
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("filenameparser", "parse_scan_info"):
self.parse_scan_info = self.config.getboolean("filenameparser", "parse_scan_info")
if self.config.has_option('filenameparser', 'parse_scan_info'):
self.parse_scan_info = self.config.getboolean(
'filenameparser', 'parse_scan_info')
if self.config.has_option("dialogflags", "ask_about_cbi_in_rar"):
self.ask_about_cbi_in_rar = self.config.getboolean("dialogflags", "ask_about_cbi_in_rar")
if self.config.has_option("dialogflags", "show_disclaimer"):
self.show_disclaimer = self.config.getboolean("dialogflags", "show_disclaimer")
if self.config.has_option("dialogflags", "dont_notify_about_this_version"):
self.dont_notify_about_this_version = self.config.get("dialogflags", "dont_notify_about_this_version")
if self.config.has_option("dialogflags", "ask_about_usage_stats"):
self.ask_about_usage_stats = self.config.getboolean("dialogflags", "ask_about_usage_stats")
if self.config.has_option('dialogflags', 'ask_about_cbi_in_rar'):
self.ask_about_cbi_in_rar = self.config.getboolean(
'dialogflags', 'ask_about_cbi_in_rar')
if self.config.has_option('dialogflags', 'show_disclaimer'):
self.show_disclaimer = self.config.getboolean(
'dialogflags', 'show_disclaimer')
if self.config.has_option(
'dialogflags', 'dont_notify_about_this_version'):
self.dont_notify_about_this_version = self.config.get(
'dialogflags', 'dont_notify_about_this_version')
if self.config.has_option('dialogflags', 'ask_about_usage_stats'):
self.ask_about_usage_stats = self.config.getboolean(
'dialogflags', 'ask_about_usage_stats')
if self.config.has_option("comicvine", "use_series_start_as_volume"):
self.use_series_start_as_volume = self.config.getboolean("comicvine", "use_series_start_as_volume")
if self.config.has_option("comicvine", "clear_form_before_populating_from_cv"):
if self.config.has_option('comicvine', 'use_series_start_as_volume'):
self.use_series_start_as_volume = self.config.getboolean(
'comicvine', 'use_series_start_as_volume')
if self.config.has_option(
'comicvine', 'clear_form_before_populating_from_cv'):
self.clear_form_before_populating_from_cv = self.config.getboolean(
"comicvine", "clear_form_before_populating_from_cv"
)
if self.config.has_option("comicvine", "remove_html_tables"):
self.remove_html_tables = self.config.getboolean("comicvine", "remove_html_tables")
'comicvine', 'clear_form_before_populating_from_cv')
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', 'cv_api_key'):
self.cv_api_key = self.config.get('comicvine', 'cv_api_key')
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")
if self.config.has_option("cbl_transform", "assume_lone_credit_is_primary"):
if self.config.has_option(
'cbl_transform', 'assume_lone_credit_is_primary'):
self.assume_lone_credit_is_primary = self.config.getboolean(
"cbl_transform", "assume_lone_credit_is_primary"
)
if self.config.has_option("cbl_transform", "copy_characters_to_tags"):
self.copy_characters_to_tags = self.config.getboolean("cbl_transform", "copy_characters_to_tags")
if self.config.has_option("cbl_transform", "copy_teams_to_tags"):
self.copy_teams_to_tags = self.config.getboolean("cbl_transform", "copy_teams_to_tags")
if self.config.has_option("cbl_transform", "copy_locations_to_tags"):
self.copy_locations_to_tags = self.config.getboolean("cbl_transform", "copy_locations_to_tags")
if self.config.has_option("cbl_transform", "copy_notes_to_comments"):
self.copy_notes_to_comments = self.config.getboolean("cbl_transform", "copy_notes_to_comments")
if self.config.has_option("cbl_transform", "copy_storyarcs_to_tags"):
self.copy_storyarcs_to_tags = self.config.getboolean("cbl_transform", "copy_storyarcs_to_tags")
if self.config.has_option("cbl_transform", "copy_weblink_to_comments"):
self.copy_weblink_to_comments = self.config.getboolean("cbl_transform", "copy_weblink_to_comments")
if self.config.has_option("cbl_transform", "apply_cbl_transform_on_cv_import"):
'cbl_transform', 'assume_lone_credit_is_primary')
if self.config.has_option('cbl_transform', 'copy_characters_to_tags'):
self.copy_characters_to_tags = self.config.getboolean(
'cbl_transform', 'copy_characters_to_tags')
if self.config.has_option('cbl_transform', 'copy_teams_to_tags'):
self.copy_teams_to_tags = self.config.getboolean(
'cbl_transform', 'copy_teams_to_tags')
if self.config.has_option('cbl_transform', 'copy_locations_to_tags'):
self.copy_locations_to_tags = self.config.getboolean(
'cbl_transform', 'copy_locations_to_tags')
if self.config.has_option('cbl_transform', 'copy_notes_to_comments'):
self.copy_notes_to_comments = self.config.getboolean(
'cbl_transform', 'copy_notes_to_comments')
if self.config.has_option('cbl_transform', 'copy_storyarcs_to_tags'):
self.copy_storyarcs_to_tags = self.config.getboolean(
'cbl_transform', 'copy_storyarcs_to_tags')
if self.config.has_option('cbl_transform', 'copy_weblink_to_comments'):
self.copy_weblink_to_comments = self.config.getboolean(
'cbl_transform', 'copy_weblink_to_comments')
if self.config.has_option(
'cbl_transform', 'apply_cbl_transform_on_cv_import'):
self.apply_cbl_transform_on_cv_import = self.config.getboolean(
"cbl_transform", "apply_cbl_transform_on_cv_import"
)
if self.config.has_option("cbl_transform", "apply_cbl_transform_on_bulk_operation"):
'cbl_transform', 'apply_cbl_transform_on_cv_import')
if self.config.has_option(
'cbl_transform', 'apply_cbl_transform_on_bulk_operation'):
self.apply_cbl_transform_on_bulk_operation = self.config.getboolean(
"cbl_transform", "apply_cbl_transform_on_bulk_operation"
)
'cbl_transform',
'apply_cbl_transform_on_bulk_operation')
if self.config.has_option("rename", "rename_template"):
self.rename_template = self.config.get("rename", "rename_template")
if self.config.has_option("rename", "rename_issue_number_padding"):
self.rename_issue_number_padding = self.config.getint("rename", "rename_issue_number_padding")
if self.config.has_option("rename", "rename_use_smart_string_cleanup"):
self.rename_use_smart_string_cleanup = self.config.getboolean("rename", "rename_use_smart_string_cleanup")
if self.config.has_option("rename", "rename_extension_based_on_archive"):
if self.config.has_option('rename', 'rename_template'):
self.rename_template = self.config.get('rename', 'rename_template')
if self.config.has_option('rename', 'rename_issue_number_padding'):
self.rename_issue_number_padding = self.config.getint(
'rename', 'rename_issue_number_padding')
if self.config.has_option('rename', 'rename_use_smart_string_cleanup'):
self.rename_use_smart_string_cleanup = self.config.getboolean(
'rename', 'rename_use_smart_string_cleanup')
if self.config.has_option(
'rename', 'rename_extension_based_on_archive'):
self.rename_extension_based_on_archive = self.config.getboolean(
"rename", "rename_extension_based_on_archive"
)
'rename', 'rename_extension_based_on_archive')
if self.config.has_option('rename', 'rename_dir'):
self.rename_dir = self.config.get('rename', 'rename_dir')
if self.config.has_option('rename', 'rename_move_dir'):
self.rename_move_dir = self.config.getboolean('rename', 'rename_move_dir')
if self.config.has_option("autotag", "save_on_low_confidence"):
self.save_on_low_confidence = self.config.getboolean("autotag", "save_on_low_confidence")
if self.config.has_option("autotag", "dont_use_year_when_identifying"):
self.dont_use_year_when_identifying = self.config.getboolean("autotag", "dont_use_year_when_identifying")
if self.config.has_option("autotag", "assume_1_if_no_issue_num"):
self.assume_1_if_no_issue_num = self.config.getboolean("autotag", "assume_1_if_no_issue_num")
if self.config.has_option("autotag", "ignore_leading_numbers_in_filename"):
if self.config.has_option('autotag', 'save_on_low_confidence'):
self.save_on_low_confidence = self.config.getboolean(
'autotag', 'save_on_low_confidence')
if self.config.has_option('autotag', 'dont_use_year_when_identifying'):
self.dont_use_year_when_identifying = self.config.getboolean(
'autotag', 'dont_use_year_when_identifying')
if self.config.has_option('autotag', 'assume_1_if_no_issue_num'):
self.assume_1_if_no_issue_num = self.config.getboolean(
'autotag', 'assume_1_if_no_issue_num')
if self.config.has_option(
'autotag', 'ignore_leading_numbers_in_filename'):
self.ignore_leading_numbers_in_filename = self.config.getboolean(
"autotag", "ignore_leading_numbers_in_filename"
)
if self.config.has_option("autotag", "remove_archive_after_successful_match"):
'autotag', 'ignore_leading_numbers_in_filename')
if self.config.has_option(
'autotag', 'remove_archive_after_successful_match'):
self.remove_archive_after_successful_match = self.config.getboolean(
"autotag", "remove_archive_after_successful_match"
)
if self.config.has_option("autotag", "wait_and_retry_on_rate_limit"):
self.wait_and_retry_on_rate_limit = self.config.getboolean("autotag", "wait_and_retry_on_rate_limit")
'autotag',
'remove_archive_after_successful_match')
if self.config.has_option('autotag', 'wait_and_retry_on_rate_limit'):
self.wait_and_retry_on_rate_limit = self.config.getboolean(
'autotag', 'wait_and_retry_on_rate_limit')
def save(self):
if not self.config.has_section("settings"):
self.config.add_section("settings")
if not self.config.has_section('settings'):
self.config.add_section('settings')
self.config.set("settings", "check_for_new_version", self.check_for_new_version)
self.config.set("settings", "rar_exe_path", self.rar_exe_path)
self.config.set("settings", "send_usage_stats", self.send_usage_stats)
if not self.config.has_section("auto"):
self.config.add_section("auto")
self.config.set("auto", "install_id", self.install_id)
self.config.set("auto", "last_selected_load_data_style", self.last_selected_load_data_style)
self.config.set("auto", "last_selected_save_data_style", self.last_selected_save_data_style)
self.config.set("auto", "last_opened_folder", self.last_opened_folder)
self.config.set("auto", "last_main_window_width", self.last_main_window_width)
self.config.set("auto", "last_main_window_height", self.last_main_window_height)
self.config.set("auto", "last_main_window_x", self.last_main_window_x)
self.config.set("auto", "last_main_window_y", self.last_main_window_y)
self.config.set("auto", "last_form_side_width", self.last_form_side_width)
self.config.set("auto", "last_list_side_width", self.last_list_side_width)
self.config.set("auto", "last_filelist_sorted_column", self.last_filelist_sorted_column)
self.config.set("auto", "last_filelist_sorted_order", self.last_filelist_sorted_order)
if not self.config.has_section("identifier"):
self.config.add_section("identifier")
self.config.set("identifier", "id_length_delta_thresh", self.id_length_delta_thresh)
self.config.set("identifier", "id_publisher_filter", self.id_publisher_filter)
if not self.config.has_section("dialogflags"):
self.config.add_section("dialogflags")
self.config.set("dialogflags", "ask_about_cbi_in_rar", self.ask_about_cbi_in_rar)
self.config.set("dialogflags", "show_disclaimer", self.show_disclaimer)
self.config.set("dialogflags", "dont_notify_about_this_version", self.dont_notify_about_this_version)
self.config.set("dialogflags", "ask_about_usage_stats", self.ask_about_usage_stats)
if not self.config.has_section("filenameparser"):
self.config.add_section("filenameparser")
self.config.set("filenameparser", "parse_scan_info", self.parse_scan_info)
if not self.config.has_section("comicvine"):
self.config.add_section("comicvine")
self.config.set("comicvine", "use_series_start_as_volume", self.use_series_start_as_volume)
self.config.set("comicvine", "clear_form_before_populating_from_cv", 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"):
self.config.add_section("cbl_transform")
self.config.set("cbl_transform", "assume_lone_credit_is_primary", self.assume_lone_credit_is_primary)
self.config.set("cbl_transform", "copy_characters_to_tags", self.copy_characters_to_tags)
self.config.set("cbl_transform", "copy_teams_to_tags", self.copy_teams_to_tags)
self.config.set("cbl_transform", "copy_locations_to_tags", self.copy_locations_to_tags)
self.config.set("cbl_transform", "copy_storyarcs_to_tags", self.copy_storyarcs_to_tags)
self.config.set("cbl_transform", "copy_notes_to_comments", self.copy_notes_to_comments)
self.config.set("cbl_transform", "copy_weblink_to_comments", self.copy_weblink_to_comments)
self.config.set("cbl_transform", "apply_cbl_transform_on_cv_import", self.apply_cbl_transform_on_cv_import)
self.config.set(
"cbl_transform", "apply_cbl_transform_on_bulk_operation", self.apply_cbl_transform_on_bulk_operation
)
'settings', 'check_for_new_version', self.check_for_new_version)
self.config.set('settings', 'rar_exe_path', self.rar_exe_path)
self.config.set('settings', 'send_usage_stats', self.send_usage_stats)
if not self.config.has_section("rename"):
self.config.add_section("rename")
if not self.config.has_section('auto'):
self.config.add_section('auto')
self.config.set("rename", "rename_template", self.rename_template)
self.config.set("rename", "rename_issue_number_padding", self.rename_issue_number_padding)
self.config.set("rename", "rename_use_smart_string_cleanup", self.rename_use_smart_string_cleanup)
self.config.set("rename", "rename_extension_based_on_archive", self.rename_extension_based_on_archive)
self.config.set('auto', 'install_id', self.install_id)
self.config.set(
'auto',
'last_selected_load_data_style',
self.last_selected_load_data_style)
self.config.set(
'auto',
'last_selected_save_data_style',
self.last_selected_save_data_style)
self.config.set('auto', 'last_opened_folder', self.last_opened_folder)
self.config.set(
'auto', 'last_main_window_width', self.last_main_window_width)
self.config.set(
'auto', 'last_main_window_height', self.last_main_window_height)
self.config.set('auto', 'last_main_window_x', self.last_main_window_x)
self.config.set('auto', 'last_main_window_y', self.last_main_window_y)
self.config.set(
'auto', 'last_form_side_width', self.last_form_side_width)
self.config.set(
'auto', 'last_list_side_width', self.last_list_side_width)
self.config.set(
'auto',
'last_filelist_sorted_column',
self.last_filelist_sorted_column)
self.config.set(
'auto',
'last_filelist_sorted_order',
self.last_filelist_sorted_order)
if not self.config.has_section("autotag"):
self.config.add_section("autotag")
self.config.set("autotag", "save_on_low_confidence", self.save_on_low_confidence)
self.config.set("autotag", "dont_use_year_when_identifying", self.dont_use_year_when_identifying)
self.config.set("autotag", "assume_1_if_no_issue_num", self.assume_1_if_no_issue_num)
self.config.set("autotag", "ignore_leading_numbers_in_filename", self.ignore_leading_numbers_in_filename)
self.config.set("autotag", "remove_archive_after_successful_match", self.remove_archive_after_successful_match)
self.config.set("autotag", "wait_and_retry_on_rate_limit", self.wait_and_retry_on_rate_limit)
if not self.config.has_section('identifier'):
self.config.add_section('identifier')
with open(self.settings_file, "w") as configfile:
self.config.set(
'identifier',
'id_length_delta_thresh',
self.id_length_delta_thresh)
self.config.set(
'identifier',
'id_publisher_blacklist',
self.id_publisher_blacklist)
if not self.config.has_section('dialogflags'):
self.config.add_section('dialogflags')
self.config.set(
'dialogflags', 'ask_about_cbi_in_rar', self.ask_about_cbi_in_rar)
self.config.set('dialogflags', 'show_disclaimer', self.show_disclaimer)
self.config.set(
'dialogflags',
'dont_notify_about_this_version',
self.dont_notify_about_this_version)
self.config.set(
'dialogflags', 'ask_about_usage_stats', self.ask_about_usage_stats)
if not self.config.has_section('filenameparser'):
self.config.add_section('filenameparser')
self.config.set(
'filenameparser', 'parse_scan_info', self.parse_scan_info)
if not self.config.has_section('comicvine'):
self.config.add_section('comicvine')
self.config.set(
'comicvine',
'use_series_start_as_volume',
self.use_series_start_as_volume)
self.config.set('comicvine', 'clear_form_before_populating_from_cv',
self.clear_form_before_populating_from_cv)
self.config.set(
'comicvine', 'remove_html_tables', self.remove_html_tables)
self.config.set('comicvine', 'cv_api_key', self.cv_api_key)
if not self.config.has_section('cbl_transform'):
self.config.add_section('cbl_transform')
self.config.set(
'cbl_transform',
'assume_lone_credit_is_primary',
self.assume_lone_credit_is_primary)
self.config.set(
'cbl_transform',
'copy_characters_to_tags',
self.copy_characters_to_tags)
self.config.set(
'cbl_transform', 'copy_teams_to_tags', self.copy_teams_to_tags)
self.config.set(
'cbl_transform',
'copy_locations_to_tags',
self.copy_locations_to_tags)
self.config.set(
'cbl_transform',
'copy_storyarcs_to_tags',
self.copy_storyarcs_to_tags)
self.config.set(
'cbl_transform',
'copy_notes_to_comments',
self.copy_notes_to_comments)
self.config.set(
'cbl_transform',
'copy_weblink_to_comments',
self.copy_weblink_to_comments)
self.config.set(
'cbl_transform',
'apply_cbl_transform_on_cv_import',
self.apply_cbl_transform_on_cv_import)
self.config.set(
'cbl_transform',
'apply_cbl_transform_on_bulk_operation',
self.apply_cbl_transform_on_bulk_operation)
if not self.config.has_section('rename'):
self.config.add_section('rename')
self.config.set('rename', 'rename_template', self.rename_template)
self.config.set(
'rename',
'rename_issue_number_padding',
self.rename_issue_number_padding)
self.config.set(
'rename',
'rename_use_smart_string_cleanup',
self.rename_use_smart_string_cleanup)
self.config.set('rename', 'rename_extension_based_on_archive',
self.rename_extension_based_on_archive)
self.config.set('rename', 'rename_dir', self.rename_dir)
self.config.set('rename', 'rename_move_dir', self.rename_move_dir)
if not self.config.has_section('autotag'):
self.config.add_section('autotag')
self.config.set(
'autotag', 'save_on_low_confidence', self.save_on_low_confidence)
self.config.set(
'autotag',
'dont_use_year_when_identifying',
self.dont_use_year_when_identifying)
self.config.set(
'autotag',
'assume_1_if_no_issue_num',
self.assume_1_if_no_issue_num)
self.config.set('autotag', 'ignore_leading_numbers_in_filename',
self.ignore_leading_numbers_in_filename)
self.config.set('autotag', 'remove_archive_after_successful_match',
self.remove_archive_after_successful_match)
self.config.set(
'autotag',
'wait_and_retry_on_rate_limit',
self.wait_and_retry_on_rate_limit)
with codecs.open(self.settings_file, 'wb', 'utf8') as configfile:
self.config.write(configfile)
# make sure the basedir is cached, in case we're on Windows running a
# script from frozen binary
ComicTaggerSettings.baseDir()

View File

@ -14,19 +14,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import os
import platform
import os
import sys
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from comicapi import utils
from comictaggerlib.comicvinecacher import ComicVineCacher
from comictaggerlib.comicvinetalker import ComicVineTalker
from comictaggerlib.imagefetcher import ImageFetcher
from comictaggerlib.settings import ComicTaggerSettings
from .settings import ComicTaggerSettings
from .comicvinecacher import ComicVineCacher
from .comicvinetalker import ComicVineTalker
from .imagefetcher import ImageFetcher
from .filerenamer import FileRenamer
from .genericmetadata import GenericMetadata
from . import utils
logger = logging.getLogger(__name__)
windowsRarHelp = """
<html><head/><body><p>To write to CBR/RAR archives,
@ -45,29 +46,28 @@ linuxRarHelp = """
<a href="https://www.rarlab.com/download.htm">here</a></span>,
and install in your path. </p></body></html>
"""
macRarHelp = """
<html><head/><body><p>To write to CBR/RAR archives,
you will need the rar tool. The easiest way to get this is
to install <span style=" text-decoration: underline; color:#0000ff;">
<a href="https://brew.sh/">homebrew</a></span>.
</p>Once homebrew is installed, run: <b>brew install caskroom/cask/rar</b></body></html>
</p>Once homebrew is installed, run: <b>brew install caskroom/cask/rar</b></body></html>
"""
class SettingsWindow(QtWidgets.QDialog):
def __init__(self, parent, settings):
super().__init__(parent)
super(SettingsWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.get_ui_file("settingswindow.ui"), self)
uic.loadUi(ComicTaggerSettings.getUIFile('settingswindow.ui'), self)
self.setWindowFlags(
QtCore.Qt.WindowType(self.windowFlags() & ~QtCore.Qt.WindowType.WindowContextHelpButtonHint)
)
self.setWindowFlags(self.windowFlags() &
~QtCore.Qt.WindowContextHelpButtonHint)
self.settings = settings
self.name = "Settings"
self.name = "Settings"
if platform.system() == "Windows":
self.lblRarHelp.setText(windowsRarHelp)
@ -76,28 +76,32 @@ class SettingsWindow(QtWidgets.QDialog):
elif platform.system() == "Darwin":
self.leRarExePath.setReadOnly(False)
self.lblRarHelp.setText(macRarHelp)
self.name = "Preferences"
self.setWindowTitle("ComicTagger " + self.name)
self.lblDefaultSettings.setText("Revert to default " + self.name.lower())
self.lblDefaultSettings.setText(
"Revert to default " + self.name.lower())
self.btnResetSettings.setText("Default " + self.name)
nldt_tip = """<html>The <b>Default Name Length Match Tolerance</b> is for eliminating automatic
nldtTip = (
"""<html>The <b>Default Name Length Match Tolerance</b> is for eliminating automatic
search matches that are too long compared to your series name search. The higher
it is, the more likely to have a good match, but each search will take longer and
use more bandwidth. Too low, and only the very closest lexical matches will be
explored.</html>"""
explored.</html>""")
self.leNameLengthDeltaThresh.setToolTip(nldt_tip)
self.leNameLengthDeltaThresh.setToolTip(nldtTip)
pbl_tip = """<html>
The <b>Publisher Filter</b> is for eliminating automatic matches to certain publishers
pblTip = (
"""<html>
The <b>Publisher Blacklist</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.tePublisherFilter.setToolTip(pbl_tip)
)
self.tePublisherBlacklist.setToolTip(pblTip)
validator = QtGui.QIntValidator(1, 4, self)
self.leIssueNumPadding.setValidator(validator)
@ -105,77 +109,152 @@ class SettingsWindow(QtWidgets.QDialog):
validator = QtGui.QIntValidator(0, 99, self)
self.leNameLengthDeltaThresh.setValidator(validator)
self.settings_to_form()
self.settingsToForm()
self.btnBrowseRar.clicked.connect(self.select_rar)
self.btnClearCache.clicked.connect(self.clear_cache)
self.btnResetSettings.clicked.connect(self.reset_settings)
self.btnTestKey.clicked.connect(self.test_api_key)
self.btnBrowseRar.clicked.connect(self.selectRar)
self.btnClearCache.clicked.connect(self.clearCache)
self.btnResetSettings.clicked.connect(self.resetSettings)
self.btnTestKey.clicked.connect(self.testAPIKey)
self.btnTemplateHelp.clicked.connect(self.showTemplateHelp)
def settings_to_form(self):
def configRenamer(self):
md = GenericMetadata()
md.isEmpty = False
md.tagOrigin = "testing"
md.series = "series name"
md.issue = "1"
md.title = "issue title"
md.publisher = "publisher"
md.year = 1998
md.month = 4
md.day = 4
md.issueCount = 1
md.volume = 256
md.genre = "test"
md.language = "en" # 2 letter iso code
md.comments = "This is definitly a comic." # use same way as Summary in CIX
md.volumeCount = 4096
md.criticalRating = "Worst Comic Ever"
md.country = "US"
md.alternateSeries = "None"
md.alternateNumber = 4.4
md.alternateCount = 4444
md.imprint = 'imprint'
md.notes = "This doesn't actually exist"
md.webLink = "https://example.com/series name/1"
md.format = "Box Set"
md.manga = "Yes"
md.blackAndWhite = False
md.pageCount = 4
md.maturityRating = "Everyone"
md.storyArc = "story"
md.seriesGroup = "seriesGroup"
md.scanInfo = "(lordwelch)"
md.characters = "character 1, character 2"
md.teams = "None"
md.locations = "Earth, 444 B.C."
md.credits = [dict({'role': 'Everything', 'person': 'author', 'primary': True})]
md.tags = ["testing", "not real"]
md.pages = [dict({'Image': '0', 'Type': 'Front Cover'}), dict({'Image': '1', 'Type': 'Story'})]
# Some CoMet-only items
md.price = 0.00
md.isVersionOf = "SERIES #1"
md.rights = "None"
md.identifier = "LW4444-Comic"
md.lastMark = "0"
md.coverImage = "https://example.com/series name/1/cover"
self.renamer = FileRenamer(md)
self.renamer.setTemplate(str(self.leRenameTemplate.text()))
self.renamer.setIssueZeroPadding(self.settings.rename_issue_number_padding)
self.renamer.setSmartCleanup(self.settings.rename_use_smart_string_cleanup)
def settingsToForm(self):
# Copy values from settings to form
self.leRarExePath.setText(self.settings.rar_exe_path)
self.leNameLengthDeltaThresh.setText(str(self.settings.id_length_delta_thresh))
self.tePublisherFilter.setPlainText(self.settings.id_publisher_filter)
self.leNameLengthDeltaThresh.setText(
str(self.settings.id_length_delta_thresh))
self.tePublisherBlacklist.setPlainText(
self.settings.id_publisher_blacklist)
if self.settings.check_for_new_version:
self.cbxCheckForNewVersion.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxCheckForNewVersion.setCheckState(QtCore.Qt.Checked)
if self.settings.parse_scan_info:
self.cbxParseScanInfo.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxParseScanInfo.setCheckState(QtCore.Qt.Checked)
if self.settings.use_series_start_as_volume:
self.cbxUseSeriesStartAsVolume.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxUseSeriesStartAsVolume.setCheckState(QtCore.Qt.Checked)
if self.settings.clear_form_before_populating_from_cv:
self.cbxClearFormBeforePopulating.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxClearFormBeforePopulating.setCheckState(QtCore.Qt.Checked)
if self.settings.remove_html_tables:
self.cbxRemoveHtmlTables.setCheckState(QtCore.Qt.CheckState.Checked)
if self.settings.always_use_publisher_filter:
self.cbxUseFilter.setCheckState(QtCore.Qt.CheckState.Checked)
if self.settings.sort_series_by_year:
self.cbxSortByYear.setCheckState(QtCore.Qt.CheckState.Checked)
if self.settings.exact_series_matches_first:
self.cbxExactMatches.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxRemoveHtmlTables.setCheckState(QtCore.Qt.Checked)
self.leKey.setText(str(self.settings.cv_api_key))
if self.settings.assume_lone_credit_is_primary:
self.cbxAssumeLoneCreditIsPrimary.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxAssumeLoneCreditIsPrimary.setCheckState(QtCore.Qt.Checked)
if self.settings.copy_characters_to_tags:
self.cbxCopyCharactersToTags.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxCopyCharactersToTags.setCheckState(QtCore.Qt.Checked)
if self.settings.copy_teams_to_tags:
self.cbxCopyTeamsToTags.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxCopyTeamsToTags.setCheckState(QtCore.Qt.Checked)
if self.settings.copy_locations_to_tags:
self.cbxCopyLocationsToTags.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxCopyLocationsToTags.setCheckState(QtCore.Qt.Checked)
if self.settings.copy_storyarcs_to_tags:
self.cbxCopyStoryArcsToTags.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxCopyStoryArcsToTags.setCheckState(QtCore.Qt.Checked)
if self.settings.copy_notes_to_comments:
self.cbxCopyNotesToComments.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxCopyNotesToComments.setCheckState(QtCore.Qt.Checked)
if self.settings.copy_weblink_to_comments:
self.cbxCopyWebLinkToComments.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxCopyWebLinkToComments.setCheckState(QtCore.Qt.Checked)
if self.settings.apply_cbl_transform_on_cv_import:
self.cbxApplyCBLTransformOnCVIMport.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxApplyCBLTransformOnCVIMport.setCheckState(
QtCore.Qt.Checked)
if self.settings.apply_cbl_transform_on_bulk_operation:
self.cbxApplyCBLTransformOnBatchOperation.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxApplyCBLTransformOnBatchOperation.setCheckState(
QtCore.Qt.Checked)
self.leRenameTemplate.setText(self.settings.rename_template)
self.leIssueNumPadding.setText(str(self.settings.rename_issue_number_padding))
self.leIssueNumPadding.setText(
str(self.settings.rename_issue_number_padding))
if self.settings.rename_use_smart_string_cleanup:
self.cbxSmartCleanup.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxSmartCleanup.setCheckState(QtCore.Qt.Checked)
if self.settings.rename_extension_based_on_archive:
self.cbxChangeExtension.setCheckState(QtCore.Qt.CheckState.Checked)
self.cbxChangeExtension.setCheckState(QtCore.Qt.Checked)
if self.settings.rename_move_dir:
self.cbxMoveFiles.setCheckState(QtCore.Qt.Checked)
self.leDirectory.setText(self.settings.rename_dir)
def accept(self):
self.configRenamer()
try:
new_name = self.renamer.determineName('test.cbz')
except Exception as e:
QtWidgets.QMessageBox.critical(self, 'Invalid format string!',
'Your rename template is invalid!'
'<br/><br/>{}<br/><br/>'
'Please consult the template help in the '
'settings and the documentation on the format at '
'<a href=\'https://docs.python.org/3/library/string.html#format-string-syntax\'>'
'https://docs.python.org/3/library/string.html#format-string-syntax</a>'.format(e))
return
# Copy values from form to settings and save
self.settings.rar_exe_path = str(self.leRarExePath.text())
# make sure rar program is now in the path for the rar class
if self.settings.rar_exe_path:
utils.add_to_path(os.path.dirname(self.settings.rar_exe_path))
utils.addtopath(os.path.dirname(self.settings.rar_exe_path))
if not str(self.leNameLengthDeltaThresh.text()).isdigit():
self.leNameLengthDeltaThresh.setText("0")
@ -184,19 +263,16 @@ class SettingsWindow(QtWidgets.QDialog):
self.settings.check_for_new_version = self.cbxCheckForNewVersion.isChecked()
self.settings.id_length_delta_thresh = int(self.leNameLengthDeltaThresh.text())
self.settings.id_publisher_filter = str(self.tePublisherFilter.toPlainText())
self.settings.id_length_delta_thresh = int(
self.leNameLengthDeltaThresh.text())
self.settings.id_publisher_blacklist = str(
self.tePublisherBlacklist.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()
@ -210,55 +286,81 @@ class SettingsWindow(QtWidgets.QDialog):
self.settings.apply_cbl_transform_on_bulk_operation = self.cbxApplyCBLTransformOnBatchOperation.isChecked()
self.settings.rename_template = str(self.leRenameTemplate.text())
self.settings.rename_issue_number_padding = int(self.leIssueNumPadding.text())
self.settings.rename_issue_number_padding = int(
self.leIssueNumPadding.text())
self.settings.rename_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked()
self.settings.rename_extension_based_on_archive = self.cbxChangeExtension.isChecked()
self.settings.rename_move_dir = self.cbxMoveFiles.isChecked()
self.settings.rename_dir = self.leDirectory.text()
self.settings.save()
QtWidgets.QDialog.accept(self)
def selectRar(self):
self.selectFile(self.leRarExePath, "RAR")
def select_rar(self):
self.select_file(self.leRarExePath, "RAR")
def clearCache(self):
ImageFetcher().clearCache()
ComicVineCacher().clearCache()
QtWidgets.QMessageBox.information(
self, self.name, "Cache has been cleared.")
def clear_cache(self):
ImageFetcher().clear_cache()
ComicVineCacher().clear_cache()
QtWidgets.QMessageBox.information(self, self.name, "Cache has been cleared.")
def test_api_key(self):
if ComicVineTalker().test_key(str(self.leKey.text()).strip()):
QtWidgets.QMessageBox.information(self, "API Key Test", "Key is valid!")
def testAPIKey(self):
if ComicVineTalker().testKey(str(self.leKey.text()).strip()):
QtWidgets.QMessageBox.information(
self, "API Key Test", "Key is valid!")
else:
QtWidgets.QMessageBox.warning(self, "API Key Test", "Key is NOT valid.")
QtWidgets.QMessageBox.warning(
self, "API Key Test", "Key is NOT valid.")
def reset_settings(self):
def resetSettings(self):
self.settings.reset()
self.settings_to_form()
QtWidgets.QMessageBox.information(self, self.name, self.name + " have been returned to default values.")
self.settingsToForm()
QtWidgets.QMessageBox.information(
self,
self.name,
self.name +
" have been returned to default values.")
def select_file(self, control: QtWidgets.QLineEdit, name):
def selectFile(self, control, name):
dialog = QtWidgets.QFileDialog(self)
dialog.setFileMode(QtWidgets.QFileDialog.FileMode.ExistingFile)
dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile)
if platform.system() == "Windows":
if name == "RAR":
flt = "Rar Program (Rar.exe)"
filter = self.tr("Rar Program (Rar.exe)")
else:
flt = "Libraries (*.dll)"
dialog.setNameFilter(flt)
filter = self.tr("Libraries (*.dll)")
dialog.setNameFilter(filter)
else:
dialog.setFilter(QtCore.QDir.Filter.Files)
# QtCore.QDir.Executable | QtCore.QDir.Files)
dialog.setFilter(QtCore.QDir.Files)
pass
dialog.setDirectory(os.path.dirname(str(control.text())))
if name == "RAR":
dialog.setWindowTitle("Find " + name + " program")
else:
dialog.setWindowTitle("Find " + name + " library")
dialog.setWindowTitle("Find " + name + " library")
if (dialog.exec_()):
fileList = dialog.selectedFiles()
control.setText(str(fileList[0]))
if dialog.exec():
file_list = dialog.selectedFiles()
control.setText(str(file_list[0]))
def show_rename_tab(self):
def showRenameTab(self):
self.tabWidget.setCurrentIndex(5)
def showTemplateHelp(self):
TemplateHelpWin = TemplateHelpWindow(self)
TemplateHelpWin.setModal(False)
TemplateHelpWin.show()
class TemplateHelpWindow(QtWidgets.QDialog):
def __init__(self, parent):
super(TemplateHelpWindow, self).__init__(parent)
uic.loadUi(ComicTaggerSettings.getUIFile('TemplateHelp.ui'), self)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>702</width>
<height>452</height>
</rect>
</property>
<property name="windowTitle">
<string>Template Help</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>2</number>
</property>
<item>
<widget class="QTextBrowser" name="textBrowser">
<property name="html">
<string>&lt;html&gt;
&lt;head/&gt;
&lt;body&gt;
&lt;h1 style=&quot;text-align: center&quot;&gt;Template help&lt;/h1&gt;
&lt;p&gt;The template uses Python format strings, in the simplest use it replaces the field (e.g. {issue}) with the value for that particular comic (e.g. 1) for advanced formatting please reference the
&lt;a href=&quot;https://docs.python.org/3/library/string.html#format-string-syntax&quot;&gt;Python 3 documentation&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;Accepts the following variables:
{isEmpty}&#009;&#009;(boolean)
{tagOrigin}&#009;&#009;(string)
{series}&#009;&#009;(string)
{issue}&#009;&#009;(string)
{title}&#009;&#009;(string)
{publisher}&#009;&#009;(string)
{month}&#009;&#009;(integer)
{year}&#009;&#009;(integer)
{day}&#009;&#009;(integer)
{issueCount}&#009;(integer)
{volume}&#009;&#009;(integer)
{genre}&#009;&#009;(string)
{language}&#009;&#009;(string)
{comments}&#009;&#009;(string)
{volumeCount}&#009;(integer)
{criticalRating}&#009;(string)
{country}&#009;&#009;(string)
{alternateSeries}&#009;(string)
{alternateNumber}&#009;(string)
{alternateCount}&#009;(integer)
{imprint}&#009;&#009;(string)
{notes}&#009;&#009;(string)
{webLink}&#009;&#009;(string)
{format}&#009;&#009;(string)
{manga}&#009;&#009;(string)
{blackAndWhite}&#009;(boolean)
{pageCount}&#009;&#009;(integer)
{maturityRating}&#009;(string)
{storyArc}&#009;&#009;(string)
{seriesGroup}&#009;(string)
{scanInfo}&#009;&#009;(string)
{characters}&#009;(string)
{teams}&#009;&#009;(string)
{locations}&#009;&#009;(string)
{credits}&#009;&#009;(list of dict({&apos;role&apos;: &apos;str&apos;, &apos;person&apos;: &apos;str&apos;, &apos;primary&apos;: boolean}))
{tags}&#009;&#009;(list of str)
{pages}&#009;&#009;(list of dict({&apos;Image&apos;: &apos;str(int)&apos;, &apos;Type&apos;: &apos;str&apos;}))
CoMet-only items:
{price}&#009;&#009;(float)
{isVersionOf}&#009;(string)
{rights}&#009;&#009;(string)
{identifier}&#009;(string)
{lastMark}&#009;(string)
{coverImage}&#009;(string)
Examples:
{series} {issue} ({year})
Spider-Geddon 1 (2018)
{series} #{issue} - {title}
Spider-Geddon #1 - New Players; Check In
&lt;/pre&gt;
&lt;/body&gt;
&lt;/html&gt;</string>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -37,9 +37,6 @@
<property name="defaultDropAction">
<enum>Qt::MoveAction</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
</widget>
</item>
<item>
@ -100,24 +97,6 @@
</item>
</layout>
</item>
<item>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Bookmark:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="leBookmark">
<property name="acceptDrops">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QWidget" name="pageContainer" native="true">
<property name="sizePolicy">

View File

@ -1,49 +1,45 @@
"""Some utilities for the GUI"""
import io
import logging
#import StringIO
#from PIL import Image
from comictaggerlib.settings import ComicTaggerSettings
logger = logging.getLogger(__name__)
try:
from PyQt5 import QtGui
qt_available = True
except ImportError:
qt_available = False
if qt_available:
try:
from PIL import Image, ImageQt
pil_available = True
except ImportError:
pil_available = False
def reduce_widget_font_size(widget, delta=2):
def reduceWidgetFontSize(widget, delta=2):
f = widget.font()
if f.pointSize() > 10:
f.setPointSize(f.pointSize() - delta)
widget.setFont(f)
def center_window_on_screen(window):
def centerWindowOnScreen(window):
"""Center the window on screen.
This implementation will handle the window
being resized or the screen resolution changing.
"""
# Get the current screens' dimensions...
screen = QtGui.QGuiApplication.primaryScreen().geometry()
# The horizontal position is calculated as (screen width - window width) / 2
hpos = int((screen.width() - window.width()) / 2)
screen = QtGui.QDesktopWidget().screenGeometry()
# ... and get this windows' dimensions
mysize = window.geometry()
# The horizontal position is calculated as screen width - window width
# / 2
hpos = (screen.width() - window.width()) / 2
# And vertical position the same, but with the height dimensions
vpos = int((screen.height() - window.height()) / 2)
vpos = (screen.height() - window.height()) / 2
# And the move call repositions the window
window.move(hpos, vpos)
def center_window_on_parent(window):
def centerWindowOnParent(window):
top_level = window
while top_level.parent() is not None:
@ -52,25 +48,43 @@ if qt_available:
# Get the current screens' dimensions...
main_window_size = top_level.geometry()
# ... and get this windows' dimensions
# The horizontal position is calculated as (screen width - window width) / 2
hpos = int((main_window_size.width() - window.width()) / 2)
mysize = window.geometry()
# The horizontal position is calculated as screen width - window width
# /2
hpos = (main_window_size.width() - window.width()) / 2
# And vertical position the same, but with the height dimensions
vpos = int((main_window_size.height() - window.height()) / 2)
vpos = (main_window_size.height() - window.height()) / 2
# And the move call repositions the window
window.move(hpos + main_window_size.left(), vpos + main_window_size.top())
window.move(
hpos +
main_window_size.left(),
vpos +
main_window_size.top())
def get_qimage_from_data(image_data):
try:
from PIL import Image
from PIL import WebPImagePlugin
import io
pil_available = True
except ImportError:
pil_available = False
def getQImageFromData(image_data):
img = QtGui.QImage()
success = img.loadFromData(image_data)
if not success:
try:
if pil_available:
# Qt doesn't understand the format, but maybe PIL does
img = ImageQt.ImageQt(Image.open(io.BytesIO(image_data)))
success = True
except Exception:
# Qt doesn't understand the format, but maybe PIL does
# so try to convert the image data to uncompressed tiff
# format
im = Image.open(io.StringIO(image_data))
output = io.StringIO()
im.save(output, format="PNG")
success = img.loadFromData(output.getvalue())
except Exception as e:
pass
# if still nothing, go with default image
if not success:
img.load(ComicTaggerSettings.get_graphic("nocover.png"))
img.load(ComicTaggerSettings.getGraphic('nocover.png'))
return img

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>702</width>
<height>478</height>
<height>432</height>
</rect>
</property>
<property name="windowTitle">
@ -28,7 +28,7 @@
</sizepolicy>
</property>
<property name="currentIndex">
<number>1</number>
<number>0</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>Searching</string>
<string>Identifier</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
@ -187,15 +187,15 @@
</property>
</widget>
</item>
<item row="2" column="0">
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Publisher Filter:</string>
<string>Publisher Blacklist:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QPlainTextEdit" name="tePublisherFilter">
<item row="1" column="1">
<widget class="QPlainTextEdit" name="tePublisherBlacklist">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
@ -204,23 +204,6 @@
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="cbxUseFilter">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Applies the &lt;span style=&quot; font-weight:600;&quot;&gt;Publisher Filter&lt;/span&gt; on all searches.&lt;br/&gt;The search window has a dynamic toggle to show the unfiltered results.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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 &quot;manual&quot; searches:</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
@ -280,33 +263,6 @@
</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>
@ -547,7 +503,7 @@
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="1" column="0">
<widget class="QLabel" name="label">
<widget class="QLabel" name="lblTemplate">
<property name="text">
<string>Template:</string>
</property>
@ -556,31 +512,80 @@
<item row="1" column="1">
<widget class="QLineEdit" name="leRenameTemplate">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The template for the new filename. Accepts the following variables:&lt;/p&gt;&lt;p&gt;%series%&lt;br/&gt;%issue%&lt;br/&gt;%volume%&lt;br/&gt;%issuecount%&lt;br/&gt;%year%&lt;br/&gt;%month%&lt;br/&gt;%month_name%&lt;br/&gt;%publisher%&lt;br/&gt;%title%&lt;br/&gt;
%genre%&lt;br/&gt;
%language_code%&lt;br/&gt;
%criticalrating%&lt;br/&gt;
%alternateseries%&lt;br/&gt;
%alternatenumber%&lt;br/&gt;
%alternatecount%&lt;br/&gt;
%imprint%&lt;br/&gt;
%format%&lt;br/&gt;
%maturityrating%&lt;br/&gt;
%storyarc%&lt;br/&gt;
%seriesgroup%&lt;br/&gt;
%scaninfo%
&lt;/p&gt;&lt;p&gt;Examples:&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;%series% %issue% (%year%)&lt;/span&gt;&lt;br/&gt;&lt;span style=&quot; font-style:italic;&quot;&gt;%series% #%issue% - %title%&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;pre&gt;The template for the new filename. Uses python format strings https://docs.python.org/3/library/string.html#format-string-syntax
Accepts the following variables:
{isEmpty} (boolean)
{tagOrigin} (string)
{series} (string)
{issue} (string)
{title} (string)
{publisher} (string)
{month} (integer)
{year} (integer)
{day} (integer)
{issueCount} (integer)
{volume} (integer)
{genre} (string)
{language} (string)
{comments} (string)
{volumeCount} (integer)
{criticalRating} (string)
{country} (string)
{alternateSeries} (string)
{alternateNumber} (string)
{alternateCount} (integer)
{imprint} (string)
{notes} (string)
{webLink} (string)
{format} (string)
{manga} (string)
{blackAndWhite} (boolean)
{pageCount} (integer)
{maturityRating} (string)
{storyArc} (string)
{seriesGroup} (string)
{scanInfo} (string)
{characters} (string)
{teams} (string)
{locations} (string)
{credits} (list of dict({&amp;apos;role&amp;apos;: &amp;apos;str&amp;apos;, &amp;apos;person&amp;apos;: &amp;apos;str&amp;apos;, &amp;apos;primary&amp;apos;: boolean}))
{tags} (list of str)
{pages} (list of dict({&amp;apos;Image&amp;apos;: &amp;apos;str(int)&amp;apos;, &amp;apos;Type&amp;apos;: &amp;apos;str&amp;apos;}))
CoMet-only items:
{price} (float)
{isVersionOf} (string)
{rights} (string)
{identifier} (string)
{lastMark} (string)
{coverImage} (string)
Examples:
{series} {issue} ({year})
Spider-Geddon 1 (2018)
{series} #{issue} - {title}
Spider-Geddon #1 - New Players; Check In
&lt;/pre&gt;</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<widget class="QPushButton" name="btnTemplateHelp">
<property name="text">
<string>Template Help</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="lblPadding">
<property name="text">
<string>Issue # Zero Padding</string>
</property>
</widget>
</item>
<item row="2" column="1">
<item row="3" column="1">
<widget class="QLineEdit" name="leIssueNumPadding">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
@ -599,7 +604,7 @@
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="cbxSmartCleanup">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;&amp;quot;Smart Text Cleanup&amp;quot; &lt;/span&gt;will attempt to clean up the new filename if there are missing fields from the template. For example, removing empty braces, repeated spaces and dashes, and more. Experimental feature.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
@ -609,13 +614,33 @@
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<item row="5" column="0" colspan="2">
<widget class="QCheckBox" name="cbxChangeExtension">
<property name="text">
<string>Change Extension Based On Archive Type</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="cbxMoveFiles">
<property name="toolTip">
<string>If checked moves files to specified folder</string>
</property>
<property name="text">
<string>Move files when renaming</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="lblDirectory">
<property name="text">
<string>Destination Directory:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QLineEdit" name="leDirectory"/>
</item>
</layout>
</item>
</layout>

View File

@ -512,6 +512,16 @@
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QLineEdit" name="leSeriesPubYear"/>
</item>
<item row="9" column="1">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Series Year</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
@ -929,31 +939,11 @@
</widget>
</item>
<item row="2" column="1">
<layout class="QGridLayout" name="gridLayout_7">
<item row="0" column="0">
<widget class="QLineEdit" name="leWebLink">
<property name="acceptDrops">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="btnOpenWebLink">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>40</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
</layout>
<widget class="QLineEdit" name="leWebLink">
<property name="acceptDrops">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="userRatingLabel">

View File

@ -148,16 +148,6 @@
</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">

1
comictaggerlib/utils.py Normal file
View File

@ -0,0 +1 @@
from comicapi.utils import *

View File

@ -14,45 +14,87 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import platform
import sys
import platform
import requests
import urllib.parse
#import os
from comictaggerlib import ctversion
try:
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from PyQt5.QtCore import QUrl, pyqtSignal, QObject, QByteArray
except ImportError:
# No Qt, so define a few dummy QObjects to help us compile
class QObject():
logger = logging.getLogger(__name__)
def __init__(self, *args):
pass
class pyqtSignal():
def __init__(self, *args):
pass
def emit(a, b, c):
pass
from . import ctversion
class VersionChecker:
def get_request_url(self, uuid, use_stats):
class VersionChecker(QObject):
def getRequestUrl(self, uuid, use_stats):
base_url = "http://comictagger1.appspot.com/latest"
params = {}
args = ""
params = dict()
if use_stats:
params = {"uuid": uuid, "version": ctversion.version}
params = {
'uuid': uuid,
'version': ctversion.version
}
if platform.system() == "Windows":
params["platform"] = "win"
params['platform'] = "win"
elif platform.system() == "Linux":
params["platform"] = "lin"
params['platform'] = "lin"
elif platform.system() == "Darwin":
params["platform"] = "mac"
params['platform'] = "mac"
else:
params["platform"] = "other"
params['platform'] = "other"
if not getattr(sys, "frozen", None):
params["src"] = "T"
if not getattr(sys, 'frozen', None):
params['src'] = 'T'
return base_url, params
return (base_url, params)
def get_latest_version(self, uuid, use_stats=True):
def getLatestVersion(self, uuid, use_stats=True):
try:
url, params = self.get_request_url(uuid, use_stats)
url, params = self.getRequestUrl(uuid, use_stats)
new_version = requests.get(url, params=params).text
except Exception:
except Exception as e:
return None
if new_version is None or new_version == "":
return None
return new_version.strip()
versionRequestComplete = pyqtSignal(str)
def asyncGetLatestVersion(self, uuid, use_stats):
url, params = self.getRequestUrl(uuid, use_stats)
self.nam = QNetworkAccessManager()
self.nam.finished.connect(self.asyncGetLatestVersionComplete)
self.nam.get(QNetworkRequest(QUrl(str(url + '?' + urllib.parse.urlencode(params)))))
def asyncGetLatestVersionComplete(self, reply):
if (reply.error() != QNetworkReply.NoError):
return
# read in the response
new_version = str(reply.readAll())
if new_version is None or new_version == "":
return
self.versionRequestComplete.emit(new_version.strip())

View File

@ -14,26 +14,30 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
#import sys
#import time
#import os
from PyQt5 import QtCore, QtWidgets, uic
from PyQt5.QtCore import pyqtSignal
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtCore import QUrl, pyqtSignal
#from PyQt4.QtCore import QObject
#from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest
from comicapi import utils
from comicapi.genericmetadata import GenericMetadata
from comictaggerlib.comicvinetalker import ComicVineTalker, ComicVineTalkerException
from comictaggerlib.coverimagewidget import CoverImageWidget
from comictaggerlib.issueidentifier import IssueIdentifier
from comictaggerlib.issueselectionwindow import IssueSelectionWindow
from comictaggerlib.matchselectionwindow import MatchSelectionWindow
from comictaggerlib.progresswindow import IDProgressWindow
from comictaggerlib.settings import ComicTaggerSettings
from comictaggerlib.ui.qtutils import reduce_widget_font_size
logger = logging.getLogger(__name__)
from .comicvinetalker import ComicVineTalker, ComicVineTalkerException
from .issueselectionwindow import IssueSelectionWindow
from .issueidentifier import IssueIdentifier
from .genericmetadata import GenericMetadata
from .progresswindow import IDProgressWindow
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
class SearchThread(QtCore.QThread):
searchComplete = pyqtSignal()
progressUpdate = pyqtSignal(int, int)
@ -42,16 +46,15 @@ class SearchThread(QtCore.QThread):
self.series_name = series_name
self.refresh = refresh
self.error_code = None
self.cv_error = False
self.cv_search_results = []
def run(self):
comic_vine = ComicVineTalker()
comicVine = ComicVineTalker()
try:
self.cv_error = False
self.cv_search_results = comic_vine.search_for_series(
self.series_name, callback=self.prog_callback, refresh_cache=self.refresh
)
self.cv_search_results = comicVine.searchForSeries(
self.series_name,
callback=self.prog_callback,
refresh_cache=self.refresh)
except ComicVineTalkerException as e:
self.cv_search_results = []
self.cv_error = True
@ -65,59 +68,49 @@ class SearchThread(QtCore.QThread):
class IdentifyThread(QtCore.QThread):
identifyComplete = pyqtSignal()
identifyLogMsg = pyqtSignal(str)
identifyProgress = pyqtSignal(int, int)
def __init__(self, identifier: IssueIdentifier):
def __init__(self, identifier):
QtCore.QThread.__init__(self)
self.identifier = identifier
self.identifier.set_output_function(self.log_output)
self.identifier.set_progress_callback(self.progress_callback)
self.identifier.setOutputFunction(self.logOutput)
self.identifier.setProgressCallback(self.progressCallback)
def log_output(self, text: str):
self.identifyLogMsg.emit(str(text))
def logOutput(self, text):
self.identifyLogMsg.emit(text)
def progress_callback(self, cur, total):
def progressCallback(self, cur, total):
self.identifyProgress.emit(cur, total)
def run(self):
self.identifier.search()
matches = self.identifier.search()
self.identifyComplete.emit()
class VolumeSelectionWindow(QtWidgets.QDialog):
def __init__(
self,
parent,
series_name,
issue_number,
year,
issue_count,
cover_index_list,
comic_archive,
settings,
autoselect=False,
):
super().__init__(parent)
uic.loadUi(ComicTaggerSettings.get_ui_file("volumeselectionwindow.ui"), self)
def __init__(self, parent, series_name, issue_number, year, issue_count,
cover_index_list, comic_archive, settings, autoselect=False):
super(VolumeSelectionWindow, self).__init__(parent)
self.imageWidget = CoverImageWidget(self.imageContainer, CoverImageWidget.URLMode)
uic.loadUi(
ComicTaggerSettings.getUIFile('volumeselectionwindow.ui'), self)
self.imageWidget = CoverImageWidget(
self.imageContainer, CoverImageWidget.URLMode)
gridlayout = QtWidgets.QGridLayout(self.imageContainer)
gridlayout.addWidget(self.imageWidget)
gridlayout.setContentsMargins(0, 0, 0, 0)
reduce_widget_font_size(self.teDetails, 1)
reduce_widget_font_size(self.twList)
reduceWidgetFontSize(self.teDetails, 1)
reduceWidgetFontSize(self.twList)
self.setWindowFlags(
QtCore.Qt.WindowType(
self.windowFlags()
| QtCore.Qt.WindowType.WindowSystemMenuHint
| QtCore.Qt.WindowType.WindowMaximizeButtonHint
)
)
self.setWindowFlags(self.windowFlags() |
QtCore.Qt.WindowSystemMenuHint |
QtCore.Qt.WindowMaximizeButtonHint)
self.settings = settings
self.parent = parent
@ -131,51 +124,50 @@ 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.current_item_changed)
self.twList.cellDoubleClicked.connect(self.cell_double_clicked)
self.twList.currentItemChanged.connect(self.currentItemChanged)
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
self.btnRequery.clicked.connect(self.requery)
self.btnIssues.clicked.connect(self.show_issues)
self.btnAutoSelect.clicked.connect(self.auto_select)
self.btnIssues.clicked.connect(self.showIssues)
self.btnAutoSelect.clicked.connect(self.autoSelect)
self.cbxFilter.setChecked(self.use_filter)
self.cbxFilter.toggled.connect(self.filter_toggled)
self.update_buttons()
self.perform_query()
self.updateButtons()
self.performQuery()
self.twList.selectRow(0)
def update_buttons(self):
enabled = self.cv_search_results is not None and len(self.cv_search_results) > 0
def updateButtons(self):
if self.cv_search_results is not None and len(
self.cv_search_results) > 0:
enabled = True
else:
enabled = False
self.btnRequery.setEnabled(enabled)
self.btnIssues.setEnabled(enabled)
self.btnAutoSelect.setEnabled(enabled)
self.buttonBox.button(QtWidgets.QDialogButtonBox.StandardButton.Ok).setEnabled(enabled)
self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(enabled)
def requery(self):
self.perform_query(refresh=True)
def requery(self,):
self.performQuery(refresh=True)
self.twList.selectRow(0)
def filter_toggled(self):
self.use_filter = not self.use_filter
self.perform_query(refresh=False)
def auto_select(self):
def autoSelect(self):
if self.comic_archive is None:
QtWidgets.QMessageBox.information(self, "Auto-Select", "You need to load a comic first!")
QtWidgets.QMessageBox.information(
self, "Auto-Select", "You need to load a comic first!")
return
if self.issue_number is None or self.issue_number == "":
QtWidgets.QMessageBox.information(self, "Auto-Select", "Can't auto-select without an issue number (yet!)")
QtWidgets.QMessageBox.information(
self,
"Auto-Select",
"Can't auto-select without an issue number (yet!)")
return
self.iddialog = IDProgressWindow(self)
self.iddialog.setModal(True)
self.iddialog.rejected.connect(self.identify_cancel)
self.iddialog.rejected.connect(self.identifyCancel)
self.iddialog.show()
self.ii = IssueIdentifier(self.comic_archive, self.settings)
@ -184,199 +176,165 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
md.series = self.series_name
md.issue = self.issue_number
md.year = self.year
md.issue_count = self.issue_count
md.issueCount = self.issue_count
self.ii.set_additional_metadata(md)
self.ii.only_use_additional_meta_data = True
self.ii.setAdditionalMetadata(md)
self.ii.onlyUseAdditionalMetaData = True
self.ii.cover_page_index = int(self.cover_index_list[0])
self.id_thread = IdentifyThread(self.ii)
self.id_thread.identifyComplete.connect(self.identify_complete)
self.id_thread.identifyLogMsg.connect(self.log_id_output)
self.id_thread.identifyProgress.connect(self.identify_progress)
self.id_thread.identifyComplete.connect(self.identifyComplete)
self.id_thread.identifyLogMsg.connect(self.logIDOutput)
self.id_thread.identifyProgress.connect(self.identifyProgress)
self.id_thread.start()
self.iddialog.exec()
self.iddialog.exec_()
def log_id_output(self, text):
print(str(text), end=" ")
def logIDOutput(self, text):
print(str(text), end=' ')
self.iddialog.textEdit.ensureCursorVisible()
self.iddialog.textEdit.insertPlainText(text)
def identify_progress(self, cur, total):
def identifyProgress(self, cur, total):
self.iddialog.progressBar.setMaximum(total)
self.iddialog.progressBar.setValue(cur)
def identify_cancel(self):
def identifyCancel(self):
self.ii.cancel = True
def identify_complete(self):
def identifyComplete(self):
matches = self.ii.match_list
result = self.ii.search_result
match_index = 0
found_match = None
choices = False
if result == self.ii.result_no_matches:
QtWidgets.QMessageBox.information(self, "Auto-Select Result", " No matches found :-(")
elif result == self.ii.result_found_match_but_bad_cover_score:
if result == self.ii.ResultNoMatches:
QtWidgets.QMessageBox.information(
self, "Auto-Select Result", " Found a match, but cover doesn't seem the same. Verify before commiting!"
)
self, "Auto-Select Result", " No matches found :-(")
elif result == self.ii.ResultFoundMatchButBadCoverScore:
QtWidgets.QMessageBox.information(
self,
"Auto-Select Result",
" Found a match, but cover doesn't seem the same. Verify before commiting!")
found_match = matches[0]
elif result == self.ii.result_found_match_but_not_first_page:
elif result == self.ii.ResultFoundMatchButNotFirstPage:
QtWidgets.QMessageBox.information(
self, "Auto-Select Result", " Found a match, but not with the first page of the archive."
)
self,
"Auto-Select Result",
" Found a match, but not with the first page of the archive.")
found_match = matches[0]
elif result == self.ii.result_multiple_matches_with_bad_image_scores:
elif result == self.ii.ResultMultipleMatchesWithBadImageScores:
QtWidgets.QMessageBox.information(
self, "Auto-Select Result", " Found some possibilities, but no confidence. Proceed manually."
)
self,
"Auto-Select Result",
" Found some possibilities, but no confidence. Proceed manually.")
choices = True
elif result == self.ii.result_one_good_match:
elif result == self.ii.ResultOneGoodMatch:
found_match = matches[0]
elif result == self.ii.result_multiple_good_matches:
elif result == self.ii.ResultMultipleGoodMatches:
QtWidgets.QMessageBox.information(
self, "Auto-Select Result", " Found multiple likely matches. Please select."
)
self,
"Auto-Select Result",
" Found multiple likely matches. Please select.")
choices = True
if choices:
selector = MatchSelectionWindow(self, matches, self.comic_archive)
selector.setModal(True)
selector.exec()
selector.exec_()
if selector.result():
# we should now have a list index
found_match = selector.current_match()
found_match = selector.currentMatch()
if found_match is not None:
self.iddialog.accept()
self.volume_id = found_match["volume_id"]
self.issue_number = found_match["issue_number"]
self.select_by_id()
self.show_issues()
self.volume_id = found_match['volume_id']
self.issue_number = found_match['issue_number']
self.selectByID()
self.showIssues()
def show_issues(self):
selector = IssueSelectionWindow(self, self.settings, self.volume_id, self.issue_number)
def showIssues(self):
selector = IssueSelectionWindow(
self, self.settings, self.volume_id, self.issue_number)
title = ""
for record in self.cv_search_results:
if record["id"] == self.volume_id:
title = record["name"]
title += " (" + str(record["start_year"]) + ")"
if record['id'] == self.volume_id:
title = record['name']
title += " (" + str(record['start_year']) + ")"
title += " - "
break
selector.setWindowTitle(title + "Select Issue")
selector.setModal(True)
selector.exec()
selector.exec_()
if selector.result():
# we should now have a volume ID
self.issue_number = selector.issue_number
self.accept()
return
def select_by_id(self):
def selectByID(self):
for r in range(0, self.twList.rowCount()):
volume_id = self.twList.item(r, 0).data(QtCore.Qt.ItemDataRole.UserRole)
if volume_id == self.volume_id:
volume_id = self.twList.item(r, 0).data(QtCore.Qt.UserRole)
if (volume_id == self.volume_id):
self.twList.selectRow(r)
break
def perform_query(self, refresh=False):
def performQuery(self, refresh=False):
self.progdialog = QtWidgets.QProgressDialog("Searching Online", "Cancel", 0, 100, self)
self.progdialog = QtWidgets.QProgressDialog(
"Searching Online", "Cancel", 0, 100, self)
self.progdialog.setWindowTitle("Online Search")
self.progdialog.canceled.connect(self.search_canceled)
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.search_complete)
self.search_thread.progressUpdate.connect(self.search_progress_update)
self.search_thread.searchComplete.connect(self.searchComplete)
self.search_thread.progressUpdate.connect(self.searchProgressUpdate)
self.search_thread.start()
self.progdialog.exec()
self.progdialog.exec_()
def search_canceled(self):
logger.info("query cancelled")
self.search_thread.searchComplete.disconnect(self.search_complete)
self.search_thread.progressUpdate.disconnect(self.search_progress_update)
self.progdialog.canceled.disconnect(self.search_canceled)
def searchCanceled(self):
print("query cancelled")
self.search_thread.searchComplete.disconnect(self.searchComplete)
self.search_thread.progressUpdate.disconnect(self.searchProgressUpdate)
self.progdialog.canceled.disconnect(self.searchCanceled)
self.progdialog.reject()
QtCore.QTimer.singleShot(200, self.close_me)
QtCore.QTimer.singleShot(200, self.closeMe)
def close_me(self):
def closeMe(self):
print("closeme")
self.reject()
def search_progress_update(self, current, total):
def searchProgressUpdate(self, current, total):
self.progdialog.setMaximum(total)
self.progdialog.setValue(current + 1)
self.progdialog.setValue(current+1)
def search_complete(self):
def searchComplete(self):
self.progdialog.accept()
del self.progdialog
QtCore.QCoreApplication.processEvents()
if self.search_thread.cv_error:
if self.search_thread.error_code == ComicVineTalkerException.RateLimit:
QtWidgets.QMessageBox.critical(self, "Comic Vine Error", ComicVineTalker.get_rate_limit_message())
QtWidgets.QMessageBox.critical(
self,
self.tr("Comic Vine Error"),
ComicVineTalker.getRateLimitMessage())
else:
QtWidgets.QMessageBox.critical(
self, "Network Issue", "Could not connect to Comic Vine to search for series!"
)
self,
self.tr("Network Issue"),
self.tr("Could not connect to Comic Vine to search for series!"))
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:
logger.exception("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:
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:
logger.exception("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)
exact_matches = list(
filter(lambda d: utils.sanitize_title(str(d["name"])) in sanitized, self.cv_search_results)
)
non_matches = list(
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:
logger.exception("bad data error filtering exact/near matches")
self.update_buttons()
self.updateButtons()
self.twList.setSortingEnabled(False)
@ -387,71 +345,74 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
for record in self.cv_search_results:
self.twList.insertRow(row)
item_text = record["name"]
item_text = record['name']
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setData(QtCore.Qt.ItemDataRole.UserRole, record["id"])
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setData(QtCore.Qt.UserRole, record['id'])
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 0, item)
item_text = str(record["start_year"])
item_text = str(record['start_year'])
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 1, item)
item_text = record["count_of_issues"]
item_text = record['count_of_issues']
item = QtWidgets.QTableWidgetItem(item_text)
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
item.setData(QtCore.Qt.ItemDataRole.DisplayRole, record["count_of_issues"])
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
item.setData(QtCore.Qt.ToolTipRole, item_text)
item.setData(QtCore.Qt.DisplayRole, record['count_of_issues'])
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 2, item)
if record["publisher"] is not None:
item_text = record["publisher"]["name"]
item.setData(QtCore.Qt.ItemDataRole.ToolTipRole, item_text)
if record['publisher'] is not None:
item_text = record['publisher']['name']
item.setData(QtCore.Qt.ToolTipRole, item_text)
item = QtWidgets.QTableWidgetItem(item_text)
item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
item.setFlags(
QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
self.twList.setItem(row, 3, item)
row += 1
self.twList.resizeColumnsToContents()
self.twList.setSortingEnabled(True)
self.twList.sortItems(2, QtCore.Qt.DescendingOrder)
self.twList.selectRow(0)
self.twList.resizeColumnsToContents()
if len(self.cv_search_results) == 0:
QtCore.QCoreApplication.processEvents()
QtWidgets.QMessageBox.information(self, "Search Result", "No matches found!")
QtCore.QTimer.singleShot(200, self.close_me)
QtWidgets.QMessageBox.information(
self, "Search Result", "No matches found!")
if self.immediate_autoselect and len(self.cv_search_results) > 0:
# defer the immediate autoselect so this dialog has time to pop up
QtCore.QCoreApplication.processEvents()
QtCore.QTimer.singleShot(10, self.do_immediate_autoselect)
QtCore.QTimer.singleShot(10, self.doImmediateAutoselect)
def do_immediate_autoselect(self):
def doImmediateAutoselect(self):
self.immediate_autoselect = False
self.auto_select()
self.autoSelect()
def cell_double_clicked(self, r, c):
self.show_issues()
def cellDoubleClicked(self, r, c):
self.showIssues()
def current_item_changed(self, curr, prev):
def currentItemChanged(self, curr, prev):
if curr is None:
return
if prev is not None and prev.row() == curr.row():
return
self.volume_id = self.twList.item(curr.row(), 0).data(QtCore.Qt.ItemDataRole.UserRole)
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:
if record["description"] is None:
if record['id'] == self.volume_id:
if record['description'] is None:
self.teDetails.setText("")
else:
self.teDetails.setText(record["description"])
self.imageWidget.set_url(record["image"]["super_url"])
self.teDetails.setText(record['description'])
self.imageWidget.setURL(record['image']['super_url'])
break

View File

@ -5,7 +5,7 @@
# binary to call the CT script. This is all so that the
# Mac menu doesn't say "Python".
realpath()
realpath()
{
[[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
}

11
google/gadgets/social.xml Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="mygaget" />
<Content type="html">
<![CDATA[
<a href="https://twitter.com/ComicTagger" class="twitter-follow-button" data-show-count="false" data-size="large">Follow @ComicTagger</a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
<iframe allowtransparency="true" frameborder="0" scrolling="no" src="http://www.facebook.com/plugins/likebox.php?href=http%3A%2F%2Fwww.facebook.com%2Fpages%2FComictagger/139615369550787&amp;width=292&amp;colorscheme=light&amp;show_faces =false&amp;border_color&amp;stream=false&amp;header=false&amp;height=62" style="background-color: white; border-bottom-style: none; border-color: initial; border-left-style: none; border-right-style: none; border-top-style: none; border-width: initial; color: #333333; font-family: Verdana; font-size: 12px; height: 62px; line-height: 19px; overflow-x: hidden; overflow-y: hidden; text-align: -webkit-auto; width: 292px;"></iframe>
]]>
</Content>
</Module>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="mygaget" />
<Content type="html">
<![CDATA[
<a href="https://twitter.com/ComicTagger" class="twitter-follow-button" data-show-count="false" data-size="large">Follow @ComicTagger</a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
]]>
</Content>
</Module>

260
google/googlecode_upload.py Executable file
View File

@ -0,0 +1,260 @@
#!/usr/bin/env python
#
# Copyright 2006, 2007 Google Inc. All Rights Reserved.
# Author: danderson@google.com (David Anderson)
#
# Script for uploading files to a Google Code project.
#
# This is intended to be both a useful script for people who want to
# streamline project uploads and a reference implementation for
# uploading files to Google Code projects.
#
# To upload a file to Google Code, you need to provide a path to the
# file on your local machine, a small summary of what the file is, a
# project name, and a valid account that is a member or owner of that
# project. You can optionally provide a list of labels that apply to
# the file. The file will be uploaded under the same name that it has
# in your local filesystem (that is, the "basename" or last path
# component). Run the script with '--help' to get the exact syntax
# and available options.
#
# Note that the upload script requests that you enter your
# googlecode.com password. This is NOT your Gmail account password!
# This is the password you use on googlecode.com for committing to
# Subversion and uploading files. You can find your password by going
# to http://code.google.com/hosting/settings when logged in with your
# Gmail account. If you have already committed to your project's
# Subversion repository, the script will automatically retrieve your
# credentials from there (unless disabled, see the output of '--help'
# for details).
#
# If you are looking at this script as a reference for implementing
# your own Google Code file uploader, then you should take a look at
# the upload() function, which is the meat of the uploader. You
# basically need to build a multipart/form-data POST request with the
# right fields and send it to https://PROJECT.googlecode.com/files .
# Authenticate the request using HTTP Basic authentication, as is
# shown below.
#
# Licensed under the terms of the Apache Software License 2.0:
# http://www.apache.org/licenses/LICENSE-2.0
#
# Questions, comments, feature requests and patches are most welcome.
# Please direct all of these to the Google Code users group:
# http://groups.google.com/group/google-code-hosting
"""Google Code file uploader script.
"""
__author__ = 'danderson@google.com (David Anderson)'
import httplib
import os.path
import optparse
import getpass
import base64
import sys
def upload(file, project_name, user_name, password, summary, labels=None):
"""Upload a file to a Google Code project's file server.
Args:
file: The local path to the file.
project_name: The name of your project on Google Code.
user_name: Your Google account name.
password: The googlecode.com password for your account.
Note that this is NOT your global Google Account password!
summary: A small description for the file.
labels: an optional list of label strings with which to tag the file.
Returns: a tuple:
http_status: 201 if the upload succeeded, something else if an
error occured.
http_reason: The human-readable string associated with http_status
file_url: If the upload succeeded, the URL of the file on Google
Code, None otherwise.
"""
# The login is the user part of user@gmail.com. If the login provided
# is in the full user@domain form, strip it down.
if user_name.endswith('@gmail.com'):
user_name = user_name[:user_name.index('@gmail.com')]
form_fields = [('summary', summary)]
if labels is not None:
form_fields.extend([('label', l.strip()) for l in labels])
content_type, body = encode_upload_request(form_fields, file)
upload_host = '%s.googlecode.com' % project_name
upload_uri = '/files'
auth_token = base64.b64encode('%s:%s' % (user_name, password))
headers = {
'Authorization': 'Basic %s' % auth_token,
'User-Agent': 'Googlecode.com uploader v0.9.4',
'Content-Type': content_type,
}
server = httplib.HTTPSConnection(upload_host)
server.request('POST', upload_uri, body, headers)
resp = server.getresponse()
server.close()
if resp.status == 201:
location = resp.getheader('Location', None)
else:
location = None
return resp.status, resp.reason, location
def encode_upload_request(fields, file_path):
"""Encode the given fields and file into a multipart form body.
fields is a sequence of (name, value) pairs. file is the path of
the file to upload. The file will be uploaded to Google Code with
the same file name.
Returns: (content_type, body) ready for httplib.HTTP instance
"""
BOUNDARY = '----------Googlecode_boundary_reindeer_flotilla'
CRLF = '\r\n'
body = []
# Add the metadata about the upload first
for key, value in fields:
body.extend(
['--' + BOUNDARY,
'Content-Disposition: form-data; name="%s"' % key,
'',
value,
])
# Now add the file itself
file_name = os.path.basename(file_path)
f = open(file_path, 'rb')
file_content = f.read()
f.close()
body.extend(
['--' + BOUNDARY,
'Content-Disposition: form-data; name="filename"; filename="%s"'
% file_name,
# The upload server determines the mime-type, no need to set it.
'Content-Type: application/octet-stream',
'',
file_content,
])
# Finalize the form body
body.extend(['--' + BOUNDARY + '--', ''])
return 'multipart/form-data; boundary=%s' % BOUNDARY, CRLF.join(body)
def upload_find_auth(file_path, project_name, summary, labels=None,
user_name=None, password=None, tries=3):
"""Find credentials and upload a file to a Google Code project's file server.
file_path, project_name, summary, and labels are passed as-is to upload.
Args:
file_path: The local path to the file.
project_name: The name of your project on Google Code.
summary: A small description for the file.
labels: an optional list of label strings with which to tag the file.
config_dir: Path to Subversion configuration directory, 'none', or None.
user_name: Your Google account name.
tries: How many attempts to make.
"""
if user_name is None or password is None:
from netrc import netrc
authenticators = netrc().authenticators("code.google.com")
if authenticators:
if user_name is None:
user_name = authenticators[0]
if password is None:
password = authenticators[2]
while tries > 0:
if user_name is None:
# Read username if not specified or loaded from svn config, or on
# subsequent tries.
sys.stdout.write('Please enter your googlecode.com username: ')
sys.stdout.flush()
user_name = sys.stdin.readline().rstrip()
if password is None:
# Read password if not loaded from svn config, or on subsequent
# tries.
print 'Please enter your googlecode.com password.'
print '** Note that this is NOT your Gmail account password! **'
print 'It is the password you use to access Subversion repositories,'
print 'and can be found here: http://code.google.com/hosting/settings'
password = getpass.getpass()
status, reason, url = upload(
file_path, project_name, user_name, password, summary, labels)
# Returns 403 Forbidden instead of 401 Unauthorized for bad
# credentials as of 2007-07-17.
if status in [httplib.FORBIDDEN, httplib.UNAUTHORIZED]:
# Rest for another try.
user_name = password = None
tries = tries - 1
else:
# We're done.
break
return status, reason, url
def main():
parser = optparse.OptionParser(usage='googlecode-upload.py -s SUMMARY '
'-p PROJECT [options] FILE')
parser.add_option('-s', '--summary', dest='summary',
help='Short description of the file')
parser.add_option('-p', '--project', dest='project',
help='Google Code project name')
parser.add_option('-u', '--user', dest='user',
help='Your Google Code username')
parser.add_option('-w', '--password', dest='password',
help='Your Google Code password')
parser.add_option(
'-l',
'--labels',
dest='labels',
help='An optional list of comma-separated labels to attach '
'to the file')
options, args = parser.parse_args()
if not options.summary:
parser.error('File summary is missing.')
elif not options.project:
parser.error('Project name is missing.')
elif len(args) < 1:
parser.error('File to upload not provided.')
elif len(args) > 1:
parser.error('Only one file may be specified.')
file_path = args[0]
if options.labels:
labels = options.labels.split(',')
else:
labels = None
status, reason, url = upload_find_auth(file_path, options.project,
options.summary, labels,
options.user, options.password)
if url:
print 'The file was uploaded successfully.'
print 'URL: %s' % url
return 0
else:
print 'An error occurred. Your file was not uploaded.'
print 'Google Code upload server said: %s (%s)' % (reason, status)
return 1
if __name__ == '__main__':
sys.exit(main())

View File

@ -1,48 +0,0 @@
import locale
import logging
import os
import subprocess
import sys
def _lang_code_mac():
"""
stolen from https://github.com/mu-editor/mu
Returns the user's language preference as defined in the Language & Region
preference pane in macOS's System Preferences.
"""
# Uses the shell command `defaults read -g AppleLocale` that prints out a
# language code to standard output. Assumptions about the command:
# - It exists and is in the shell's PATH.
# - It accepts those arguments.
# - It returns a usable language code.
#
# Reference documentation:
# - The man page for the `defaults` command on macOS.
# - The macOS underlying API:
# https://developer.apple.com/documentation/foundation/nsuserdefaults.
LANG_DETECT_COMMAND = "defaults read -g AppleLocale"
status, output = subprocess.getstatusoutput(LANG_DETECT_COMMAND)
if status == 0:
# Command was successful.
lang_code = output
else:
logging.warning("Language detection command failed: %r", output)
lang_code = ""
return lang_code
def configure_locale():
if sys.platform == "darwin" and "LANG" not in os.environ:
code = _lang_code_mac()
if code != "":
os.environ["LANG"] = f"{code}.utf-8"
locale.setlocale(locale.LC_ALL, "")
sys.stdout.reconfigure(encoding=sys.getdefaultencoding())
sys.stderr.reconfigure(encoding=sys.getdefaultencoding())
sys.stdin.reconfigure(encoding=sys.getdefaultencoding())

View File

@ -1,3 +1,5 @@
#PYINSTALLER_CMD := VERSIONER_PYTHON_PREFER_32_BIT=yes arch -i386 python $(HOME)/pyinstaller-2.0/pyinstaller.py
#PYINSTALLER_CMD := python $(HOME)/pyinstaller-2.0/pyinstaller.py
PYINSTALLER_CMD := pyinstaller
TAGGER_BASE ?= ../
TAGGER_SRC := $(TAGGER_BASE)/comictaggerlib
@ -15,12 +17,21 @@ DMG_FILE := $(VOLUME_NAME).dmg
all: clean dist diskimage
dist:
$(PYINSTALLER_CMD) $(TAGGER_BASE)/comictagger.py -w -n $(APP_NAME) -s
#$(PYINSTALLER_CMD) $(TAGGER_BASE)/comictagger.py -o $(MAC_BASE) -w -n $(APP_NAME) -s
$(PYINSTALLER_CMD) $(TAGGER_BASE)/comictagger.py -w -n $(APP_NAME) -s
cp -a $(TAGGER_SRC)/ui $(APP_BUNDLE)/Contents/MacOS
cp -a $(TAGGER_SRC)/graphics $(APP_BUNDLE)/Contents/MacOS
cp $(MAC_BASE)/app.icns $(APP_BUNDLE)/Contents/Resources/icon-windowed.icns
# fix the version string in the Info.plist
sed -i -e 's/0\.0\.0/$(VERSION_STR)/' $(MAC_BASE)/dist/ComicTagger.app/Contents/Info.plist
# strip out PPC/x64
#./make_thin.sh dist/ComicTagger.app/Contents/MacOS
#./make_thin.sh dist/ComicTagger.app/Contents/MacOS/qt4_plugins/accessible
#./make_thin.sh dist/ComicTagger.app/Contents/MacOS/qt4_plugins/bearer
#./make_thin.sh dist/ComicTagger.app/Contents/MacOS/qt4_plugins/codecs
#./make_thin.sh dist/ComicTagger.app/Contents/MacOS/qt4_plugins/graphicssystems
#./make_thin.sh dist/ComicTagger.app/Contents/MacOS/qt4_plugins/iconengines
#./make_thin.sh dist/ComicTagger.app/Contents/MacOS/qt4_plugins/imageformats
clean:
rm -rf $(DIST_DIR) $(MAC_BASE)/build
@ -30,7 +41,7 @@ clean:
rm -f raw*.dmg
echo $(VERSION_STR)
diskimage:
# Set up disk image staging folder
#Set up disk image staging folder
rm -rf $(STAGING)
mkdir $(STAGING)
cp $(TAGGER_BASE)/release_notes.txt $(STAGING)
@ -39,28 +50,28 @@ diskimage:
cp $(MAC_BASE)/volume.icns $(STAGING)/.VolumeIcon.icns
SetFile -c icnC $(STAGING)/.VolumeIcon.icns
# generate raw disk image
##generate raw disk image
rm -f $(DMG_FILE)
hdiutil create -srcfolder $(STAGING) -volname $(VOLUME_NAME) -format UDRW -ov raw-$(DMG_FILE)
hdiutil create -srcfolder $(STAGING) -volname $(VOLUME_NAME) -format UDRW -ov raw-$(DMG_FILE)
# remove working files and folders
#remove working files and folders
rm -rf $(STAGING)
# we now have a raw DMG file.
# remount it so we can set the volume icon properly
mkdir -p $(STAGING)
hdiutil attach raw-$(DMG_FILE) -mountpoint $(STAGING)
SetFile -a C $(STAGING)
hdiutil detach $(STAGING)
rm -rf $(STAGING)
# convert the raw image
rm -f $(DMG_FILE)
hdiutil convert raw-$(DMG_FILE) -format UDZO -o $(DMG_FILE)
rm -f raw-$(DMG_FILE)
# move finished product to release folder
#move finished product to release folder
mkdir -p $(TAGGER_BASE)/release
mv $(DMG_FILE) $(TAGGER_BASE)/release

View File

@ -8,12 +8,12 @@ do
then
echo "Fat Binary: $FILE"
mkdir -p thin
lipo -thin i386 -output thin/$FILE $BINFOLDER/$FILE
lipo -thin i386 -output thin/$FILE $BINFOLDER/$FILE
fi
done
if [ -d thin ]
then
then
mv thin/* $BINFOLDER
else
echo No files to lipo

View File

@ -1,11 +1,8 @@
[tool.black]
line-length = 120
extend-exclude = "scripts/"
line-length = 150
[tool.isort]
line_length = 120
extend_skip = ["scripts"]
profile = "black"
line_length = 150
[build-system]
requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"]
@ -14,12 +11,3 @@ build-backend = "setuptools.build_meta"
[tool.setuptools_scm]
write_to = "comictaggerlib/ctversion.py"
local_scheme = "no-local-version"
[tool.pylint.messages_control]
disable = "C0330, C0326, C0115, C0116, C0103"
[tool.pylint.format]
max-line-length=120
[tool.pylint.master]
extension-pkg-whitelist="PyQt5"

View File

@ -1,221 +1,221 @@
---------------------------------
1.1.16-beta-rc - 07-Apr-2017
---------------------------------
* Fix ComicVine SSL problems (issue #87)
---------------------------------
1.1.15-beta - 13-Jun-2014
---------------------------------
* WebP support
* Added user-configurable API key for Comic Vine access
* Experimental option to wait and retry after exceeding Comic Vine rate limit
---------------------------------
1.1.14-beta - 13-Apr-2014
---------------------------------
* Make sure app gets raised when enforcing single instance
* Added warning dialog for when opening rar files, and no (un)rar tool
* remove pil from python package requirements
---------------------------------
1.1.13-beta - 9-Apr-2014
---------------------------------
* Handle non-ascii user names properly
* better parsing of html table in summary text, and optional removal
* Python package should auto-install requirements
* Specify default GUI tag style on command-line
* enforce single GUI instance
* new CBL transform to copy story arcs to generic tags
* Persist some auto-tag settings
---------------------------------
1.1.12-beta - 23-Mar-2014
---------------------------------
* Fixed noisy version update error
---------------------------------
1.1.11-beta - 23-Mar-2014
---------------------------------
* Updated unrar library to hand Rar tools 5.0 and greater
* Other misc bug fixes
---------------------------------
1.1.10-beta - 30-Jan-2014
---------------------------------
* Updated series query to match changes on Comic Vine side
* Added a message when not able to open a file or folder
* Fixed an issue where series names with periods would fail on search
* Other misc bug fixes
---------------------------------
1.1.9-beta - 8-May-2013
---------------------------------
* Filename parser and identification enhancements
* Misc bug fixes
---------------------------------
1.1.8-beta - 21-Apr-2013
---------------------------------
* Handle occasional error 500 from Comic Vine by retrying a few times
* Nicer handling of colon (":") in file rename
* Fixed command-line option parsing issue for add-on scripts
* Misc bug fixes
---------------------------------
1.1.7-beta - 12-Apr-2013
---------------------------------
* Added description and cover date to issue selection dialogs
* Added notification of new version
* Added setting to attempt to parse scan info from file name
* Last sorted column in the file list is now remembered
* Added CLI option ('-1') to assume issue #1 if not found/parsed
* Misc bug fixes
---------------------------------
1.1.6-beta - 3-Apr-2013
---------------------------------
* More ComicVine API-related fixes
* More efficient automated search using new CV API issue filters
* Minor bug fixes
---------------------------------
1.1.5-beta - 30-Mar-2013
---------------------------------
* More updates for handling changes to ComicVine API and result sets
* Even better handling of non-numeric issue "numbers" ("½", "X")
---------------------------------
1.1.4-beta - 27-Mar-2013
---------------------------------
* Updated to match the changes to the ComicVine API and result sets
* Better handling of weird issue numbers ("0.1", "6au")
---------------------------------
1.1.3-beta - 25-Feb-2013
---------------------------------
Bug Fixes:
* Fixed a bug when renaming on non-English systems
* Fixed issue when saving settings on non-English systems
* Fixed a bug when comic contains non-RGB images
* Fixed a rare crash when comic image is not-RGB format
* Fixed sequence order of ComicInfo.xml items
Note:
New requirement for users of the python package: "configparser"
---------------------------------
1.1.2-beta - 14-Feb-2013
---------------------------------
Changes:
* Source is now packaged using Python distutils
* Recursive mode for CLI
* Run custom add-on scripts from CLI
* Minor UI tweaks
* Misc bug fixes
---------------------------------
1.1.0-beta - 06-Feb-2013
---------------------------------
Changes:
* Enhanced identification process to use alternative covers from ComicVine
* Post auto-tag manual matching now includes single low-confidence matches (CLI & GUI)
* Page and cover view mini-browser available throughout app. Most images can be
double-clicked for enlarged view
* Export-to-zip in CLI (very handy in scripts!)
* More rename template variables
* Misc GUI & CLI Tweaks
---------------------------------
1.0.3-beta - 31-Jan-2013
---------------------------------
Changes:
Misc bug fixes and enhancements
---------------------------------
1.0.2-beta - 25-Jan-2013
---------------------------------
Changes:
More verbose logging during auto-tag
Added %month% and %month_name% for renaming
Better parsing of volume numbers in file name
Bugs:
Better exception handling with corrupted image data
Fixed issues with RAR reading on OS X
Other minor bug fixes
---------------------------------
1.0.1-beta - 24-Jan-2013
---------------------------------
Bug Fix:
Fixed an issue where unicode strings can't be printed to OS X Console
---------------------------------
1.0.0-beta - 23-Jan-2013
---------------------------------
Version 1! New multi-file processing in GUI!
GUI Changes:
Open multiple files and/or folders via drag/drop or file dialog
File management list for easy viewing and selection
Batch tag remove
Batch export as zip
Batch rename
Batch tag copy
Batch auto-tag (automatic identification and save!)
---------------------------------
0.9.5-beta - 16-Jan-2013
---------------------------------
Changes:
Added CLI option to search by Comic Vine issue ID
Some image loading optimizations
Bug Fix: Some CBL fields that should have been ints were written as strings
---------------------------------
0.9.4-beta - 7-Jan-2013
---------------------------------
Changes:
Better handling of non-ascii characters in file names and data
Add CBL Transform to copy Web Link and Notes to comments
Minor bug fixes
---------------------------------
0.9.3-beta - 19-Dec-2012
---------------------------------
Changes:
File rename in GUI
Setting for file rename
Option to use series start year as volume
Added "CBL Transform" to handle primary credits copying data into the generic tags field
Bug Fix: unicode characters in credits caused crash
Bug Fix: bad or non-image data in file caused crash
Note:
The user should clear the cache and delete the existing settings when first running this version.
---------------------------------
0.9.2-beta - 13-Dec-2012
---------------------------------
Page List/Type editing in GUI
File globbing for windows CLI (i.e. use of wildcards like '*.cbz')
Fixed RAR writing bug on windows
Minor bug and crash fixes
---------------------------------
0.9.1-beta - 07-Dec-2012
---------------------------------
Export as ZIP Archive
Added help menu option for websites
Added Primary Credit Flag editing
Menu enhancements
CLI Enhancements:
Interactive selection of matches
Tag copy
Better output
CoMet support
Minor bug and crash fixes
---------------------------------
0.9.0-beta - 30-Nov-2012
---------------------------------
Initial beta release
---------------------------------
1.1.16-beta-rc - 07-Apr-2017
---------------------------------
* Fix ComicVine SSL problems (issue #87)
---------------------------------
1.1.15-beta - 13-Jun-2014
---------------------------------
* WebP support
* Added user-configurable API key for Comic Vine access
* Experimental option to wait and retry after exceeding Comic Vine rate limit
---------------------------------
1.1.14-beta - 13-Apr-2014
---------------------------------
* Make sure app gets raised when enforcing single instance
* Added warning dialog for when opening rar files, and no (un)rar tool
* remove pil from python package requirements
---------------------------------
1.1.13-beta - 9-Apr-2014
---------------------------------
* Handle non-ascii user names properly
* better parsing of html table in summary text, and optional removal
* Python package should auto-install requirements
* Specify default GUI tag style on command-line
* enforce single GUI instance
* new CBL transform to copy story arcs to generic tags
* Persist some auto-tag settings
---------------------------------
1.1.12-beta - 23-Mar-2014
---------------------------------
* Fixed noisy version update error
---------------------------------
1.1.11-beta - 23-Mar-2014
---------------------------------
* Updated unrar library to hand Rar tools 5.0 and greater
* Other misc bug fixes
---------------------------------
1.1.10-beta - 30-Jan-2014
---------------------------------
* Updated series query to match changes on Comic Vine side
* Added a message when not able to open a file or folder
* Fixed an issue where series names with periods would fail on search
* Other misc bug fixes
---------------------------------
1.1.9-beta - 8-May-2013
---------------------------------
* Filename parser and identification enhancements
* Misc bug fixes
---------------------------------
1.1.8-beta - 21-Apr-2013
---------------------------------
* Handle occasional error 500 from Comic Vine by retrying a few times
* Nicer handling of colon (":") in file rename
* Fixed command-line option parsing issue for add-on scripts
* Misc bug fixes
---------------------------------
1.1.7-beta - 12-Apr-2013
---------------------------------
* Added description and cover date to issue selection dialogs
* Added notification of new version
* Added setting to attempt to parse scan info from file name
* Last sorted column in the file list is now remembered
* Added CLI option ('-1') to assume issue #1 if not found/parsed
* Misc bug fixes
---------------------------------
1.1.6-beta - 3-Apr-2013
---------------------------------
* More ComicVine API-related fixes
* More efficient automated search using new CV API issue filters
* Minor bug fixes
---------------------------------
1.1.5-beta - 30-Mar-2013
---------------------------------
* More updates for handling changes to ComicVine API and result sets
* Even better handling of non-numeric issue "numbers" ("½", "X")
---------------------------------
1.1.4-beta - 27-Mar-2013
---------------------------------
* Updated to match the changes to the ComicVine API and result sets
* Better handling of weird issue numbers ("0.1", "6au")
---------------------------------
1.1.3-beta - 25-Feb-2013
---------------------------------
Bug Fixes:
* Fixed a bug when renaming on non-English systems
* Fixed issue when saving settings on non-English systems
* Fixed a bug when comic contains non-RGB images
* Fixed a rare crash when comic image is not-RGB format
* Fixed sequence order of ComicInfo.xml items
Note:
New requirement for users of the python package: "configparser"
---------------------------------
1.1.2-beta - 14-Feb-2013
---------------------------------
Changes:
* Source is now packaged using Python distutils
* Recursive mode for CLI
* Run custom add-on scripts from CLI
* Minor UI tweaks
* Misc bug fixes
---------------------------------
1.1.0-beta - 06-Feb-2013
---------------------------------
Changes:
* Enhanced identification process to use alternative covers from ComicVine
* Post auto-tag manual matching now includes single low-confidence matches (CLI & GUI)
* Page and cover view mini-browser available throughout app. Most images can be
double-clicked for enlarged view
* Export-to-zip in CLI (very handy in scripts!)
* More rename template variables
* Misc GUI & CLI Tweaks
---------------------------------
1.0.3-beta - 31-Jan-2013
---------------------------------
Changes:
Misc bug fixes and enhancements
---------------------------------
1.0.2-beta - 25-Jan-2013
---------------------------------
Changes:
More verbose logging during auto-tag
Added %month% and %month_name% for renaming
Better parsing of volume numbers in file name
Bugs:
Better exception handling with corrupted image data
Fixed issues with RAR reading on OS X
Other minor bug fixes
---------------------------------
1.0.1-beta - 24-Jan-2013
---------------------------------
Bug Fix:
Fixed an issue where unicode strings can't be printed to OS X Console
---------------------------------
1.0.0-beta - 23-Jan-2013
---------------------------------
Version 1! New multi-file processing in GUI!
GUI Changes:
Open multiple files and/or folders via drag/drop or file dialog
File management list for easy viewing and selection
Batch tag remove
Batch export as zip
Batch rename
Batch tag copy
Batch auto-tag (automatic identification and save!)
---------------------------------
0.9.5-beta - 16-Jan-2013
---------------------------------
Changes:
Added CLI option to search by Comic Vine issue ID
Some image loading optimizations
Bug Fix: Some CBL fields that should have been ints were written as strings
---------------------------------
0.9.4-beta - 7-Jan-2013
---------------------------------
Changes:
Better handling of non-ascii characters in file names and data
Add CBL Transform to copy Web Link and Notes to comments
Minor bug fixes
---------------------------------
0.9.3-beta - 19-Dec-2012
---------------------------------
Changes:
File rename in GUI
Setting for file rename
Option to use series start year as volume
Added "CBL Transform" to handle primary credits copying data into the generic tags field
Bug Fix: unicode characters in credits caused crash
Bug Fix: bad or non-image data in file caused crash
Note:
The user should clear the cache and delete the existing settings when first running this version.
---------------------------------
0.9.2-beta - 13-Dec-2012
---------------------------------
Page List/Type editing in GUI
File globbing for windows CLI (i.e. use of wildcards like '*.cbz')
Fixed RAR writing bug on windows
Minor bug and crash fixes
---------------------------------
0.9.1-beta - 07-Dec-2012
---------------------------------
Export as ZIP Archive
Added help menu option for websites
Added Primary Credit Flag editing
Menu enhancements
CLI Enhancements:
Interactive selection of matches
Tag copy
Better output
CoMet support
Minor bug and crash fixes
---------------------------------
0.9.0-beta - 30-Nov-2012
---------------------------------
Initial beta release

View File

@ -1 +1 @@
PyQt5
PyQt5<=5.15.3

View File

@ -1,6 +1,7 @@
beautifulsoup4 >= 4.1
natsort>=8.1.0
PyPDF2==1.24
configparser
natsort
pillow>=4.3.0
requests==2.*
pycountry
py7zr
requests
pathvalidate

View File

@ -1,7 +1,4 @@
pyinstaller>=4.10
pyinstaller==4.3
setuptools>=42
setuptools_scm[toml]>=3.4
wheel
black>=22
flake8==4.*
isort>=5.10

View File

@ -1,28 +1,28 @@
This folder contains a set of example scripts that be used to extend the
capabilities of the ComicTagger app. They can be run either directly through
the python interpreter, or via the ComicTagger app.
To run via python directly, install ComicTagger source on your system using
the setup.py file.
To run via the ComicTagger app, invoke:
$ comictagger.py -S script.py [script args]
(This will work also for binary distributions on Mac and Windows. No need for
an extra python install.)
The script must have an entry point function called "main()" to be invoked
via the app.
-----------------------------------------------------------------------------
This feature is UNSUPPORTED, and is for the convenience of development-minded
users of ComicTagger. The comictaggerlib module will remain largely
undocumented, and it will be up to the crafty script developer to look through
the code to discern APIs and such.
That said, if there are questions, please post in the forums, and hopefully we
can get your add-on scripts working!
http://comictagger.forumotion.com/
This folder contains a set of example scripts that be used to extend the
capabilities of the ComicTagger app. They can be run either directly through
the python interpreter, or via the ComicTagger app.
To run via python directly, install ComicTagger source on your system using
the setup.py file.
To run via the ComicTagger app, invoke:
$ comictagger.py -S script.py [script args]
(This will work also for binary distributions on Mac and Windows. No need for
an extra python install.)
The script must have an entry point function called "main()" to be invoked
via the app.
-----------------------------------------------------------------------------
This feature is UNSUPPORTED, and is for the convenience of development-minded
users of ComicTagger. The comictaggerlib module will remain largely
undocumented, and it will be up to the crafty script developer to look through
the code to discern APIs and such.
That said, if there are questions, please post in the forums, and hopefully we
can get your add-on scripts working!
http://comictagger.forumotion.com/

View File

@ -1,13 +1,12 @@
#!/usr/bin/python
"""Find all duplicate comics"""
# import sys
#import sys
from comictaggerlib.comicarchive import *
from comictaggerlib.settings import *
# from comictaggerlib.issuestring import *
# import comictaggerlib.utils
#from comictaggerlib.issuestring import *
#import comictaggerlib.utils
def main():
@ -43,13 +42,13 @@ def main():
# sort the list by series+issue+year, to put all the dupes together
def makeKey(x):
return "<" + unicode(x[1].series) + u" #" + unicode(x[1].issue) + u" - " + unicode(x[1].year) + ">"
return "<" + unicode(x[1].series) + u" #" + \
unicode(x[1].issue) + u" - " + unicode(x[1].year) + ">"
comic_list.sort(key=makeKey, reverse=False)
# look for duplicate blocks
dupe_set_list = []
dupe_set = []
dupe_set_list = list()
dupe_set = list()
prev_key = ""
for filename, md in comic_list:
print >> sys.stderr, fmt_str.format(filename) + "\r",
@ -66,7 +65,7 @@ def main():
# only add if the dupe list has 2 or more
if len(dupe_set) > 1:
dupe_set_list.append(dupe_set)
dupe_set = []
dupe_set = list()
dupe_set.append(filename)
prev_key = new_key
@ -81,6 +80,5 @@ def main():
for filename in dupe_set:
print "------>{0}".format(filename)
if __name__ == "__main__":
if __name__ == '__main__':
main()

View File

@ -15,14 +15,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# import sys
# import os
#import sys
#import os
from comictaggerlib.comicarchive import *
from comictaggerlib.issuestring import *
from comictaggerlib.settings import *
# import comictaggerlib.utils
from comictaggerlib.issuestring import *
#import comictaggerlib.utils
def main():
@ -70,14 +69,15 @@ def main():
fmt_str = u"{0:" + str(w0) + "} {1:" + str(w1) + "} #{2:6} ({3})"
# now sort the list by issue, and then series
metadata_list.sort(key=lambda x: IssueString(x[1].issue).asString(3), reverse=False)
metadata_list.sort(key=lambda x: unicode(x[1].series).lower() + str(x[1].year), reverse=False)
metadata_list.sort(
key=lambda x: IssueString(x[1].issue).asString(3), reverse=False)
metadata_list.sort(
key=lambda x: unicode(x[1].series).lower() + str(x[1].year), reverse=False)
# now print
for filename, md in metadata_list:
if not md.isEmpty:
print fmt_str.format(os.path.split(filename)[1] + ":", md.series, md.issue, md.year), md.title
if __name__ == "__main__":
if __name__ == '__main__':
main()

View File

@ -18,15 +18,14 @@ organizing by date and series, in different trees
# See the License for the specific language governing permissions and
# limitations under the License.
# import sys
# import os
# import platform
#import sys
#import os
#import platform
from comictaggerlib.comicarchive import *
from comictaggerlib.settings import *
# from comictaggerlib.issuestring import *
# import comictaggerlib.utils
#from comictaggerlib.issuestring import *
#import comictaggerlib.utils
def make_folder(folder):
@ -53,7 +52,8 @@ def main():
print >> sys.stderr, "Sorry, this script works only on UNIX systems"
if len(sys.argv) < 3:
print >> sys.stderr, "Usage: {0} [comic_root][link_root]".format(sys.argv[0])
print >> sys.stderr, "Usage: {0} [comic_root][link_root]".format(
sys.argv[0])
return
comic_root = sys.argv[1]
@ -93,7 +93,8 @@ def main():
month_str = "00"
date_folder = os.path.join(link_root, "date", str(md.year), month_str)
make_folder(date_folder)
make_link(filename, os.path.join(date_folder, os.path.basename(filename)))
make_link(
filename, os.path.join(date_folder, os.path.basename(filename)))
# do publisher/series organizing:
fixed_series_name = md.series
@ -101,10 +102,11 @@ def main():
# some tweaks to keep various filesystems happy
fixed_series_name = fixed_series_name.replace("/", "-")
fixed_series_name = fixed_series_name.replace("?", "")
series_folder = os.path.join(link_root, "series", str(md.publisher), unicode(fixed_series_name))
series_folder = os.path.join(
link_root, "series", str(md.publisher), unicode(fixed_series_name))
make_folder(series_folder)
make_link(filename, os.path.join(series_folder, os.path.basename(filename)))
make_link(filename, os.path.join(
series_folder, os.path.basename(filename)))
if __name__ == "__main__":
if __name__ == '__main__':
main()

View File

@ -18,17 +18,14 @@
# limitations under the License.
import shutil
#import sys
#import os
#import platform
from comicapi.comicarchive import *
from comictaggerlib.settings import *
# import sys
# import os
# import platform
# from comicapi.issuestring import *
# import comicapi.utils
from comicapi.comicarchive import *
#from comicapi.issuestring import *
#import comicapi.utils
def make_folder(folder):
@ -55,7 +52,8 @@ def main():
print >> sys.stderr, "Sorry, this script works only on UNIX systems"
if len(sys.argv) < 3:
print >> sys.stderr, "Usage: {0} [comic_root][tree_root]".format(sys.argv[0])
print >> sys.stderr, "Usage: {0} [comic_root][tree_root]".format(
sys.argv[0])
return
comic_root = sys.argv[1]
@ -80,7 +78,7 @@ def main():
max_name_len = 2
fmt_str = ""
for filename in filelist:
ca = ComicArchive(filename, settings.rar_exe_path, ComicTaggerSettings.getGraphic("nocover.png"))
ca = ComicArchive(filename, settings.rar_exe_path, ComicTaggerSettings.getGraphic('nocover.png'))
if ca.seemsToBeAComicArchive() and ca.hasMetadata(style):
comic_list.append((filename, ca.readMetadata(style)))
@ -109,11 +107,12 @@ def main():
series_name = series_name.replace("/", "-")
series_name = series_name.replace("?", "")
series_folder = os.path.join(
tree_root, unicode(publisher_name), unicode(series_name) + " (" + unicode(start_year) + ")"
)
tree_root,
unicode(publisher_name),
unicode(series_name) + " (" + unicode(start_year) + ")")
make_folder(series_folder)
move_file(filename, os.path.join(series_folder, os.path.basename(filename)))
move_file(filename, os.path.join(
series_folder, os.path.basename(filename)))
if __name__ == "__main__":
if __name__ == '__main__':
main()

View File

@ -17,27 +17,34 @@
import argparse
import json
#import sys
#import os
#import re
from comictaggerlib.comicarchive import *
from comictaggerlib.filerenamer import *
from comictaggerlib.settings import *
# import sys
# import os
# import re
# import comictaggerlib.utils
from comictaggerlib.filerenamer import *
#import comictaggerlib.utils
def parse_args():
input_args = sys.argv[1:]
parser = argparse.ArgumentParser(description="A script to rename comic files")
parser.add_argument("-t", "--transforms", metavar="xformfile", help="The file with transforms")
parser.add_argument("-n", "--noconfirm", action="store_true", help="Don't confirm before rename")
parser.add_argument("paths", metavar="PATH", type=str, nargs="+", help="path to look for comic files")
parser = argparse.ArgumentParser(
description='A script to rename comic files')
parser.add_argument(
'-t',
'--transforms',
metavar='xformfile',
help="The file with transforms")
parser.add_argument(
'-n',
'--noconfirm',
action='store_true',
help="Don't confirm before rename")
parser.add_argument('paths', metavar='PATH', type=str,
nargs='+', help='path to look for comic files')
parsed_args = parser.parse_args(input_args)
return parsed_args
@ -53,7 +60,8 @@ def calculate_rename(ca, md, settings):
new_ext = ".cbr"
renamer = FileRenamer(md)
renamer.setTemplate("%series% V%volume% #%issue% (of %issuecount%) (%year%) %scaninfo%")
renamer.setTemplate(
"%series% V%volume% #%issue% (of %issuecount%) (%year%) %scaninfo%")
renamer.setIssueZeroPadding(0)
renamer.setSmartCleanup(settings.rename_use_smart_string_cleanup)
@ -88,11 +96,11 @@ def main():
print "Reading in transforms from:", parsed_args.transforms
json_data = open(parsed_args.transforms).read()
data = json.loads(json_data)
xform_list = data["xforms"]
xform_list = data['xforms']
else:
xform_list = default_xform_list
# pprint( xform_list, indent=4)
#pprint( xform_list, indent=4)
filelist = utils.get_recursive_filelist(parsed_args.paths)
@ -116,7 +124,7 @@ def main():
print >> sys.stderr, fmt_str.format("")
print "Found {0} comics.".format(len(comic_list))
modify_list = []
modify_list = list()
# walk through the comic list fix the file names
for ca in comic_list:
@ -145,12 +153,11 @@ def main():
print u"'{0}' -> '{1}'".format(os.path.basename(old_name), new_name)
i = raw_input("Do you want to proceed with rename? [y/N] ")
if i.lower() not in ("y", "yes"):
if i.lower() not in ('y', 'yes'):
print "exiting without rename."
sys.exit(0)
perform_rename(modify_list)
if __name__ == "__main__":
if __name__ == '__main__':
main()

View File

@ -19,18 +19,18 @@ are kept in a sub-folder at the level of the original
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import shutil
import sys
import os
import tempfile
import zipfile
import shutil
import comictaggerlib.utils
from comictaggerlib.comicarchive import *
from comictaggerlib.settings import *
from comictaggerlib.comicarchive import *
subfolder_name = "PRE_AD_REMOVAL"
unwanted_types = ["Deleted", "Advertisement"]
unwanted_types = ['Deleted', 'Advertisement']
def main():
@ -55,7 +55,7 @@ def main():
md = ca.readMetadata(style)
if len(md.pages) != 0:
for p in md.pages:
if "Type" in p and p["Type"] in unwanted_types:
if 'Type' in p and p['Type'] in unwanted_types:
# This one has pages to remove. add to list!
modify_list.append((filename, md))
break
@ -75,7 +75,8 @@ def main():
if not os.access(filename, os.W_OK):
print "Can't move: {0}: skipped!".format(filename)
continue
if not os.path.exists(curr_subfolder) and not os.access(curr_folder, os.W_OK):
if not os.path.exists(curr_subfolder) and not os.access(
curr_folder, os.W_OK):
print "Can't create subfolder here: {0}: skipped!".format(filename)
continue
if not os.path.exists(curr_subfolder):
@ -89,20 +90,20 @@ def main():
os.close(tmp_fd)
try:
zout = zipfile.ZipFile(tmp_name, "w")
zout = zipfile.ZipFile(tmp_name, 'w')
# now read in all the pages from the old one, except the ones we
# want to skip
new_num = 0
new_pages = []
new_pages = list()
for p in md.pages:
if "Type" in p and p["Type"] in unwanted_types:
if 'Type' in p and p['Type'] in unwanted_types:
continue
else:
pageNum = int(p["Image"])
pageNum = int(p['Image'])
name = ca.getPageName(pageNum)
buffer = ca.getPage(pageNum)
sys.stdout.write(".")
sys.stdout.write('.')
sys.stdout.flush()
# Generate a new name for the page file
@ -111,10 +112,10 @@ def main():
zout.writestr(new_name, buffer)
# create new page entry
new_p = {}
new_p["Image"] = str(new_num)
if "Type" in p:
new_p["Type"] = p["Type"]
new_p = dict()
new_p['Image'] = str(new_num)
if 'Type' in p:
new_p['Type'] = p['Type']
new_pages.append(new_p)
new_num += 1
@ -146,5 +147,5 @@ def main():
ca.writeMetadata(style, md)
if __name__ == "__main__":
if __name__ == '__main__':
main()

View File

@ -16,19 +16,16 @@
# limitations under the License.
import shutil
#import sys
#import os
#import tempfile
#import zipfile
import Image
from comictaggerlib.comicarchive import *
from comictaggerlib.settings import *
# import sys
# import os
# import tempfile
# import zipfile
# import comictaggerlib.utils
from comictaggerlib.comicarchive import *
#import comictaggerlib.utils
subfolder_name = "ORIGINALS"
@ -55,7 +52,7 @@ def main():
for filename in filelist:
ca = ComicArchive(filename, settings.rar_exe_path)
if ca.seemsToBeAComicArchive():
if (ca.seemsToBeAComicArchive()):
# Check the images in the file, see if we need to reduce any
for idx in range(ca.getNumberOfPages()):
@ -69,7 +66,8 @@ def main():
max_name_len = max(max_name_len, len(filename))
fmt_str = u"{{0:{0}}}".format(max_name_len)
print >> sys.stderr, fmt_str.format(filename) + "\r",
print >> sys.stderr, fmt_str.format(
filename) + "\r",
sys.stderr.flush()
break
@ -101,7 +99,8 @@ def main():
if not os.access(filename, os.W_OK):
print "Can't move: {0}: skipped!".format(filename)
continue
if not os.path.exists(curr_subfolder) and not os.access(curr_folder, os.W_OK):
if not os.path.exists(curr_subfolder) and not os.access(
curr_folder, os.W_OK):
print "Can't create subfolder here: {0}: skipped!".format(filename)
continue
if not os.path.exists(curr_subfolder):
@ -119,7 +118,7 @@ def main():
cix_md = ca.readCIX()
try:
zout = zipfile.ZipFile(tmp_name, "w")
zout = zipfile.ZipFile(tmp_name, 'w')
# Check the images in the file, see if we want to reduce them
page_count = ca.getNumberOfPages()
@ -134,7 +133,7 @@ def main():
w, h = im.size
if h > max_height:
# resize the image
hpercent = max_height / float(h)
hpercent = (max_height / float(h))
wsize = int((float(w) * float(hpercent)))
size = (wsize, max_height)
im = im.resize(size, Image.ANTIALIAS)
@ -152,7 +151,7 @@ def main():
# page is empty?? nothing to write
out_data = ""
sys.stdout.write(".")
sys.stdout.write('.')
sys.stdout.flush()
# write out the new resized image
@ -187,5 +186,5 @@ def main():
ca.writeCIX(cix_md)
if __name__ == "__main__":
if __name__ == '__main__':
main()

View File

@ -16,15 +16,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# import sys
# import os
#import sys
#import os
from comictaggerlib.comicarchive import *
from comictaggerlib.comicvinetalker import *
from comictaggerlib.issueidentifier import *
from comictaggerlib.settings import *
# import comictaggerlib.utils
from comictaggerlib.comicarchive import *
from comictaggerlib.issueidentifier import *
from comictaggerlib.comicvinetalker import *
#import comictaggerlib.utils
def main():
@ -33,7 +32,8 @@ def main():
settings = ComicTaggerSettings()
if len(sys.argv) < 3:
print >> sys.stderr, "Usage: {0} [comicfile][issueid]".format(sys.argv[0])
print >> sys.stderr, "Usage: {0} [comicfile][issueid]".format(
sys.argv[0])
return
filename = sys.argv[1]
@ -45,7 +45,8 @@ def main():
ca = ComicArchive(filename, settings.rar_exe_path)
if not ca.seemsToBeAComicArchive():
print >> sys.stderr, "Sorry, but " + filename + " is not a comic archive!"
print >> sys.stderr, "Sorry, but " + \
filename + " is not a comic archive!"
return
ii = IssueIdentifier(ca, settings)
@ -58,15 +59,16 @@ def main():
hash_list = [cover_hash0, cover_hash1]
comicVine = ComicVineTalker()
result = ii.getIssueCoverMatchScore(comicVine, issue_id, hash_list, useRemoteAlternates=True, useLog=False)
result = ii.getIssueCoverMatchScore(
comicVine, issue_id, hash_list, useRemoteAlternates=True, useLog=False)
print "Best cover match score is:", result["score"]
if result["score"] < ii.min_alternate_score_thresh:
print "Best cover match score is:", result['score']
if result['score'] < ii.min_alternate_score_thresh:
print "Looks like a match!"
else:
print "Bad score, maybe not a match?"
print result["url"]
print result['url']
if __name__ == "__main__":
if __name__ == '__main__':
main()

162
setup.py
View File

@ -1,81 +1,81 @@
# Setup file for comictagger python source (no wheels yet)
#
# An entry point script called "comictagger" will be created
#
# Currently commented out, an experiment at desktop integration.
# It seems that post installation tweaks are broken by wheel files.
# Kept here for further research
import glob
import os
from setuptools import setup
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()
install_requires = read("requirements.txt").splitlines()
# 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 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})
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",
)
# Setup file for comictagger python source (no wheels yet)
#
# An entry point script called "comictagger" will be created
#
# Currently commented out, an experiment at desktop integration.
# It seems that post installation tweaks are broken by wheel files.
# Kept here for further research
import glob
import os
from setuptools import setup
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()
install_requires = read("requirements.txt").splitlines()
# 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 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})
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'
)

View File

@ -14,25 +14,25 @@ Maybe replace configparser -- seems to be causing all sorts of problems
Feature Requests:
Move CBR to other folder after conversion to ZIP
Pre-process series name before identification
(using a list of regex transforms)
(using a list of regex transforms)
(GC #24) Multiple options for -t i.e. "-t cr,cbl"
(GC #18 ) Option for handling colon in rename
(GC #31 ) Specify CV Series ID for auto-tag
Re-org - move to new folder based on template
Denied Requests (for now):
Auto-rename on auto-tag
Re-zip (to remove compression)
Selective fields on CLI print (use -m option. Maybe internally remove all but specified fields in MD object before print )
Docs:
Auto-Tagging Tips:
Multiple Passes with different options
Multiple Passes with different options
-----------------------------------------------------
Bugs
Bugs
-----------------------------------------------------
Zip flakes out when filename differs from index (or whatever) i.e "\" vs "/". Python issue
@ -48,14 +48,14 @@ GCD scraper or DB reader
(GC #29) Batch Edit
Form Mode: Single vs Batch
-----------------------------------------------------
Small(er) Future Feature
-----------------------------------------------------
Parse out the rest of the scan info from filename
Style sheets for windows/mac/linux
CLI
explicit metadata settings option format
-- figure out how to add CBI "tags"
@ -68,7 +68,7 @@ Archive function to detect tag blocks out of sync
Settings
Add setting to dis-allow writing CBI to RAR
Google App engine to store hashes
Google App engine to store hashes
Content Hashes, Image hashes, who knows?
Filename parsing:
@ -91,12 +91,12 @@ Release Process
Tag the repository
Manually upload release packages to Google Drive
Update the Downloads wiki page with direct links
"make upload" to the pypi site
"make upload" to the pypi site
Announce on Forum and Main Page and Twitter and Facebook
MacUpdate
Update appspot value
----------------------------------------------
rename 's/([A-Za-z]+)(\d+)(.cb[rz])/$1 $2$3/' *.cb?