PICARD-356: Allow user defined replacements for Windows incompatible characters

This commit is contained in:
Philipp Wolfer
2022-12-04 13:53:58 +01:00
parent 95c736a13e
commit cb339c6a4a
12 changed files with 774 additions and 35 deletions

View File

@@ -32,24 +32,29 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import re
from PyQt5 import (
QtCore,
QtGui,
QtWidgets,
)
from picard.config import (
BoolOption,
Option,
get_config,
)
from picard.const.sys import IS_WIN
from picard.util import system_supports_long_paths
from picard.ui import PicardDialog
from picard.ui.options import (
OptionsPage,
register_options_page,
)
from picard.ui.ui_options_renaming_compat import Ui_RenamingCompatOptionsPage
from picard.ui.ui_win_compat_dialog import Ui_WinCompatDialog
class RenamingCompatOptionsPage(OptionsPage):
@@ -65,21 +70,34 @@ class RenamingCompatOptionsPage(OptionsPage):
BoolOption("setting", "windows_long_paths", system_supports_long_paths() if IS_WIN else False),
BoolOption("setting", "ascii_filenames", False),
BoolOption("setting", "replace_spaces_with_underscores", False),
Option("setting", "win_compat_replacements", {
'*': '_',
':': '_',
'<': '_',
'>': '_',
'?': '_',
'|': '_',
'"': '_',
})
]
options_changed = QtCore.pyqtSignal(dict)
def __init__(self, parent=None):
super().__init__(parent)
config = get_config()
self.win_compat_replacements = config.setting["win_compat_replacements"]
self.ui = Ui_RenamingCompatOptionsPage()
self.ui.setupUi(self)
self.ui.ascii_filenames.toggled.connect(self.on_options_changed)
self.ui.windows_compatibility.toggled.connect(self.on_options_changed)
self.ui.windows_long_paths.toggled.connect(self.on_options_changed)
self.ui.replace_spaces_with_underscores.toggled.connect(self.on_options_changed)
self.ui.btn_windows_compatibility_change.clicked.connect(self.open_win_compat_dialog)
def load(self):
config = get_config()
self.win_compat_replacements = config.setting["win_compat_replacements"]
try:
self.ui.windows_long_paths.toggled.disconnect(self.toggle_windows_long_paths)
except TypeError:
@@ -123,7 +141,62 @@ class RenamingCompatOptionsPage(OptionsPage):
'windows_compatibility': self.ui.windows_compatibility.isChecked(),
'windows_long_paths': self.ui.windows_long_paths.isChecked(),
'replace_spaces_with_underscores': self.ui.replace_spaces_with_underscores.isChecked(),
'win_compat_replacements': self.win_compat_replacements,
}
def open_win_compat_dialog(self):
dialog = WinCompatDialog(self.win_compat_replacements, parent=self)
if dialog.exec_() == QtWidgets.QDialog.DialogCode.Accepted:
self.win_compat_replacements = dialog.replacements
self.on_options_changed()
class WinCompatReplacementValidator(QtGui.QValidator):
_re_valid_win_replacement = re.compile(r'^[^"*:<>?|/\\\s]?$')
def validate(self, text: str, pos):
if self._re_valid_win_replacement.match(text):
state = QtGui.QValidator.State.Acceptable
else:
state = QtGui.QValidator.State.Invalid
return state, text, pos
class WinCompatDialog(PicardDialog):
def __init__(self, replacements, parent=None):
super().__init__(parent)
self.replacements = dict(replacements)
self.ui = Ui_WinCompatDialog()
self.ui.setupUi(self)
self.ui.replace_asterisk.setValidator(WinCompatReplacementValidator())
self.ui.replace_colon.setValidator(WinCompatReplacementValidator())
self.ui.replace_gt.setValidator(WinCompatReplacementValidator())
self.ui.replace_lt.setValidator(WinCompatReplacementValidator())
self.ui.replace_pipe.setValidator(WinCompatReplacementValidator())
self.ui.replace_questionmark.setValidator(WinCompatReplacementValidator())
self.ui.replace_quotationmark.setValidator(WinCompatReplacementValidator())
self.ui.buttonbox.accepted.connect(self.accept)
self.ui.buttonbox.rejected.connect(self.reject)
self.load()
def load(self):
self.ui.replace_asterisk.setText(self.replacements['*'])
self.ui.replace_colon.setText(self.replacements[':'])
self.ui.replace_gt.setText(self.replacements['>'])
self.ui.replace_lt.setText(self.replacements['<'])
self.ui.replace_pipe.setText(self.replacements['|'])
self.ui.replace_questionmark.setText(self.replacements['?'])
self.ui.replace_quotationmark.setText(self.replacements['"'])
def accept(self):
self.replacements['*'] = self.ui.replace_asterisk.text()
self.replacements[':'] = self.ui.replace_colon.text()
self.replacements['>'] = self.ui.replace_gt.text()
self.replacements['<'] = self.ui.replace_lt.text()
self.replacements['|'] = self.ui.replace_pipe.text()
self.replacements['?'] = self.ui.replace_questionmark.text()
self.replacements['"'] = self.ui.replace_quotationmark.text()
super().accept()
register_options_page(RenamingCompatOptionsPage)

View File

@@ -11,7 +11,7 @@ class Ui_RenamingCompatOptionsPage(object):
def setupUi(self, RenamingCompatOptionsPage):
RenamingCompatOptionsPage.setObjectName("RenamingCompatOptionsPage")
RenamingCompatOptionsPage.setEnabled(True)
RenamingCompatOptionsPage.resize(453, 374)
RenamingCompatOptionsPage.resize(453, 332)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -22,9 +22,21 @@ class Ui_RenamingCompatOptionsPage(object):
self.ascii_filenames = QtWidgets.QCheckBox(RenamingCompatOptionsPage)
self.ascii_filenames.setObjectName("ascii_filenames")
self.verticalLayout_5.addWidget(self.ascii_filenames)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.windows_compatibility = QtWidgets.QCheckBox(RenamingCompatOptionsPage)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.windows_compatibility.sizePolicy().hasHeightForWidth())
self.windows_compatibility.setSizePolicy(sizePolicy)
self.windows_compatibility.setObjectName("windows_compatibility")
self.verticalLayout_5.addWidget(self.windows_compatibility)
self.horizontalLayout.addWidget(self.windows_compatibility)
self.btn_windows_compatibility_change = QtWidgets.QPushButton(RenamingCompatOptionsPage)
self.btn_windows_compatibility_change.setEnabled(False)
self.btn_windows_compatibility_change.setObjectName("btn_windows_compatibility_change")
self.horizontalLayout.addWidget(self.btn_windows_compatibility_change)
self.verticalLayout_5.addLayout(self.horizontalLayout)
self.windows_long_paths = QtWidgets.QCheckBox(RenamingCompatOptionsPage)
self.windows_long_paths.setEnabled(False)
self.windows_long_paths.setObjectName("windows_long_paths")
@@ -32,6 +44,8 @@ class Ui_RenamingCompatOptionsPage(object):
self.replace_spaces_with_underscores = QtWidgets.QCheckBox(RenamingCompatOptionsPage)
self.replace_spaces_with_underscores.setObjectName("replace_spaces_with_underscores")
self.verticalLayout_5.addWidget(self.replace_spaces_with_underscores)
spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout_5.addItem(spacerItem)
self.example_selection_note = QtWidgets.QLabel(RenamingCompatOptionsPage)
self.example_selection_note.setText("")
self.example_selection_note.setWordWrap(True)
@@ -40,13 +54,14 @@ class Ui_RenamingCompatOptionsPage(object):
self.retranslateUi(RenamingCompatOptionsPage)
self.windows_compatibility.toggled['bool'].connect(self.windows_long_paths.setEnabled) # type: ignore
self.windows_compatibility.toggled['bool'].connect(self.btn_windows_compatibility_change.setEnabled) # type: ignore
QtCore.QMetaObject.connectSlotsByName(RenamingCompatOptionsPage)
RenamingCompatOptionsPage.setTabOrder(self.ascii_filenames, self.windows_compatibility)
RenamingCompatOptionsPage.setTabOrder(self.windows_compatibility, self.windows_long_paths)
RenamingCompatOptionsPage.setTabOrder(self.ascii_filenames, self.windows_long_paths)
def retranslateUi(self, RenamingCompatOptionsPage):
_translate = QtCore.QCoreApplication.translate
self.ascii_filenames.setText(_("Replace non-ASCII characters"))
self.windows_compatibility.setText(_("Windows compatibility"))
self.btn_windows_compatibility_change.setText(_("Customize..."))
self.windows_long_paths.setText(_("Allow paths longer than 259 characters"))
self.replace_spaces_with_underscores.setText(_("Replace spaces with underscores"))

View File

@@ -0,0 +1,193 @@
# -*- coding: utf-8 -*-
# Automatically generated - don't edit.
# Use `python setup.py build_ui` to update it.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_WinCompatDialog(object):
def setupUi(self, WinCompatDialog):
WinCompatDialog.setObjectName("WinCompatDialog")
WinCompatDialog.setWindowModality(QtCore.Qt.WindowModal)
WinCompatDialog.resize(285, 295)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(1)
sizePolicy.setVerticalStretch(1)
sizePolicy.setHeightForWidth(WinCompatDialog.sizePolicy().hasHeightForWidth())
WinCompatDialog.setSizePolicy(sizePolicy)
self.verticalLayout = QtWidgets.QVBoxLayout(WinCompatDialog)
self.verticalLayout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize)
self.verticalLayout.setObjectName("verticalLayout")
self.gridLayout = QtWidgets.QGridLayout()
self.gridLayout.setObjectName("gridLayout")
self.label_header_character = QtWidgets.QLabel(WinCompatDialog)
font = QtGui.QFont()
font.setBold(True)
font.setWeight(75)
self.label_header_character.setFont(font)
self.label_header_character.setObjectName("label_header_character")
self.gridLayout.addWidget(self.label_header_character, 0, 0, 1, 1)
self.label_header_replace = QtWidgets.QLabel(WinCompatDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.label_header_replace.sizePolicy().hasHeightForWidth())
self.label_header_replace.setSizePolicy(sizePolicy)
font = QtGui.QFont()
font.setBold(True)
font.setWeight(75)
self.label_header_replace.setFont(font)
self.label_header_replace.setObjectName("label_header_replace")
self.gridLayout.addWidget(self.label_header_replace, 0, 2, 1, 1)
self.label_lt = QtWidgets.QLabel(WinCompatDialog)
font = QtGui.QFont()
font.setFamily("Monospace")
self.label_lt.setFont(font)
self.label_lt.setObjectName("label_lt")
self.gridLayout.addWidget(self.label_lt, 3, 0, 1, 1)
self.label_colon = QtWidgets.QLabel(WinCompatDialog)
font = QtGui.QFont()
font.setFamily("Monospace")
self.label_colon.setFont(font)
self.label_colon.setObjectName("label_colon")
self.gridLayout.addWidget(self.label_colon, 2, 0, 1, 1)
self.label_gt = QtWidgets.QLabel(WinCompatDialog)
font = QtGui.QFont()
font.setFamily("Monospace")
self.label_gt.setFont(font)
self.label_gt.setObjectName("label_gt")
self.gridLayout.addWidget(self.label_gt, 4, 0, 1, 1)
self.replace_questionmark = QtWidgets.QLineEdit(WinCompatDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.replace_questionmark.sizePolicy().hasHeightForWidth())
self.replace_questionmark.setSizePolicy(sizePolicy)
self.replace_questionmark.setMaximumSize(QtCore.QSize(20, 16777215))
self.replace_questionmark.setMaxLength(1)
self.replace_questionmark.setObjectName("replace_questionmark")
self.gridLayout.addWidget(self.replace_questionmark, 5, 2, 1, 1)
self.label_questionmark = QtWidgets.QLabel(WinCompatDialog)
font = QtGui.QFont()
font.setFamily("Monospace")
self.label_questionmark.setFont(font)
self.label_questionmark.setObjectName("label_questionmark")
self.gridLayout.addWidget(self.label_questionmark, 5, 0, 1, 1)
self.label_pipe = QtWidgets.QLabel(WinCompatDialog)
font = QtGui.QFont()
font.setFamily("Monospace")
self.label_pipe.setFont(font)
self.label_pipe.setObjectName("label_pipe")
self.gridLayout.addWidget(self.label_pipe, 6, 0, 1, 1)
self.replace_asterisk = QtWidgets.QLineEdit(WinCompatDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.replace_asterisk.sizePolicy().hasHeightForWidth())
self.replace_asterisk.setSizePolicy(sizePolicy)
self.replace_asterisk.setMaximumSize(QtCore.QSize(20, 16777215))
self.replace_asterisk.setMaxLength(1)
self.replace_asterisk.setObjectName("replace_asterisk")
self.gridLayout.addWidget(self.replace_asterisk, 1, 2, 1, 1)
self.replace_gt = QtWidgets.QLineEdit(WinCompatDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.replace_gt.sizePolicy().hasHeightForWidth())
self.replace_gt.setSizePolicy(sizePolicy)
self.replace_gt.setMaximumSize(QtCore.QSize(20, 16777215))
self.replace_gt.setMaxLength(1)
self.replace_gt.setObjectName("replace_gt")
self.gridLayout.addWidget(self.replace_gt, 4, 2, 1, 1)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.gridLayout.addItem(spacerItem, 0, 3, 1, 1)
self.replace_lt = QtWidgets.QLineEdit(WinCompatDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.replace_lt.sizePolicy().hasHeightForWidth())
self.replace_lt.setSizePolicy(sizePolicy)
self.replace_lt.setMaximumSize(QtCore.QSize(20, 16777215))
self.replace_lt.setMaxLength(1)
self.replace_lt.setObjectName("replace_lt")
self.gridLayout.addWidget(self.replace_lt, 3, 2, 1, 1)
self.label_asterisk = QtWidgets.QLabel(WinCompatDialog)
font = QtGui.QFont()
font.setFamily("Monospace")
self.label_asterisk.setFont(font)
self.label_asterisk.setObjectName("label_asterisk")
self.gridLayout.addWidget(self.label_asterisk, 1, 0, 1, 1)
self.replace_pipe = QtWidgets.QLineEdit(WinCompatDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.replace_pipe.sizePolicy().hasHeightForWidth())
self.replace_pipe.setSizePolicy(sizePolicy)
self.replace_pipe.setMaximumSize(QtCore.QSize(20, 16777215))
self.replace_pipe.setMaxLength(1)
self.replace_pipe.setObjectName("replace_pipe")
self.gridLayout.addWidget(self.replace_pipe, 6, 2, 1, 1)
self.replace_colon = QtWidgets.QLineEdit(WinCompatDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.replace_colon.sizePolicy().hasHeightForWidth())
self.replace_colon.setSizePolicy(sizePolicy)
self.replace_colon.setMaximumSize(QtCore.QSize(20, 16777215))
self.replace_colon.setMaxLength(1)
self.replace_colon.setObjectName("replace_colon")
self.gridLayout.addWidget(self.replace_colon, 2, 2, 1, 1)
self.label_quotationmark = QtWidgets.QLabel(WinCompatDialog)
font = QtGui.QFont()
font.setFamily("Monospace")
self.label_quotationmark.setFont(font)
self.label_quotationmark.setObjectName("label_quotationmark")
self.gridLayout.addWidget(self.label_quotationmark, 7, 0, 1, 1)
self.replace_quotationmark = QtWidgets.QLineEdit(WinCompatDialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.replace_quotationmark.sizePolicy().hasHeightForWidth())
self.replace_quotationmark.setSizePolicy(sizePolicy)
self.replace_quotationmark.setMaximumSize(QtCore.QSize(20, 16777215))
self.replace_quotationmark.setObjectName("replace_quotationmark")
self.gridLayout.addWidget(self.replace_quotationmark, 7, 2, 1, 1)
self.verticalLayout.addLayout(self.gridLayout)
spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem1)
self.buttonbox = QtWidgets.QDialogButtonBox(WinCompatDialog)
self.buttonbox.setOrientation(QtCore.Qt.Horizontal)
self.buttonbox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Save)
self.buttonbox.setObjectName("buttonbox")
self.verticalLayout.addWidget(self.buttonbox)
self.retranslateUi(WinCompatDialog)
QtCore.QMetaObject.connectSlotsByName(WinCompatDialog)
WinCompatDialog.setTabOrder(self.replace_asterisk, self.replace_colon)
WinCompatDialog.setTabOrder(self.replace_colon, self.replace_lt)
WinCompatDialog.setTabOrder(self.replace_lt, self.replace_gt)
WinCompatDialog.setTabOrder(self.replace_gt, self.replace_questionmark)
WinCompatDialog.setTabOrder(self.replace_questionmark, self.replace_pipe)
WinCompatDialog.setTabOrder(self.replace_pipe, self.replace_quotationmark)
def retranslateUi(self, WinCompatDialog):
_translate = QtCore.QCoreApplication.translate
WinCompatDialog.setWindowTitle(_("Windows compatibility"))
self.label_header_character.setText(_("Character"))
self.label_header_replace.setText(_("Replacement"))
self.label_lt.setText(_("<"))
self.label_colon.setText(_(":"))
self.label_gt.setText(_(">"))
self.replace_questionmark.setText(_("_"))
self.label_questionmark.setText(_("?"))
self.label_pipe.setText(_("|"))
self.replace_asterisk.setText(_("_"))
self.replace_gt.setText(_("_"))
self.replace_lt.setText(_("_"))
self.label_asterisk.setText(_("*"))
self.replace_pipe.setText(_("_"))
self.replace_colon.setText(_("_"))
self.label_quotationmark.setText(_("\""))
self.replace_quotationmark.setText(_("_"))

View File

@@ -39,7 +39,10 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
from collections import namedtuple
from collections import (
defaultdict,
namedtuple,
)
from collections.abc import Mapping
from itertools import chain
import json
@@ -293,16 +296,21 @@ def sanitize_date(datestr):
return ("", "%04d", "%04d-%02d", "%04d-%02d-%02d")[len(date)] % tuple(date)
_re_win32_incompat = re.compile(r'["*:<>?|]', re.UNICODE)
def replace_win32_incompat(string, repl="_"): # noqa: E302
def replace_win32_incompat(string, repl="_", replacements=None): # noqa: E302
"""Replace win32 filename incompatible characters from ``string`` by
``repl``."""
# Don't replace : with _ for windows drive
# Don't replace : for windows drive
if IS_WIN and os.path.isabs(string):
drive, rest = ntpath.splitdrive(string)
return drive + _re_win32_incompat.sub(repl, rest)
drive, string = ntpath.splitdrive(string)
else:
return _re_win32_incompat.sub(repl, string)
drive = ''
replacements = defaultdict(lambda: repl, replacements or {})
for char in {'"', '*', ':', '<', '>', '?', '|'}:
if char in string:
string = string.replace(char, replacements[char])
return drive + string
_re_non_alphanum = re.compile(r'\W+', re.UNICODE)

View File

@@ -66,7 +66,7 @@ def script_to_filename_with_metadata(naming_format, metadata, file=None, setting
filename = replace_non_ascii(filename, pathsave=True, win_compat=win_compat)
# replace incompatible characters
if win_compat:
filename = replace_win32_incompat(filename)
filename = replace_win32_incompat(filename, replacements=settings["win_compat_replacements"])
if settings["replace_spaces_with_underscores"]:
filename = _re_replace_underscores.sub('_', filename.strip())
# remove null characters

View File

@@ -63,6 +63,7 @@ settings = {
'remove_wave_riff_info': False,
'wave_riff_info_encoding': 'iso-8859-1',
'replace_spaces_with_underscores': False,
'win_compat_replacements': {},
}

View File

@@ -166,6 +166,7 @@ class CoverArtImageTest(PicardTestCase):
self.set_config_values({
'image_type_as_filename': True,
'windows_compatibility': True,
'win_compat_replacements': {},
'windows_long_paths': False,
'replace_spaces_with_underscores': False,
'enabled_plugins': [],
@@ -197,6 +198,7 @@ class CoverArtImageMakeFilenameTest(PicardTestCase):
self.metadata = Metadata()
self.set_config_values({
'windows_compatibility': False,
'win_compat_replacements': {},
'enabled_plugins': [],
'ascii_filenames': False,
'replace_spaces_with_underscores': False,

View File

@@ -210,6 +210,7 @@ class FileNamingTest(PicardTestCase):
'move_files': False,
'rename_files': False,
'windows_compatibility': True,
'win_compat_replacements': {},
'windows_long_paths': False,
'replace_spaces_with_underscores': False,
'file_renaming_scripts': {'test_id': {'script': '%album%/%title%'}},

View File

@@ -39,6 +39,7 @@ settings = {
'ascii_filenames': False,
'enabled_plugins': [],
'windows_compatibility': False,
'win_compat_replacements': {},
'replace_spaces_with_underscores': False,
}
@@ -116,6 +117,19 @@ class ScriptToFilenameTest(PicardTestCase):
filename = script_to_filename('%artist%?', metadata, settings=settings)
self.assertEqual(expect_compat, filename)
def test_windows_compatibility_custom_replacements(self):
metadata = Metadata()
metadata['artist'] = '\\*:'
expect_compat = '_+_!'
settings = config.setting.copy()
settings['windows_compatibility'] = True
settings['win_compat_replacements'] = {
'*': '+',
'?': '!',
}
filename = script_to_filename('%artist%?', metadata, settings=settings)
self.assertEqual(expect_compat, filename)
def test_replace_spaces_with_underscores(self):
metadata = Metadata()
metadata['artist'] = ' The \t New* _ Artist '

View File

@@ -88,27 +88,62 @@ class ReplaceWin32IncompatTest(PicardTestCase):
@unittest.skipUnless(IS_WIN, "windows test")
def test_correct_absolute_win32(self):
self.assertEqual(util.replace_win32_incompat("c:\\test\\te\"st/2"),
"c:\\test\\te_st/2")
self.assertEqual(util.replace_win32_incompat("c:\\test\\d:/2"),
"c:\\test\\d_/2")
self.assertEqual(util.replace_win32_incompat('c:\\test\\te"st/2'),
'c:\\test\\te_st/2')
self.assertEqual(util.replace_win32_incompat('c:\\test\\d:/2'),
'c:\\test\\d_/2')
@unittest.skipUnless(not IS_WIN, "non-windows test")
@unittest.skipUnless(not IS_WIN, 'non-windows test')
def test_correct_absolute_non_win32(self):
self.assertEqual(util.replace_win32_incompat("/test/te\"st/2"),
"/test/te_st/2")
self.assertEqual(util.replace_win32_incompat("/test/d:/2"),
"/test/d_/2")
self.assertEqual(util.replace_win32_incompat('/test/te"st/2'),
'/test/te_st/2')
self.assertEqual(util.replace_win32_incompat('/test/d:/2'),
'/test/d_/2')
def test_correct_relative(self):
self.assertEqual(util.replace_win32_incompat("A\"*:<>?|b"),
"A_______b")
self.assertEqual(util.replace_win32_incompat("d:tes<t"),
"d_tes_t")
self.assertEqual(util.replace_win32_incompat('A"*:<>?|b'),
'A_______b')
self.assertEqual(util.replace_win32_incompat('d:tes<t'),
'd_tes_t')
def test_incorrect(self):
self.assertNotEqual(util.replace_win32_incompat("c:\\test\\te\"st2"),
"c:\\test\\te\"st2")
self.assertNotEqual(util.replace_win32_incompat('c:\\test\\te"st2'),
'c:\\test\\te"st2')
def test_custom_replacement_char(self):
self.assertEqual(util.replace_win32_incompat('A"*:<>?|b', repl='+'),
"A+++++++b")
def test_custom_replacement_map(self):
input = 'foo*:<>?|"'
replacments = {
'*': 'A',
':': 'B',
'<': 'C',
'>': 'D',
'?': 'E',
'|': 'F',
'"': 'G',
}
replaced = util.replace_win32_incompat(input, replacements=replacments)
self.assertEqual('fooABCDEFG', replaced)
def test_partial_replacement_map(self):
input = 'foo*:<>?|"'
replacments = {
'*': 'A',
'<': 'C',
}
replaced = util.replace_win32_incompat(input, repl='-', replacements=replacments)
self.assertEqual('fooA-C----', replaced)
def test_empty_string_replacement_map(self):
input = 'foo:bar'
replacments = {
':': '',
}
replaced = util.replace_win32_incompat(input, replacements=replacments)
self.assertEqual('foobar', replaced)
class MakeFilenameTest(PicardTestCase):

View File

@@ -10,7 +10,7 @@
<x>0</x>
<y>0</y>
<width>453</width>
<height>374</height>
<height>332</height>
</rect>
</property>
<property name="sizePolicy">
@@ -19,7 +19,7 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5" stretch="0,0,0,0,0">
<layout class="QVBoxLayout" name="verticalLayout_5" stretch="0,0,0,0,0,0">
<item>
<widget class="QCheckBox" name="ascii_filenames">
<property name="text">
@@ -27,13 +27,33 @@
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QCheckBox" name="windows_compatibility">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Windows compatibility</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btn_windows_compatibility_change">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Customize...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="windows_long_paths">
<property name="enabled">
@@ -51,6 +71,19 @@
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="example_selection_note">
<property name="text">
@@ -65,7 +98,6 @@
</widget>
<tabstops>
<tabstop>ascii_filenames</tabstop>
<tabstop>windows_compatibility</tabstop>
<tabstop>windows_long_paths</tabstop>
</tabstops>
<resources/>
@@ -86,5 +118,21 @@
</hint>
</hints>
</connection>
<connection>
<sender>windows_compatibility</sender>
<signal>toggled(bool)</signal>
<receiver>btn_windows_compatibility_change</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>178</x>
<y>51</y>
</hint>
<hint type="destinationlabel">
<x>397</x>
<y>51</y>
</hint>
</hints>
</connection>
</connections>
</ui>

349
ui/win_compat_dialog.ui Normal file
View File

@@ -0,0 +1,349 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>WinCompatDialog</class>
<widget class="QDialog" name="WinCompatDialog">
<property name="windowModality">
<enum>Qt::WindowModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>285</width>
<height>295</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>1</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Windows compatibility</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetFixedSize</enum>
</property>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_header_character">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Character</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label_header_replace">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Replacement</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_lt">
<property name="font">
<font>
<family>Monospace</family>
</font>
</property>
<property name="text">
<string>&lt;</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_colon">
<property name="font">
<font>
<family>Monospace</family>
</font>
</property>
<property name="text">
<string>:</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_gt">
<property name="font">
<font>
<family>Monospace</family>
</font>
</property>
<property name="text">
<string>&gt;</string>
</property>
</widget>
</item>
<item row="5" column="2">
<widget class="QLineEdit" name="replace_questionmark">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>20</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>_</string>
</property>
<property name="maxLength">
<number>1</number>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_questionmark">
<property name="font">
<font>
<family>Monospace</family>
</font>
</property>
<property name="text">
<string>?</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_pipe">
<property name="font">
<font>
<family>Monospace</family>
</font>
</property>
<property name="text">
<string>|</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLineEdit" name="replace_asterisk">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>20</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>_</string>
</property>
<property name="maxLength">
<number>1</number>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QLineEdit" name="replace_gt">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>20</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>_</string>
</property>
<property name="maxLength">
<number>1</number>
</property>
</widget>
</item>
<item row="0" column="3">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="2">
<widget class="QLineEdit" name="replace_lt">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>20</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>_</string>
</property>
<property name="maxLength">
<number>1</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_asterisk">
<property name="font">
<font>
<family>Monospace</family>
</font>
</property>
<property name="text">
<string>*</string>
</property>
</widget>
</item>
<item row="6" column="2">
<widget class="QLineEdit" name="replace_pipe">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>20</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>_</string>
</property>
<property name="maxLength">
<number>1</number>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLineEdit" name="replace_colon">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>20</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>_</string>
</property>
<property name="maxLength">
<number>1</number>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_quotationmark">
<property name="font">
<font>
<family>Monospace</family>
</font>
</property>
<property name="text">
<string>&quot;</string>
</property>
</widget>
</item>
<item row="7" column="2">
<widget class="QLineEdit" name="replace_quotationmark">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>20</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>_</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonbox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>replace_asterisk</tabstop>
<tabstop>replace_colon</tabstop>
<tabstop>replace_lt</tabstop>
<tabstop>replace_gt</tabstop>
<tabstop>replace_questionmark</tabstop>
<tabstop>replace_pipe</tabstop>
<tabstop>replace_quotationmark</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>