Compare commits

..

2 Commits

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

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

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

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

View File

@ -25,6 +25,7 @@ import time
import io
import natsort
from PyPDF2 import PdfFileReader
try:
from unrar.cffi import rarfile
except:
@ -527,6 +528,34 @@ class UnknownArchiver:
def getArchiveFilenameList(self):
return []
class PdfArchiver:
def __init__(self, path):
self.path = path
def getArchiveComment(self):
return ""
def setArchiveComment(self, comment):
return False
def readArchiveFile(self, page_num):
return subprocess.check_output(
['mudraw', '-o', '-', self.path, str(int(os.path.basename(page_num)[:-4]))])
def writeArchiveFile(self, archive_file, data):
return False
def removeArchiveFile(self, archive_file):
return False
def getArchiveFilenameList(self):
out = []
pdf = PdfFileReader(open(self.path, 'rb'))
for page in range(1, pdf.getNumPages() + 1):
out.append("/%04d.jpg" % (page))
return out
class ComicArchive:
logo_data = None
class ArchiveType:
@ -567,6 +596,9 @@ class ComicArchive:
self.archiver = RarArchiver(
self.path,
rar_exe_path=self.rar_exe_path)
elif os.path.basename(self.path)[-3:] == 'pdf':
self.archive_type = self.ArchiveType.Pdf
self.archiver = PdfArchiver(self.path)
if ComicArchive.logo_data is None:
#fname = ComicTaggerSettings.getGraphic('nocover.png')
@ -645,7 +677,7 @@ class ComicArchive:
if (
# or self.isFolder() )
(self.isZip() or self.isRar())
(self.isZip() or self.isRar() or self.isPdf())
and
(self.getNumberOfPages() > 0)
@ -892,10 +924,7 @@ class ComicArchive:
def writeCIX(self, metadata):
if metadata is not None:
self.applyArchiveInfoToMetadata(metadata, calc_page_sizes=True)
rawCIX = self.readRawCIX()
if rawCIX == "":
rawCIX = None
cix_string = ComicInfoXml().stringFromMetadata(metadata, xml=rawCIX)
cix_string = ComicInfoXml().stringFromMetadata(metadata)
write_success = self.archiver.writeArchiveFile(
self.ci_xml_filename,
cix_string)

View File

@ -40,6 +40,7 @@ class ComicBookInfo:
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'])
@ -106,6 +107,7 @@ class ComicBookInfo:
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))

View File

@ -50,11 +50,11 @@ class ComicInfoXml:
tree = ET.ElementTree(ET.fromstring(string))
return self.convertXMLToMetadata(tree)
def stringFromMetadata(self, metadata, xml=None):
def stringFromMetadata(self, metadata):
header = '<?xml version="1.0"?>\n'
tree = self.convertMetadataToXML(self, metadata, xml)
tree = self.convertMetadataToXML(self, metadata)
tree_str = ET.tostring(tree.getroot()).decode()
return header + tree_str
@ -74,28 +74,20 @@ class ComicInfoXml:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
def convertMetadataToXML(self, filename, metadata, xml=None):
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:
print(cix_entry, md_entry)
et_entry = root.find(cix_entry)
if et_entry is not None:
et_entry.text = "{0}".format(md_entry)
else:
ET.SubElement(root, cix_entry).text = "{0}".format(md_entry)
ET.SubElement(root, cix_entry).text = "{0}".format(md_entry)
assign('Title', md.title)
assign('Series', md.series)
@ -112,6 +104,7 @@ class ComicInfoXml:
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
@ -149,19 +142,33 @@ class ComicInfoXml:
credit_editor_list.append(credit['person'].replace(",", ""))
# second, convert each list to string, and add to XML struct
assign('Writer', utils.listToString(credit_writer_list))
if len(credit_writer_list) > 0:
node = ET.SubElement(root, 'Writer')
node.text = utils.listToString(credit_writer_list)
assign('Penciller', utils.listToString(credit_penciller_list))
if len(credit_penciller_list) > 0:
node = ET.SubElement(root, 'Penciller')
node.text = utils.listToString(credit_penciller_list)
assign('Inker', utils.listToString(credit_inker_list))
if len(credit_inker_list) > 0:
node = ET.SubElement(root, 'Inker')
node.text = utils.listToString(credit_inker_list)
assign('Colorist', utils.listToString(credit_colorist_list))
if len(credit_colorist_list) > 0:
node = ET.SubElement(root, 'Colorist')
node.text = utils.listToString(credit_colorist_list)
assign('Letterer', utils.listToString(credit_letterer_list))
if len(credit_letterer_list) > 0:
node = ET.SubElement(root, 'Letterer')
node.text = utils.listToString(credit_letterer_list)
assign('CoverArtist', utils.listToString(credit_cover_list))
if len(credit_cover_list) > 0:
node = ET.SubElement(root, 'CoverArtist')
node.text = utils.listToString(credit_cover_list)
assign('Editor', utils.listToString(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)
@ -172,7 +179,7 @@ class ComicInfoXml:
assign('Format', md.format)
assign('AgeRating', md.maturityRating)
if md.blackAndWhite is not None and md.blackAndWhite:
assign('BlackAndWhite', "Yes")
ET.SubElement(root, 'BlackAndWhite').text = "Yes"
assign('Manga', md.manga)
assign('Characters', md.characters)
assign('Teams', md.teams)
@ -180,15 +187,11 @@ class ComicInfoXml:
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:
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 = page_dict
# self pretty-print
self.indent(root)
@ -226,6 +229,7 @@ class ComicInfoXml:
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'))
@ -274,9 +278,9 @@ class ComicInfoXml:
return md
def writeToExternalFile(self, filename, metadata, xml=None):
def writeToExternalFile(self, filename, metadata):
tree = self.convertMetadataToXML(self, metadata, xml)
tree = self.convertMetadataToXML(self, metadata)
# ET.dump(tree)
tree.write(filename, encoding='utf-8')

View File

@ -65,6 +65,7 @@ class GenericMetadata:
self.issue = None
self.title = None
self.publisher = None
self.seriesYear = None
self.month = None
self.year = None
self.day = None
@ -132,6 +133,7 @@ class GenericMetadata:
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)
@ -263,6 +265,7 @@ class GenericMetadata:
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")

View File

@ -38,7 +38,8 @@ class IssueString:
if text is None:
return
text = str(text)
if isinstance(text, int):
text = str(text)
if len(text) == 0:
return

View File

@ -507,19 +507,31 @@ def process_file_cli(filename, opts, settings, match_results):
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.determineName(filename, ext=new_ext)
if new_name == os.path.basename(filename):
print(msg_hdr + "Filename is already good!", file=sys.stderr)
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(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.makedirs(os.path.dirname(new_abs_path), 0o777, True)
os.rename(filename, new_abs_path)
else:
suffix = " (dry-run, no change)"

View File

@ -505,11 +505,12 @@ class ComicVineTalker(QObject):
metadata.publisher = utils.xlate(volume_results['publisher']['name'])
metadata.day, metadata.month, metadata.year = self.parseDateStr(issue_results['cover_date'])
#metadata.issueCount = volume_results['count_of_issues']
metadata.seriesYear = utils.xlate(volume_results['start_year'])
metadata.issueCount = utils.xlate(volume_results['count_of_issues'])
metadata.comments = self.cleanup_html(
issue_results['description'], settings.remove_html_tables)
if settings.use_series_start_as_volume:
metadata.volume = volume_results['start_year']
metadata.volume = utils.xlate(volume_results['start_year'])
metadata.notes = "Tagged with ComicTagger {0} using info from Comic Vine on {1}. [Issue ID {2}]".format(
ctversion.version,

View File

@ -320,7 +320,7 @@ class CoverImageWidget(QWidget):
img_w = scaled_pixmap.width()
img_h = scaled_pixmap.height()
self.lblImage.resize(img_w, img_h)
self.lblImage.move(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 showPopup(self):
self.popup = ImagePopup(self, self.current_pixmap)

View File

@ -17,19 +17,96 @@
import os
import re
import datetime
import sys
import string
from pathvalidate import sanitize_filepath
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.setMetadata(metadata)
self.setTemplate(
"%series% v%volume% #%issue% (of %issuecount%) (%year%)")
"{publisher}/{series}/{series} v{volume} #{issue} (of {issueCount}) ({year})")
self.smart_cleanup = True
self.issue_zero_padding = 3
self.move = False
def setMetadata(self, metadata):
self.metdata = metadata
@ -43,114 +120,37 @@ class FileRenamer:
def setTemplate(self, template):
self.template = template
def replaceToken(self, text, value, token):
# helper func
def isToken(word):
return (word[0] == "%" and word[-1:] == "%")
if value is not None:
return text.replace(token, str(value))
else:
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()
# 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 isToken(text_list[idx - 1]):
text_list[idx - 1] = ""
text_list = [x for x in text_list if token not in x]
return " ".join(text_list)
else:
return text.replace(token, "")
def determineName(self, filename, ext=None):
class Default(dict):
def __missing__(self, key):
return "{" + key + "}"
md = self.metdata
new_name = self.template
preferred_encoding = utils.get_actual_preferred_encoding()
# print(u"{0}".format(md))
new_name = self.replaceToken(new_name, md.series, '%series%')
new_name = self.replaceToken(new_name, md.volume, '%volume%')
# padding for issue
md.issue = IssueString(md.issue).asString(pad=self.issue_zero_padding)
if md.issue is not None:
issue_str = "{0}".format(
IssueString(md.issue).asString(pad=self.issue_zero_padding))
else:
issue_str = None
new_name = self.replaceToken(new_name, issue_str, '%issue%')
template = self.template
new_name = self.replaceToken(new_name, md.issueCount, '%issuecount%')
new_name = self.replaceToken(new_name, md.year, '%year%')
new_name = self.replaceToken(new_name, md.publisher, '%publisher%')
new_name = self.replaceToken(new_name, md.title, '%title%')
new_name = self.replaceToken(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".encode(preferred_encoding)).decode(preferred_encoding)
month_name = dt.strftime("%B")
new_name = self.replaceToken(new_name, month_name, '%month_name%')
pathComponents = template.split(os.sep)
new_name = ""
new_name = self.replaceToken(new_name, md.genre, '%genre%')
new_name = self.replaceToken(new_name, md.language, '%language_code%')
new_name = self.replaceToken(
new_name, md.criticalRating, '%criticalrating%')
new_name = self.replaceToken(
new_name, md.alternateSeries, '%alternateseries%')
new_name = self.replaceToken(
new_name, md.alternateNumber, '%alternatenumber%')
new_name = self.replaceToken(
new_name, md.alternateCount, '%alternatecount%')
new_name = self.replaceToken(new_name, md.imprint, '%imprint%')
new_name = self.replaceToken(new_name, md.format, '%format%')
new_name = self.replaceToken(
new_name, md.maturityRating, '%maturityrating%')
new_name = self.replaceToken(new_name, md.storyArc, '%storyarc%')
new_name = self.replaceToken(new_name, md.seriesGroup, '%seriesgroup%')
new_name = self.replaceToken(new_name, md.scanInfo, '%scaninfo%')
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("/", "-"))
if self.smart_cleanup:
# remove empty braces,brackets, parentheses
new_name = re.sub("\(\s*[-:]*\s*\)", "", new_name)
new_name = re.sub("\[\s*[-:]*\s*\]", "", new_name)
new_name = re.sub("\{\s*[-:]*\s*\}", "", new_name)
# remove duplicate spaces
new_name = " ".join(new_name.split())
# remove remove duplicate -, _,
new_name = re.sub("[-_]{2,}\s+", "-- ", new_name)
new_name = re.sub("(\s--)+", " --", new_name)
new_name = re.sub("(\s-)+", " -", new_name)
# remove dash or double dash at end of line
new_name = re.sub("[-]{1,2}\s*$", "", new_name)
# remove duplicate spaces (again!)
new_name = " ".join(new_name.split())
if ext is None:
if ext is None or ext == "":
ext = os.path.splitext(filename)[1]
new_name += ext
# some tweaks to keep various filesystems happy
new_name = new_name.replace("/", "-")
new_name = new_name.replace(" :", " -")
new_name = new_name.replace(": ", " - ")
new_name = new_name.replace(":", "-")
new_name = new_name.replace("?", "")
return new_name
# remove padding
md.issue = IssueString(md.issue).asString()
if self.move:
return sanitize_filepath(new_name.strip())
else:
return os.path.basename(sanitize_filepath(new_name.strip()))

View File

@ -20,6 +20,7 @@ from functools import reduce
try:
from PIL import Image
from PIL import WebPImagePlugin
pil_available = True
except ImportError:
pil_available = False

View File

@ -87,7 +87,7 @@ class ImagePopup(QtWidgets.QDialog):
img_w = display_pixmap.width()
img_h = display_pixmap.height()
self.lblImage.resize(img_w, img_h)
self.lblImage.move(int((win_w - img_w) / 2), int((win_h - img_h) / 2))
self.lblImage.move((win_w - img_w) / 2, (win_h - img_h) / 2)
def mousePressEvent(self, event):
self.close()

View File

@ -19,6 +19,7 @@ import io
try:
from PIL import Image
from PIL import WebPImagePlugin
pil_available = True
except ImportError:
pil_available = False
@ -75,8 +76,8 @@ 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.defaultWriteOutput
@ -99,8 +100,8 @@ class IssueIdentifier:
def setNameLengthDeltaThreshold(self, delta):
self.length_delta_thresh = delta
def setPublisherFilter(self, filter):
self.publisher_filter = filter
def setPublisherBlackList(self, blacklist):
self.publisher_blacklist = blacklist
def setHasherAlgorithm(self, algo):
self.image_hasher = algo
@ -230,9 +231,9 @@ class IssueIdentifier:
sys.stdout.flush()
def log_msg(self, msg, newline=True):
if newline:
msg += "\n"
self.output_function(msg)
if newline:
self.output_function("\n")
def getIssueCoverMatchScore(
self,
@ -393,7 +394,7 @@ class IssueIdentifier:
if keys['month'] is not None:
self.log_msg("\tMonth: " + str(keys['month']))
#self.log_msg("Publisher Filter: " + str(self.publisher_filter))
#self.log_msg("Publisher Blacklist: " + str(self.publisher_blacklist))
comicVine = ComicVineTalker()
comicVine.wait_for_rate_limit = self.waitAndRetryOnRateLimit
@ -441,11 +442,11 @@ class IssueIdentifier:
len(shortened_key) + self.length_delta_thresh):
length_approved = True
# remove any series from publishers on the 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_filter:
) in self.publisher_blacklist:
publisher_approved = False
if length_approved and publisher_approved and date_approved:

View File

@ -15,7 +15,6 @@
# limitations under the License.
#import os
from operator import itemgetter, attrgetter
from PyQt5.QtCore import *
from PyQt5.QtGui import *
@ -127,75 +126,25 @@ class PageListEditor(QWidget):
self.comic_archive = None
self.pages_list = None
def getNewIndexes(self, movement):
selection = self.listWidget.selectionModel().selectedRows()
selection.sort(reverse=movement>0)
current = 0
newindexes = []
oldindexes = []
for x in selection:
current = x.row()
oldindexes.append(current)
if current + movement >= 0 and current + movement <= self.listWidget.count()-1:
if len(newindexes) < 1 or current + movement != newindexes[-1]:
current += movement
else:
prev = current
newindexes.append(current)
oldindexes.sort()
newindexes.sort()
return list(zip(newindexes, oldindexes))
def SetSelection(self, indexes):
selectionRanges = []
first = 0
for i, selection in enumerate(indexes):
if i == 0:
first = selection[0]
continue
if selection != indexes[i-1][0]+1:
selectionRanges.append((first,indexes[i-1][0]))
first = selection[0]
selectionRanges.append((first, indexes[-1][0]))
selection = QItemSelection()
for x in selectionRanges:
selection.merge(QItemSelection(self.listWidget.model().index(x[0], 0), self.listWidget.model().index(x[1], 0)), QItemSelectionModel.Select)
self.listWidget.selectionModel().select(selection, QItemSelectionModel.ClearAndSelect)
return selectionRanges
def moveCurrentUp(self):
row = self.listWidget.currentRow()
selection = self.getNewIndexes(-1)
for sel in selection:
item = self.listWidget.takeItem(sel[1])
self.listWidget.insertItem(sel[0], item)
if row > 0:
item = self.listWidget.takeItem(row)
self.listWidget.insertItem(row - 1, item)
self.listWidget.setCurrentRow(row - 1)
self.SetSelection(selection)
self.listOrderChanged.emit()
self.emitFrontCoverChange()
self.modified.emit()
self.listOrderChanged.emit()
self.emitFrontCoverChange()
self.modified.emit()
def moveCurrentDown(self):
row = self.listWidget.currentRow()
selection = self.getNewIndexes(1)
selection.sort(reverse=True)
for sel in selection:
item = self.listWidget.takeItem(sel[1])
self.listWidget.insertItem(sel[0], item)
if row < self.listWidget.count() - 1:
item = self.listWidget.takeItem(row)
self.listWidget.insertItem(row + 1, item)
self.listWidget.setCurrentRow(row + 1)
self.listOrderChanged.emit()
self.emitFrontCoverChange()
self.SetSelection(selection)
self.modified.emit()
self.listOrderChanged.emit()
self.emitFrontCoverChange()
self.modified.emit()
def itemMoveEvent(self, s):
# print "move event: ", s, self.listWidget.currentRow()

View File

@ -76,7 +76,20 @@ class RenameWindow(QtWidgets.QDialog):
if md.isEmpty:
md = ca.metadataFromFilename(self.settings.parse_scan_info)
self.renamer.setMetadata(md)
new_name = self.renamer.determineName(ca.path, ext=new_ext)
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)
@ -149,17 +162,20 @@ class RenameWindow(QtWidgets.QDialog):
centerWindowOnParent(progdialog)
QtCore.QCoreApplication.processEvents()
if item['new_name'] == os.path.basename(item['archive'].path):
folder = os.path.dirname(os.path.abspath(item['archive'].path))
if self.settings.rename_move_dir and len(self.settings.rename_dir.strip()) > 3:
folder = self.settings.rename_dir.strip()
new_abs_path = utils.unique_file(os.path.join(folder, item['new_name']))
if os.path.join(folder, item['new_name']) == item['archive'].path:
print(item['new_name'], "Filename is already good!")
continue
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)
item['archive'].rename(new_abs_path)

View File

@ -78,8 +78,8 @@ 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
self.show_disclaimer = True
@ -95,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
@ -112,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
@ -226,9 +224,9 @@ class ComicTaggerSettings:
if self.config.has_option('identifier', 'id_length_delta_thresh'):
self.id_length_delta_thresh = self.config.getint(
'identifier', 'id_length_delta_thresh')
if self.config.has_option('identifier', 'id_publisher_filter'):
self.id_publisher_filter = self.config.get(
'identifier', 'id_publisher_filter')
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(
@ -258,17 +256,6 @@ class ComicTaggerSettings:
if self.config.has_option('comicvine', 'remove_html_tables'):
self.remove_html_tables = self.config.getboolean(
'comicvine', 'remove_html_tables')
if self.config.has_option('comicvine', 'sort_series_by_year'):
self.sort_series_by_year = self.config.getboolean(
'comicvine', 'sort_series_by_year')
if self.config.has_option('comicvine', 'exact_series_matches_first'):
self.exact_series_matches_first = self.config.getboolean(
'comicvine', 'exact_series_matches_first')
if self.config.has_option('comicvine', 'always_use_publisher_filter'):
self.always_use_publisher_filter = self.config.getboolean(
'comicvine', 'always_use_publisher_filter')
if self.config.has_option('comicvine', 'cv_api_key'):
self.cv_api_key = self.config.get('comicvine', 'cv_api_key')
@ -316,6 +303,10 @@ class ComicTaggerSettings:
'rename', 'rename_extension_based_on_archive'):
self.rename_extension_based_on_archive = self.config.getboolean(
'rename', 'rename_extension_based_on_archive')
if self.config.has_option('rename', 'rename_dir'):
self.rename_dir = self.config.get('rename', 'rename_dir')
if self.config.has_option('rename', 'rename_move_dir'):
self.rename_move_dir = self.config.getboolean('rename', 'rename_move_dir')
if self.config.has_option('autotag', 'save_on_low_confidence'):
self.save_on_low_confidence = self.config.getboolean(
@ -390,8 +381,8 @@ class ComicTaggerSettings:
self.id_length_delta_thresh)
self.config.set(
'identifier',
'id_publisher_filter',
self.id_publisher_filter)
'id_publisher_blacklist',
self.id_publisher_blacklist)
if not self.config.has_section('dialogflags'):
self.config.add_section('dialogflags')
@ -423,14 +414,6 @@ class ComicTaggerSettings:
self.clear_form_before_populating_from_cv)
self.config.set(
'comicvine', 'remove_html_tables', self.remove_html_tables)
self.config.set(
'comicvine', 'sort_series_by_year', self.sort_series_by_year)
self.config.set(
'comicvine', 'exact_series_matches_first', self.exact_series_matches_first)
self.config.set(
'comicvine', 'always_use_publisher_filter', self.always_use_publisher_filter)
self.config.set('comicvine', 'cv_api_key', self.cv_api_key)
if not self.config.has_section('cbl_transform'):
@ -485,6 +468,8 @@ class ComicTaggerSettings:
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')

View File

@ -24,6 +24,8 @@ 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
@ -94,12 +96,12 @@ class SettingsWindow(QtWidgets.QDialog):
pblTip = (
"""<html>
The <b>Publisher Filter</b> is for eliminating automatic matches to certain publishers
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(pblTip)
self.tePublisherBlacklist.setToolTip(pblTip)
validator = QtGui.QIntValidator(1, 4, self)
self.leIssueNumPadding.setValidator(validator)
@ -113,6 +115,66 @@ class SettingsWindow(QtWidgets.QDialog):
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 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):
@ -120,8 +182,8 @@ class SettingsWindow(QtWidgets.QDialog):
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.tePublisherBlacklist.setPlainText(
self.settings.id_publisher_blacklist)
if self.settings.check_for_new_version:
self.cbxCheckForNewVersion.setCheckState(QtCore.Qt.Checked)
@ -135,14 +197,6 @@ class SettingsWindow(QtWidgets.QDialog):
self.cbxClearFormBeforePopulating.setCheckState(QtCore.Qt.Checked)
if self.settings.remove_html_tables:
self.cbxRemoveHtmlTables.setCheckState(QtCore.Qt.Checked)
if self.settings.always_use_publisher_filter:
self.cbxUseFilter.setCheckState(QtCore.Qt.Checked)
if self.settings.sort_series_by_year:
self.cbxSortByYear.setCheckState(QtCore.Qt.Checked)
if self.settings.exact_series_matches_first:
self.cbxExactMatches.setCheckState(QtCore.Qt.Checked)
self.leKey.setText(str(self.settings.cv_api_key))
if self.settings.assume_lone_credit_is_primary:
@ -173,9 +227,27 @@ class SettingsWindow(QtWidgets.QDialog):
self.cbxSmartCleanup.setCheckState(QtCore.Qt.Checked)
if self.settings.rename_extension_based_on_archive:
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())
@ -193,19 +265,14 @@ class SettingsWindow(QtWidgets.QDialog):
self.settings.id_length_delta_thresh = int(
self.leNameLengthDeltaThresh.text())
self.settings.id_publisher_filter = str(
self.tePublisherFilter.toPlainText())
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()
@ -223,6 +290,8 @@ class SettingsWindow(QtWidgets.QDialog):
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)
@ -281,3 +350,17 @@ class SettingsWindow(QtWidgets.QDialog):
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)

View File

@ -187,6 +187,8 @@ class TaggerWindow(QtWidgets.QMainWindow):
validator = QtGui.QIntValidator(1900, 2099, self)
self.lePubYear.setValidator(validator)
self.leSeriesPubYear.setValidator(validator)
validator = QtGui.QIntValidator(1, 12, self)
self.lePubMonth.setValidator(validator)
@ -780,6 +782,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
assignText(self.lePubMonth, md.month)
assignText(self.lePubYear, md.year)
assignText(self.lePubDay, md.day)
assignText(self.leSeriesPubYear, md.seriesYear)
assignText(self.leGenre, md.genre)
assignText(self.leImprint, md.imprint)
assignText(self.teComments, md.comments)
@ -905,6 +908,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
md.month = utils.xlate(self.lePubMonth.text(), True)
md.year = utils.xlate(self.lePubYear.text(), True)
md.day = utils.xlate(self.lePubDay.text(), True)
md.seriesYear = utils.xlate(self.leSeriesPubYear.text(), "int")
md.criticalRating = utils.xlate(self.leCriticalRating.text(), True)
md.alternateCount = utils.xlate(self.leAltIssueCount.text(), True)
@ -958,6 +962,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
row += 1
md.pages = self.pageListEditor.getPageList()
self.metadata = md
def useFilename(self):
@ -1385,8 +1390,8 @@ class TaggerWindow(QtWidgets.QMainWindow):
else:
screen = QtWidgets.QDesktopWidget().screenGeometry()
size = self.frameGeometry()
self.move(int((screen.width() - size.width()) / 2),
int((screen.height() - size.height()) / 2))
self.move((screen.width() - size.width()) / 2,
(screen.height() - size.height()) / 2)
def adjustLoadStyleCombo(self):
# select the current style
@ -1422,7 +1427,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
# Add the entries to the language combobox
self.cbLanguage.addItem("", "")
lang_dict = utils.getLanguageDict()
for key in sorted(lang_dict, key=lang_dict.get):
for key in sorted(lang_dict, key=functools.cmp_to_key(locale.strcoll)):
self.cbLanguage.addItem(lang_dict[key], key)
# Add the entries to the manga combobox
@ -1721,7 +1726,7 @@ class TaggerWindow(QtWidgets.QMainWindow):
def autoTagLog(self, text):
IssueIdentifier.defaultWriteOutput(text)
if self.atprogdialog is not None:
self.atprogdialog.textEdit.append(text.rstrip())
self.atprogdialog.textEdit.insertPlainText(text)
self.atprogdialog.textEdit.ensureCursorVisible()
QtCore.QCoreApplication.processEvents()
QtCore.QCoreApplication.processEvents()
@ -2064,13 +2069,8 @@ class TaggerWindow(QtWidgets.QMainWindow):
self.comic_archive = None
self.clearForm()
if not os.path.exists(comic_archive.path):
self.fileSelectionList.dirty_flag = False
self.fileSelectionList.removeArchiveList([comic_archive])
QtCore.QTimer.singleShot(1, self.fileSelectionList.revertSelection)
return
self.settings.last_opened_folder = os.path.abspath(os.path.split(comic_archive.path)[0])
self.settings.last_opened_folder = os.path.abspath(
os.path.split(comic_archive.path)[0])
self.comic_archive = comic_archive
self.metadata = self.comic_archive.readMetadata(self.load_data_style)
if self.metadata is None:

View File

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

View File

@ -37,9 +37,6 @@
<property name="defaultDropAction">
<enum>Qt::MoveAction</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
</widget>
</item>
<item>

View File

@ -51,9 +51,9 @@ if qt_available:
mysize = window.geometry()
# The horizontal position is calculated as screen width - window width
# /2
hpos = int((main_window_size.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 +
@ -63,6 +63,7 @@ if qt_available:
try:
from PIL import Image
from PIL import WebPImagePlugin
import io
pil_available = True
except ImportError:

View File

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

View File

@ -512,6 +512,16 @@
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QLineEdit" name="leSeriesPubYear"/>
</item>
<item row="9" column="1">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Series Year</string>
</property>
</widget>
</item>
</layout>
</item>
<item>

View File

@ -148,16 +148,6 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cbxFilter">
<property name="text">
<string>Filter Publishers</string>
</property>
<property name="toolTip">
<string>Filter the publishers based on the publisher filter.</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">

View File

@ -32,11 +32,9 @@ from .settings import ComicTaggerSettings
from .matchselectionwindow import MatchSelectionWindow
from .coverimagewidget import CoverImageWidget
from comictaggerlib.ui.qtutils import reduceWidgetFontSize, centerWindowOnParent
from comictaggerlib import settings
#from imagefetcher import ImageFetcher
#import utils
from . import utils
class SearchThread(QtCore.QThread):
@ -126,8 +124,6 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
self.cover_index_list = cover_index_list
self.cv_search_results = None
self.use_filter = self.settings.always_use_publisher_filter
self.twList.resizeColumnsToContents()
self.twList.currentItemChanged.connect(self.currentItemChanged)
self.twList.cellDoubleClicked.connect(self.cellDoubleClicked)
@ -135,9 +131,6 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
self.btnIssues.clicked.connect(self.showIssues)
self.btnAutoSelect.clicked.connect(self.autoSelect)
self.cbxFilter.setChecked(self.use_filter)
self.cbxFilter.toggled.connect(self.filterToggled)
self.updateButtons()
self.performQuery()
self.twList.selectRow(0)
@ -158,10 +151,6 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
self.performQuery(refresh=True)
self.twList.selectRow(0)
def filterToggled(self):
self.use_filter = not self.use_filter
self.performQuery(refresh=False)
def autoSelect(self):
if self.comic_archive is None:
@ -304,6 +293,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
self.progdialog.canceled.connect(self.searchCanceled)
self.progdialog.setModal(True)
self.progdialog.setMinimumDuration(300)
QtCore.QCoreApplication.processEvents()
self.search_thread = SearchThread(self.series_name, refresh)
self.search_thread.searchComplete.connect(self.searchComplete)
self.search_thread.progressUpdate.connect(self.searchProgressUpdate)
@ -344,40 +334,6 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
return
self.cv_search_results = self.search_thread.cv_search_results
# filter the publishers if enabled set
if self.use_filter:
try:
publisher_filter = {s.strip().lower() for s in self.settings.id_publisher_filter.split(',')}
# use '' as publisher name if None
self.cv_search_results = list(filter(lambda d: ('' if d['publisher'] is None else str(d['publisher']['name']).lower()) not in publisher_filter, self.cv_search_results))
except:
print('bad data error filtering filter publishers')
# pre sort the data - so that we can put exact matches first afterwards
# compare as str incase extra chars ie. '1976?'
# - missing (none) values being converted to 'None' - consistant with prior behaviour in v1.2.3
# sort by start_year if set
if self.settings.sort_series_by_year:
try:
self.cv_search_results = sorted(self.cv_search_results, key = lambda i: (str(i['start_year']), str(i['count_of_issues'])), reverse=True)
except:
print('bad data error sorting results by start_year,count_of_issues')
else:
try:
self.cv_search_results = sorted(self.cv_search_results, key = lambda i: str(i['count_of_issues']), reverse=True)
except:
print('bad data error sorting results by count_of_issues')
# move sanitized matches to the front
if self.settings.exact_series_matches_first:
try:
sanitized = utils.sanitize_title(self.series_name)
exactMatches = list(filter(lambda d: utils.sanitize_title(str(d['name'])) in sanitized, self.cv_search_results))
nonMatches = list(filter(lambda d: utils.sanitize_title(str(d['name'])) not in sanitized, self.cv_search_results))
self.cv_search_results = exactMatches + nonMatches
except:
print('bad data error filtering exact/near matches')
self.updateButtons()
self.twList.setSortingEnabled(False)
@ -419,7 +375,9 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
row += 1
self.twList.resizeColumnsToContents()
self.twList.setSortingEnabled(True)
self.twList.sortItems(2, QtCore.Qt.DescendingOrder)
self.twList.selectRow(0)
self.twList.resizeColumnsToContents()
@ -448,7 +406,7 @@ class VolumeSelectionWindow(QtWidgets.QDialog):
return
self.volume_id = self.twList.item(curr.row(), 0).data(QtCore.Qt.UserRole)
# list selection was changed, update the info on the volume
for record in self.cv_search_results:
if record['id'] == self.volume_id:

View File

@ -1,5 +1,7 @@
beautifulsoup4 >= 4.1
PyPDF2==1.24
configparser
natsort
pillow>=4.3.0
requests
pathvalidate