diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py
index 688907d..25acae1 100644
--- a/comictaggerlib/cli.py
+++ b/comictaggerlib/cli.py
@@ -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)"
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..6ff51c1 100644
--- a/comictaggerlib/renamewindow.py
+++ b/comictaggerlib/renamewindow.py
@@ -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!'
+ '
{}
'
+ 'Please 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'.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)
diff --git a/comictaggerlib/settings.py b/comictaggerlib/settings.py
index 8808788..3442527 100644
--- a/comictaggerlib/settings.py
+++ b/comictaggerlib/settings.py
@@ -108,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
@@ -301,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(
@@ -462,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')
diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py
index 0149a12..79fb11d 100644
--- a/comictaggerlib/settingswindow.py
+++ b/comictaggerlib/settingswindow.py
@@ -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
@@ -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):
@@ -165,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!'
+ '
{}
'
+ 'Please 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'.format(e))
+ return
+
# Copy values from form to settings and save
self.settings.rar_exe_path = str(self.leRarExePath.text())
@@ -210,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)
@@ -268,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)
+
+
diff --git a/comictaggerlib/ui/TemplateHelp.ui b/comictaggerlib/ui/TemplateHelp.ui
new file mode 100644
index 0000000..2b581ed
--- /dev/null
+++ b/comictaggerlib/ui/TemplateHelp.ui
@@ -0,0 +1,107 @@
+
+
+ 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)
+{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>
+
+
+ true
+
+
+
+
+
+
+
+
diff --git a/comictaggerlib/ui/settingswindow.ui b/comictaggerlib/ui/settingswindow.ui
index c64f5f5..82805c5 100644
--- a/comictaggerlib/ui/settingswindow.ui
+++ b/comictaggerlib/ui/settingswindow.ui
@@ -503,7 +503,7 @@
QFormLayout::AllNonFixedFieldsGrow
-
-
+
Template:
@@ -512,31 +512,80 @@
-
- <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} (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>
-
-
+
+
+ Template Help
+
+
+
+ -
+
Issue # Zero Padding
- -
+
-
@@ -555,7 +604,7 @@
- -
+
-
<html><head/><body><p><span style=" font-weight:600;">"Smart Text Cleanup" </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 +614,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 792c868..e5a7087 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,3 +4,4 @@ configparser
natsort
pillow>=4.3.0
requests
+pathvalidate