From 0747a6b0ef6805010e40268f2e4b88949ae5f465 Mon Sep 17 00:00:00 2001 From: lordwelch Date: Tue, 3 Sep 2019 00:36:36 -0700 Subject: [PATCH] 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. --- comictaggerlib/cli.py | 13 +- comictaggerlib/filerenamer.py | 192 ++++++++++++++-------------- comictaggerlib/renamewindow.py | 14 +- comictaggerlib/settings.py | 30 +++-- comictaggerlib/settingswindow.py | 39 ++++-- comictaggerlib/taggerwindow.py | 13 +- comictaggerlib/ui/TemplateHelp.ui | 105 +++++++++++++++ comictaggerlib/ui/settingswindow.ui | 106 ++++++++++++--- requirements.txt | 3 +- 9 files changed, 363 insertions(+), 152 deletions(-) create mode 100644 comictaggerlib/ui/TemplateHelp.ui diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index 688907d..91e10fc 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -507,19 +507,24 @@ 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): + 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 - folder = os.path.dirname(os.path.abspath(filename)) - new_abs_path = utils.unique_file(os.path.join(folder, new_name)) - 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)" diff --git a/comictaggerlib/filerenamer.py b/comictaggerlib/filerenamer.py index 0f18151..150c01b 100644 --- a/comictaggerlib/filerenamer.py +++ b/comictaggerlib/filerenamer.py @@ -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())) diff --git a/comictaggerlib/renamewindow.py b/comictaggerlib/renamewindow.py index 9d1e2e1..abe856d 100644 --- a/comictaggerlib/renamewindow.py +++ b/comictaggerlib/renamewindow.py @@ -76,6 +76,7 @@ class RenameWindow(QtWidgets.QDialog): if md.isEmpty: md = ca.metadataFromFilename(self.settings.parse_scan_info) self.renamer.setMetadata(md) + self.renamer.move = self.settings.rename_move_dir new_name = self.renamer.determineName(ca.path, ext=new_ext) row = self.twList.rowCount() @@ -149,17 +150,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) diff --git a/comictaggerlib/settings.py b/comictaggerlib/settings.py index 3879445..a82b6c3 100644 --- a/comictaggerlib/settings.py +++ b/comictaggerlib/settings.py @@ -44,7 +44,7 @@ class ComicTaggerSettings: @staticmethod def haveOwnUnrarLib(): return os.path.exists(ComicTaggerSettings.defaultLibunrarPath()) - + @staticmethod def baseDir(): if getattr(sys, 'frozen', None): @@ -118,10 +118,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 @@ -166,15 +168,15 @@ 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 + # make sure rar program is now in the path for the rar class utils.addtopath(os.path.dirname(self.rar_exe_path)) - + if self.haveOwnUnrarLib(): # We have a 'personal' copy of the unrar lib in the basedir, so # don't search and change the setting # NOTE: a manual edit of the settings file overrides this below os.environ["UNRAR_LIB_PATH"] = self.defaultLibunrarPath() - + elif self.unrar_lib_path == "": # Priority is for unrar lib search is: # 1. explicit setting in settings file @@ -186,11 +188,11 @@ class ComicTaggerSettings: # look in some platform specific places: if platform.system() == "Windows": # Default location for the RARLab DLL installer - if (platform.architecture()[0] == '64bit' and + if (platform.architecture()[0] == '64bit' and os.path.exists("C:\\Program Files (x86)\\UnrarDLL\\x64\\UnRAR64.dll") ): self.unrar_lib_path = "C:\\Program Files (x86)\\UnrarDLL\\x64\\UnRAR64.dll" - elif (platform.architecture()[0] == '32bit' and + elif (platform.architecture()[0] == '32bit' and os.path.exists("C:\\Program Files\\UnrarDLL\\UnRAR.dll") ): self.unrar_lib_path = "C:\\Program Files\\UnrarDLL\\UnRAR.dll" @@ -202,14 +204,14 @@ class ComicTaggerSettings: if os.path.exists("/usr/local/lib/libunrar.so"): self.unrar_lib_path = "/usr/local/lib/libunrar.so" elif os.path.exists("/usr/lib/libunrar.so"): - self.unrar_lib_path = "/usr/lib/libunrar.so" - + self.unrar_lib_path = "/usr/lib/libunrar.so" + if self.unrar_lib_path != "": self.save() - + if self.unrar_lib_path != "": # This needs to occur before the unrar module is loaded for the first time - os.environ["UNRAR_LIB_PATH"] = self.unrar_lib_path + os.environ["UNRAR_LIB_PATH"] = self.unrar_lib_path def reset(self): os.unlink(self.settings_file) @@ -358,6 +360,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( @@ -522,6 +528,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') diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py index 857b885..28978b9 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -44,13 +44,13 @@ linuxRarHelp = """ here, and install in your path.

""" - + macRarHelp = """

To write to CBR/RAR archives, you will need the rar tool. The easiest way to get this is to install homebrew. -

Once homebrew is installed, run: brew install caskroom/cask/rar +

Once homebrew is installed, run: brew install caskroom/cask/rar """ windowsUnrarHelp = """ @@ -70,7 +70,7 @@ linuxUnrarHelp = """ here for the UnRAR source (which is easy to compile on Linux).

""" - + macUnrarHelp = """

To read CBR/RAR archives, you will need the unrar library. The easiest way to get this is @@ -92,13 +92,13 @@ class SettingsWindow(QtWidgets.QDialog): self.settings = settings self.name = "Settings" - + self.priorUnrarLibPath = self.settings.unrar_lib_path if self.settings.haveOwnUnrarLib(): # We have our own unrarlib, so no need for this GUI self.grpBoxUnrar.hide() - + if platform.system() == "Windows": self.lblRarHelp.setText(windowsRarHelp) self.lblUnrarHelp.setText(windowsUnrarHelp) @@ -111,7 +111,7 @@ class SettingsWindow(QtWidgets.QDialog): # Mac file dialog hides "/usr" and others, so allow user to type self.leUnrarLibPath.setReadOnly(False) self.leRarExePath.setReadOnly(False) - + self.lblRarHelp.setText(macRarHelp) self.lblUnrarHelp.setText(macUnrarHelp) self.name = "Preferences" @@ -152,6 +152,7 @@ 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 settingsToForm(self): @@ -205,12 +206,15 @@ 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): # Copy values from form to settings and save self.settings.rar_exe_path = str(self.leRarExePath.text()) - + # Don't accept the form info if we have our own unrar lib if not self.settings.haveOwnUnrarLib(): self.settings.unrar_lib_path = str(self.leUnrarLibPath.text()) @@ -222,7 +226,7 @@ class SettingsWindow(QtWidgets.QDialog): if self.settings.unrar_lib_path: os.environ["UNRAR_LIB_PATH"] = self.settings.unrar_lib_path # This doesn't do anything... we need to restart! - + if not str(self.leNameLengthDeltaThresh.text()).isdigit(): self.leNameLengthDeltaThresh.setText("0") @@ -258,6 +262,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) @@ -266,7 +272,7 @@ class SettingsWindow(QtWidgets.QDialog): QtWidgets.QMessageBox.information( self, "UnRar Library Change", "ComicTagger will need to be restarted for changes to take effect.") - + def selectRar(self): self.selectFile(self.leRarExePath, "RAR") @@ -317,10 +323,23 @@ class SettingsWindow(QtWidgets.QDialog): dialog.setWindowTitle("Find " + name + " program") else: dialog.setWindowTitle("Find " + name + " library") - + if (dialog.exec_()): fileList = dialog.selectedFiles() control.setText(str(fileList[0])) def showRenameTab(self): self.tabWidget.setCurrentIndex(5) + + def showTemplateHelp(self): + TemplateHelpWin = TemplateHelpWindow(self) + TemplateHelpWin.exec_() + +class TemplateHelpWindow(QtWidgets.QDialog): + + def __init__(self, parent): + super(TemplateHelpWindow, self).__init__(parent) + + uic.loadUi(ComicTaggerSettings.getUIFile('TemplateHelp.ui'), self) + + diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index 35c9ae5..9d799dd 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -25,6 +25,7 @@ import json import webbrowser import re import pickle +import datetime #import signal from PyQt5 import QtCore, QtGui, QtWidgets, uic @@ -87,7 +88,7 @@ class TaggerWindow(QtWidgets.QMainWindow): def __init__(self, file_list, settings, parent=None, opts=None): super(TaggerWindow, self).__init__(parent) - + uic.loadUi(ComicTaggerSettings.getUIFile('taggerwindow.ui'), self) self.settings = settings @@ -184,7 +185,7 @@ class TaggerWindow(QtWidgets.QMainWindow): self.resetApp() # set up some basic field validators - validator = QtGui.QIntValidator(1900, 2099, self) + validator = QtGui.QIntValidator(1900, datetime.date.today().year + 15, self) self.lePubYear.setValidator(validator) validator = QtGui.QIntValidator(1, 12, self) @@ -294,7 +295,7 @@ class TaggerWindow(QtWidgets.QMainWindow): self.setWindowIcon( QtGui.QIcon(ComicTaggerSettings.getGraphic('app.png'))) - + if self.comic_archive is None: self.setWindowTitle(self.appName) else: @@ -610,7 +611,7 @@ class TaggerWindow(QtWidgets.QMainWindow): ) local = QUrl(str(absCFURL[0])).toLocalFile() - + return local def dropEvent(self, event): @@ -1195,11 +1196,11 @@ class TaggerWindow(QtWidgets.QMainWindow): def updateCreditColors(self): #!!!ATB qt5 porting TODO - #return + #return inactive_color = QtGui.QColor(255, 170, 150) active_palette = self.leSeries.palette() active_color = active_palette.color(QtGui.QPalette.Base) - + inactive_brush = QtGui.QBrush(inactive_color) active_brush = QtGui.QBrush(active_color) diff --git a/comictaggerlib/ui/TemplateHelp.ui b/comictaggerlib/ui/TemplateHelp.ui new file mode 100644 index 0000000..4540fc1 --- /dev/null +++ b/comictaggerlib/ui/TemplateHelp.ui @@ -0,0 +1,105 @@ + + + Dialog + + + + 0 + 0 + 702 + 452 + + + + Template Help + + + true + + + + 0 + + + 2 + + + 2 + + + + + <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) +{alternateinteger} (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} (integer) +{isVersionOf} (boolean) +{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> + + + true + + + + + + + + diff --git a/comictaggerlib/ui/settingswindow.ui b/comictaggerlib/ui/settingswindow.ui index d7f1457..9b97e76 100644 --- a/comictaggerlib/ui/settingswindow.ui +++ b/comictaggerlib/ui/settingswindow.ui @@ -503,7 +503,7 @@ QFormLayout::AllNonFixedFieldsGrow - + Template: @@ -512,31 +512,79 @@ - <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> + <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} (number) +{year} (number) +{day} (number) +{date} (Date) +{issueCount} (number) +{volume} (number) +{genre} (string) +{language} (string) +{comments} (string) +{volumeCount} (number) +{criticalRating} (string) +{country} (string) +{alternateSeries} (string) +{alternateNumber} (string) +{alternateCount} (number) +{imprint} (string) +{notes} (string) +{webLink} (string) +{format} (string) +{manga} (string) +{blackAndWhite} (boolean) +{pageCount} (number) +{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} (number) +{isVersionOf} (boolean) +{rights} (string) +{identifier} (string) +{lastMark} (string) +{coverImage} (string) + +Examples: + +{series} {issue} ({year}) + +{series} #{issue} - {title} +</pre> - + + + Template Help + + + + + Issue # Zero Padding - + @@ -555,7 +603,7 @@ - + <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> @@ -565,13 +613,33 @@ - + Change Extension Based On Archive Type + + + + If checked moves files to specified folder + + + Move files when renaming + + + + + + + Destination Directory: + + + + + + diff --git a/requirements.txt b/requirements.txt index a20fb10..511943f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ natsort==3.5.2 PyPDF2==1.24 pillow>=4.3.0 PyQt5>=5.10.1 -git+https://github.com/pyinstaller/pyinstaller@develop \ No newline at end of file +git+https://github.com/pyinstaller/pyinstaller@develop +pathvalidate