diff --git a/NEWS.txt b/NEWS.txt index 7bd9ba336..8babfed17 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -16,6 +16,8 @@ * Main window is now emitting a "selection_updated" signal, plugin api version bumps to 1.3.0 * Append system information to user-agent string * Compilation tag/variable aligned with iTunes, set only for Various Artists type compilations. + * Ignore directories and files while indexing when show_hidden_files option is set to False (PICARD-528) + * Add ignore_regex option which allows one to ignore matching paths, can be set in Options > Advanced (PICARD-528) Version 1.2 - 2013-03-30 * Picard now requires at least Python 2.6 diff --git a/picard/tagger.py b/picard/tagger.py index 52e5c42cf..0158272ed 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -68,7 +68,8 @@ from picard.util import ( thread, mbid_validate, check_io_encoding, - uniqify + uniqify, + is_hidden_path, ) from picard.webservice import XmlWebService @@ -271,9 +272,20 @@ class Tagger(QtGui.QApplication): def add_files(self, filenames, target=None): """Add files to the tagger.""" log.debug("Adding files %r", filenames) + ignoreregex = None + pattern = config.setting['ignore_regex'] + if pattern: + ignoreregex = re.compile(pattern) + ignore_hidden = not config.persist["show_hidden_files"] new_files = [] for filename in filenames: filename = os.path.normpath(os.path.realpath(filename)) + if ignore_hidden and is_hidden_path(filename): + log.debug("File ignored (hidden): %s" % (filename)) + continue + if ignoreregex is not None and ignoreregex.search(filename): + log.info("File ignored (matching %s): %s" % (pattern, filename)) + continue if filename not in self.files: file = open_file(filename) if file: diff --git a/picard/ui/options/__init__.py b/picard/ui/options/__init__.py index da17d3b4b..b8cf15731 100644 --- a/picard/ui/options/__init__.py +++ b/picard/ui/options/__init__.py @@ -33,6 +33,7 @@ class OptionsPage(QtGui.QWidget): PARENT = None SORT_ORDER = 1000 ACTIVE = True + STYLESHEET_ERROR = "QWidget { background-color: #f55; color: white; font-weight:bold }" def info(self): raise NotImplementedError diff --git a/picard/ui/options/advanced.py b/picard/ui/options/advanced.py index 881312fa4..3f3593ed7 100644 --- a/picard/ui/options/advanced.py +++ b/picard/ui/options/advanced.py @@ -17,7 +17,12 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -from picard.ui.options import OptionsPage, register_options_page +from PyQt4.QtGui import QPalette, QColor +import re + +from picard import config +from picard.ui.options import OptionsPage, OptionsCheckError, register_options_page +from picard.ui.ui_options_advanced import Ui_AdvancedOptionsPage class AdvancedOptionsPage(OptionsPage): @@ -26,7 +31,39 @@ class AdvancedOptionsPage(OptionsPage): TITLE = N_("Advanced") PARENT = None SORT_ORDER = 90 - ACTIVE = False + ACTIVE = True + + options = [ + config.TextOption("setting", "ignore_regex", ""), + ] + + def __init__(self, parent=None): + super(AdvancedOptionsPage, self).__init__(parent) + self.ui = Ui_AdvancedOptionsPage() + self.ui.setupUi(self) + self.ui.ignore_regex.textChanged.connect(self.live_checker) + + def load(self): + self.ui.ignore_regex.setText(config.setting["ignore_regex"]) + + def save(self): + config.setting["ignore_regex"] = unicode(self.ui.ignore_regex.text()) + + def live_checker(self, text): + self.ui.regex_error.setStyleSheet("") + self.ui.regex_error.setText("") + try: + self.check() + except OptionsCheckError as e: + self.ui.regex_error.setStyleSheet(self.STYLESHEET_ERROR) + self.ui.regex_error.setText(e.info) + return + + def check(self): + try: + re.compile(unicode(self.ui.ignore_regex.text())) + except re.error as e: + raise OptionsCheckError(_("Regex Error"), str(e)) register_options_page(AdvancedOptionsPage) diff --git a/picard/ui/options/renaming.py b/picard/ui/options/renaming.py index 67ec77001..a487376b7 100644 --- a/picard/ui/options/renaming.py +++ b/picard/ui/options/renaming.py @@ -270,8 +270,6 @@ class RenamingOptionsPage(OptionsPage): file.metadata['musicbrainz_releasetrackid'] = 'eac99807-93d4-3668-9714-fa0c1b487ccf' return file - STYLESHEET_ERROR = "QWidget { background-color: #f55; color: white; font-weight:bold }" - def move_files_to_browse(self): path = QtGui.QFileDialog.getExistingDirectory(self, "", self.ui.move_files_to.text()) if path: diff --git a/picard/ui/options/scripting.py b/picard/ui/options/scripting.py index 6ec9d0e89..9df9c6e66 100644 --- a/picard/ui/options/scripting.py +++ b/picard/ui/options/scripting.py @@ -70,8 +70,6 @@ class ScriptingOptionsPage(OptionsPage): config.TextOption("setting", "tagger_script", ""), ] - STYLESHEET_ERROR = "QWidget { background-color: #f55; color: white; font-weight:bold }" - def __init__(self, parent=None): super(ScriptingOptionsPage, self).__init__(parent) self.ui = Ui_ScriptingOptionsPage() diff --git a/picard/ui/ui_options_advanced.py b/picard/ui/ui_options_advanced.py new file mode 100644 index 000000000..8d3fb9c82 --- /dev/null +++ b/picard/ui/ui_options_advanced.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ui/options_advanced.ui' +# +# Created: Wed Dec 25 02:35:20 2013 +# by: PyQt4 UI code generator 4.9.3 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + _fromUtf8 = lambda s: s + +class Ui_AdvancedOptionsPage(object): + def setupUi(self, AdvancedOptionsPage): + AdvancedOptionsPage.setObjectName(_fromUtf8("AdvancedOptionsPage")) + AdvancedOptionsPage.resize(338, 435) + self.vboxlayout = QtGui.QVBoxLayout(AdvancedOptionsPage) + self.vboxlayout.setObjectName(_fromUtf8("vboxlayout")) + self.groupBox = QtGui.QGroupBox(AdvancedOptionsPage) + self.groupBox.setObjectName(_fromUtf8("groupBox")) + self.gridlayout = QtGui.QGridLayout(self.groupBox) + self.gridlayout.setSpacing(2) + self.gridlayout.setObjectName(_fromUtf8("gridlayout")) + self.label_ignore_regex = QtGui.QLabel(self.groupBox) + self.label_ignore_regex.setObjectName(_fromUtf8("label_ignore_regex")) + self.gridlayout.addWidget(self.label_ignore_regex, 1, 0, 1, 1) + self.ignore_regex = QtGui.QLineEdit(self.groupBox) + self.ignore_regex.setObjectName(_fromUtf8("ignore_regex")) + self.gridlayout.addWidget(self.ignore_regex, 2, 0, 1, 1) + self.regex_error = QtGui.QLabel(self.groupBox) + self.regex_error.setText(_fromUtf8("")) + self.regex_error.setObjectName(_fromUtf8("regex_error")) + self.gridlayout.addWidget(self.regex_error, 3, 0, 1, 1) + self.vboxlayout.addWidget(self.groupBox) + spacerItem = QtGui.QSpacerItem(181, 21, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.vboxlayout.addItem(spacerItem) + + self.retranslateUi(AdvancedOptionsPage) + QtCore.QMetaObject.connectSlotsByName(AdvancedOptionsPage) + + def retranslateUi(self, AdvancedOptionsPage): + self.groupBox.setTitle(_("Advanced options")) + self.label_ignore_regex.setText(_("Ignore file paths matching the following regular expression:")) + diff --git a/picard/util/__init__.py b/picard/util/__init__.py index f4797fc79..03ea2dc5a 100644 --- a/picard/util/__init__.py +++ b/picard/util/__init__.py @@ -365,3 +365,9 @@ if sys.platform == 'win32': return ap1 == ap2 else: os_path_samefile = os.path.samefile + + +def is_hidden_path(path): + """Returns true if at least one element of the path starts with a dot""" + path = os.path.normpath(path) # we need to ignore /./ and /a/../ cases + return any(s.startswith('.') for s in path.split(os.sep)) diff --git a/test/test_utils.py b/test/test_utils.py index 12f863fe2..13462a7a0 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +import os.path import unittest from picard import util @@ -119,3 +120,18 @@ class SaveReleaseTypeScoresTest(unittest.TestCase): self.assertTrue("Single 0.50" in saved_scores) self.assertTrue("Other 0.00" in saved_scores) self.assertEqual(6, len(saved_scores.split())) + + +class HiddenPathTest(unittest.TestCase): + + def test(self): + self.assertEqual(util.is_hidden_path('/a/.b/c.mp3'), True) + self.assertEqual(util.is_hidden_path('/a/b/c.mp3'), False) + self.assertEqual(util.is_hidden_path('/a/.b/.c.mp3'), True) + self.assertEqual(util.is_hidden_path('/a/b/.c.mp3'), True) + self.assertEqual(util.is_hidden_path('c.mp3'), False) + self.assertEqual(util.is_hidden_path('.c.mp3'), True) + self.assertEqual(util.is_hidden_path('/a/./c.mp3'), False) + self.assertEqual(util.is_hidden_path('/a/./.c.mp3'), True) + self.assertEqual(util.is_hidden_path('/a/../c.mp3'), False) + self.assertEqual(util.is_hidden_path('/a/../.c.mp3'), True) diff --git a/ui/options_advanced.ui b/ui/options_advanced.ui new file mode 100644 index 000000000..0d8a399c5 --- /dev/null +++ b/ui/options_advanced.ui @@ -0,0 +1,63 @@ + + + AdvancedOptionsPage + + + + 0 + 0 + 338 + 435 + + + + + + + Advanced options + + + + 2 + + + + + Ignore file paths matching the following regular expression: + + + + + + + + + + + + + + + + + + + + Qt::Vertical + + + + 181 + 21 + + + + + + + + ignore_regex + + + +