Compare commits
2 Commits
1.3.2-alph
...
seriesYear
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50c53e14da | ||
|
|
2e01a4db14 |
4
.flake8
4
.flake8
@@ -1,4 +0,0 @@
|
||||
[flake8]
|
||||
max-line-length = 120
|
||||
extend-ignore = E203, E501, E722
|
||||
extend-exclude = venv, scripts
|
||||
17
Makefile
17
Makefile
@@ -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
100
README.md
@@ -1,50 +1,50 @@
|
||||
[](https://travis-ci.org/comictagger/comictagger)
|
||||
[](https://gitter.im/comictagger/community)
|
||||
[](https://groups.google.com/forum/#!forum/comictagger)
|
||||
[](https://twitter.com/comictagger)
|
||||
[](https://www.facebook.com/ComicTagger-139615369550787/)
|
||||
|
||||
# ComicTagger
|
||||
|
||||
ComicTagger is a **multi-platform** app for **writing metadata to digital comics**, written in Python and PyQt.
|
||||
|
||||

|
||||
|
||||
## 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`
|
||||
[](https://travis-ci.org/comictagger/comictagger)
|
||||
[](https://gitter.im/comictagger/community)
|
||||
[](https://groups.google.com/forum/#!forum/comictagger)
|
||||
[](https://twitter.com/comictagger)
|
||||
[](https://www.facebook.com/ComicTagger-139615369550787/)
|
||||
|
||||
# ComicTagger
|
||||
|
||||
ComicTagger is a **multi-platform** app for **writing metadata to digital comics**, written in Python and PyQt.
|
||||
|
||||

|
||||
|
||||
## 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`
|
||||
|
||||
@@ -1 +1 @@
|
||||
__author__ = "dromanin"
|
||||
__author__ = 'dromanin'
|
||||
|
||||
@@ -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
@@ -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", encoding="utf-8") as f:
|
||||
f.write(json.dumps(cbi_container, indent=4))
|
||||
f = open(filename, 'w')
|
||||
f.write(json.dumps(cbi_container, indent=4))
|
||||
f.close
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 and not re.search(r"\[__\d+__\]", 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
|
||||
|
||||
@@ -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,85 +42,74 @@ class PageType:
|
||||
Other = "Other"
|
||||
Deleted = "Deleted"
|
||||
|
||||
|
||||
class ImageMetadata(TypedDict):
|
||||
Type: PageType
|
||||
Bookmark: str
|
||||
DoublePage: bool
|
||||
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
|
||||
@@ -141,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)
|
||||
@@ -178,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
|
||||
@@ -197,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
|
||||
|
||||
@@ -261,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 1⁄2 not 1/2
|
||||
# this will probably cause issues with titles in other character sets e.g. chinese, japanese
|
||||
text = unicodedata.normalize("NFKD", text).encode("ascii", "ignore").decode("ascii")
|
||||
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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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!"))
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
1
comictaggerlib/comet.py
Normal file
@@ -0,0 +1 @@
|
||||
from comicapi.comet import *
|
||||
1
comictaggerlib/comicarchive.py
Normal file
1
comictaggerlib/comicarchive.py
Normal file
@@ -0,0 +1 @@
|
||||
from comicapi.comicarchive import *
|
||||
1
comictaggerlib/comicbookinfo.py
Normal file
1
comictaggerlib/comicbookinfo.py
Normal file
@@ -0,0 +1 @@
|
||||
from comicapi.comicbookinfo import *
|
||||
1
comictaggerlib/comicinfoxml.py
Normal file
1
comictaggerlib/comicinfoxml.py
Normal file
@@ -0,0 +1 @@
|
||||
from comicapi.comicinfoxml import *
|
||||
@@ -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", encoding="utf-8") 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, "wb").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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
1
comictaggerlib/filenameparser.py
Normal file
1
comictaggerlib/filenameparser.py
Normal file
@@ -0,0 +1 @@
|
||||
from comicapi.filenameparser import *
|
||||
@@ -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()))
|
||||
|
||||
@@ -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)
|
||||
|
||||
1
comictaggerlib/genericmetadata.py
Normal file
1
comictaggerlib/genericmetadata.py
Normal file
@@ -0,0 +1 @@
|
||||
from comicapi.genericmetadata import *
|
||||
@@ -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, "wb").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
|
||||
|
||||
@@ -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:])
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
1
comictaggerlib/issuestring.py
Normal file
1
comictaggerlib/issuestring.py
Normal file
@@ -0,0 +1 @@
|
||||
from comicapi.issuestring import *
|
||||
@@ -14,37 +14,28 @@
|
||||
# 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 import qtutils
|
||||
|
||||
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()
|
||||
self.textEdit.setPlainText(text)
|
||||
except AttributeError:
|
||||
except:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.exception("Displaying raw tags failed")
|
||||
qtutils.qt_error("Displaying raw tags failed:", e)
|
||||
self.textEdit.setPlainText(text)
|
||||
|
||||
@@ -14,108 +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 QtCore, QtGui, QtWidgets
|
||||
|
||||
def show_exception_box(log_msg):
|
||||
"""Checks if a QApplication instance is available and shows a messagebox with the exception message.
|
||||
If unavailable (non-console application), log an additional notice.
|
||||
"""
|
||||
if QtWidgets.QApplication.instance() is not None:
|
||||
errorbox = QtWidgets.QMessageBox()
|
||||
errorbox.setText("Oops. An unexpected error occured:\n{0}".format(log_msg))
|
||||
errorbox.exec_()
|
||||
QtWidgets.QApplication.exit(1)
|
||||
else:
|
||||
logger.debug("No QApplication instance available.")
|
||||
|
||||
class UncaughtHook(QtCore.QObject):
|
||||
_exception_caught = QtCore.pyqtSignal(object)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(UncaughtHook, self).__init__(*args, **kwargs)
|
||||
|
||||
# this registers the exception_hook() function as hook with the Python interpreter
|
||||
sys.excepthook = self.exception_hook
|
||||
|
||||
# connect signal to execute the message box function always on main thread
|
||||
self._exception_caught.connect(show_exception_box)
|
||||
|
||||
def exception_hook(self, exc_type, exc_value, exc_traceback):
|
||||
"""Function handling uncaught exceptions.
|
||||
It is triggered each time an uncaught exception occurs.
|
||||
"""
|
||||
if issubclass(exc_type, KeyboardInterrupt):
|
||||
# ignore keyboard interrupt to support console applications
|
||||
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
||||
else:
|
||||
exc_info = (exc_type, exc_value, exc_traceback)
|
||||
log_msg = "\n".join(
|
||||
["".join(traceback.format_tb(exc_traceback)), "{0}: {1}".format(exc_type.__name__, exc_value)]
|
||||
)
|
||||
logger.critical(
|
||||
"Uncaught exception: %s", "{0}: {1}".format(exc_type.__name__, exc_value), exc_info=exc_info
|
||||
)
|
||||
|
||||
# trigger message box show
|
||||
self._exception_caught.emit(log_msg)
|
||||
|
||||
qt_exception_hook = UncaughtHook()
|
||||
from comictaggerlib.taggerwindow import TaggerWindow
|
||||
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:
|
||||
@@ -130,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 mode failed")
|
||||
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()
|
||||
@@ -185,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("GUI mode failed")
|
||||
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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,313 +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.chkDoublePage.toggled.connect(self.toggle_double_page)
|
||||
self.leBookmark.editingFinished.connect(self.save_bookmark)
|
||||
self.btnUp.clicked.connect(self.move_current_up)
|
||||
self.btnDown.clicked.connect(self.move_current_down)
|
||||
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.chkDoublePage.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)
|
||||
|
||||
self.chkDoublePage.setChecked("DoublePage" in self.listWidget.item(row).data(QtCore.Qt.UserRole)[0])
|
||||
|
||||
if "Bookmark" in self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]:
|
||||
self.leBookmark.setText(self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]["Bookmark"])
|
||||
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 toggle_double_page(self):
|
||||
row = self.listWidget.currentRow()
|
||||
page_dict = self.listWidget.item(row).data(QtCore.Qt.UserRole)[0]
|
||||
|
||||
if self.sender().isChecked():
|
||||
page_dict["DoublePage"] = str("true")
|
||||
elif "DoublePage" in page_dict:
|
||||
del page_dict["DoublePage"]
|
||||
self.modified.emit()
|
||||
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 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()
|
||||
|
||||
item = self.listWidget.item(row)
|
||||
# wrap the dict in a tuple to keep from being converted to QStrings
|
||||
item.setData(QtCore.Qt.UserRole, (page_dict,))
|
||||
item.setText(self.list_entry_text(page_dict))
|
||||
|
||||
self.listWidget.setFocus()
|
||||
|
||||
def 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.chkDoublePage.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 "DoublePage" in page_dict:
|
||||
text += " " + "\U00002461"
|
||||
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.chkDoublePage.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.chkDoublePage.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.chkDoublePage.setEnabled(False)
|
||||
self.leBookmark.setEnabled(False)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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", encoding="utf-8") 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", encoding="utf-8") 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()
|
||||
|
||||
@@ -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
107
comictaggerlib/ui/TemplateHelp.ui
Normal file
107
comictaggerlib/ui/TemplateHelp.ui
Normal 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><html>
|
||||
<head/>
|
||||
<body>
|
||||
<h1 style="text-align: center">Template help</h1>
|
||||
<p>The template uses Python format strings, in the simplest use it replaces the field (e.g. {issue}) with the value for that particular comic (e.g. 1) for advanced formatting please reference the
|
||||
|
||||
<a href="https://docs.python.org/3/library/string.html#format-string-syntax">Python 3 documentation</a></p>
|
||||
<pre>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({'role': 'str', 'person': 'str', 'primary': boolean}))
|
||||
{tags}		(list of str)
|
||||
{pages}		(list of dict({'Image': 'str(int)', 'Type': 'str'}))
|
||||
|
||||
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
|
||||
|
||||
</pre>
|
||||
</body>
|
||||
</html></string>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
@@ -37,9 +37,6 @@
|
||||
<property name="defaultDropAction">
|
||||
<enum>Qt::MoveAction</enum>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
@@ -87,45 +84,17 @@
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout_1">
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Page Type:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="comboBox"/>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QCheckBox" name="chkDoublePage">
|
||||
<property name="text">
|
||||
<string>Double Page?</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QFormLayout" name="formLayout_2">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<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>
|
||||
|
||||
@@ -1,50 +1,45 @@
|
||||
"""Some utilities for the GUI"""
|
||||
|
||||
import io
|
||||
import logging
|
||||
import traceback
|
||||
#import StringIO
|
||||
|
||||
#from PIL import Image
|
||||
|
||||
from comictaggerlib.settings import ComicTaggerSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from PyQt5 import QtGui, QtWidgets
|
||||
|
||||
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:
|
||||
@@ -53,32 +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
|
||||
|
||||
def qt_error(msg: str, e: Exception = None):
|
||||
trace = ""
|
||||
if e:
|
||||
trace = "\n".join(traceback.format_exception(type(e), e, e.__traceback__))
|
||||
|
||||
QtWidgets.QMessageBox.critical(QtWidgets.QMainWindow(), "Error", msg + trace)
|
||||
|
||||
@@ -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><html><head/><body><p>Applies the <span style=" font-weight:600;">Publisher Filter</span> on all searches.<br/>The search window has a dynamic toggle to show the unfiltered results.</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="text">
|
||||
<string>Always use Publisher Filter on "manual" searches:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
@@ -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><html><head/><body><p>The template for the new filename. Accepts the following variables:</p><p>%series%<br/>%issue%<br/>%volume%<br/>%issuecount%<br/>%year%<br/>%month%<br/>%month_name%<br/>%publisher%<br/>%title%<br/>
|
||||
%genre%<br/>
|
||||
%language_code%<br/>
|
||||
%criticalrating%<br/>
|
||||
%alternateseries%<br/>
|
||||
%alternatenumber%<br/>
|
||||
%alternatecount%<br/>
|
||||
%imprint%<br/>
|
||||
%format%<br/>
|
||||
%maturityrating%<br/>
|
||||
%storyarc%<br/>
|
||||
%seriesgroup%<br/>
|
||||
%scaninfo%
|
||||
</p><p>Examples:</p><p><span style=" font-style:italic;">%series% %issue% (%year%)</span><br/><span style=" font-style:italic;">%series% #%issue% - %title%</span></p></body></html></string>
|
||||
<string><pre>The template for the new filename. Uses python format strings https://docs.python.org/3/library/string.html#format-string-syntax
|
||||
Accepts the following variables:
|
||||
{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({&apos;role&apos;: &apos;str&apos;, &apos;person&apos;: &apos;str&apos;, &apos;primary&apos;: boolean}))
|
||||
{tags} (list of str)
|
||||
{pages} (list of dict({&apos;Image&apos;: &apos;str(int)&apos;, &apos;Type&apos;: &apos;str&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
|
||||
</pre></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><html><head/><body><p><span style=" font-weight:600;">&quot;Smart Text Cleanup&quot; </span>will attempt to clean up the new filename if there are missing fields from the template. For example, removing empty braces, repeated spaces and dashes, and more. Experimental feature.</p></body></html></string>
|
||||
@@ -609,13 +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>
|
||||
|
||||
@@ -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">
|
||||
@@ -1198,7 +1188,6 @@
|
||||
<addaction name="actionAutoIdentify"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionApplyCBLTransform"/>
|
||||
<addaction name="actionReCalcPageDims"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuWindow">
|
||||
<property name="title">
|
||||
@@ -1373,11 +1362,6 @@
|
||||
<string>Apply CBL Transform</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionReCalcPageDims">
|
||||
<property name="text">
|
||||
<string>Re-Calculate Page Dimensions</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionLoadFolder">
|
||||
<property name="text">
|
||||
<string>Open Folder</string>
|
||||
|
||||
@@ -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
1
comictaggerlib/utils.py
Normal file
@@ -0,0 +1 @@
|
||||
from comicapi.utils import *
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
11
google/gadgets/social.xml
Normal 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&width=292&colorscheme=light&show_faces =false&border_color&stream=false&header=false&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>
|
||||
10
google/gadgets/twitter.xml
Normal file
10
google/gadgets/twitter.xml
Normal 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
260
google/googlecode_upload.py
Executable 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())
|
||||
48
localefix.py
48
localefix.py
@@ -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())
|
||||
33
mac/Makefile
33
mac/Makefile
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
PyQt5
|
||||
PyQt5<=5.15.3
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
pyinstaller>=4.10
|
||||
pyinstaller==4.3
|
||||
setuptools>=42
|
||||
setuptools_scm[toml]>=3.4
|
||||
wheel
|
||||
black>=22
|
||||
flake8==4.*
|
||||
flake8-encodings
|
||||
isort>=5.10
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
162
setup.py
@@ -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), encoding="utf-8") 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'
|
||||
)
|
||||
|
||||
28
todo.txt
28
todo.txt
@@ -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?
|
||||
|
||||
Reference in New Issue
Block a user