diff --git a/beets/library.py b/beets/library.py index 8d95561f7..35b763321 100644 --- a/beets/library.py +++ b/beets/library.py @@ -24,6 +24,7 @@ import unicodedata import time import re from unidecode import unidecode +import platform from beets import logging from beets.mediafile import MediaFile, MutagenError, UnreadableFileError @@ -42,30 +43,44 @@ log = logging.getLogger('beets') # Library-specific query types. class PathQuery(dbcore.FieldQuery): - """A query that matches all items under a given path.""" + """A query that matches all items under a given path. + + On Windows paths are case-insensitive, contratly to UNIX platforms. + """ escape_re = re.compile(r'[\\_%]') escape_char = b'\\' + _is_windows = platform.system() == 'Windows' + def __init__(self, field, pattern, fast=True): super(PathQuery, self).__init__(field, pattern, fast) + if self._is_windows: + pattern = pattern.lower() + # Match the path as a single file. self.file_path = util.bytestring_path(util.normpath(pattern)) # As a directory (prefix). self.dir_path = util.bytestring_path(os.path.join(self.file_path, b'')) def match(self, item): - return (item.path == self.file_path) or \ - item.path.startswith(self.dir_path) + path = item.path.lower() if self._is_windows else item.path + return (path == self.file_path) or path.startswith(self.dir_path) def col_clause(self): + file_blob = buffer(self.file_path) + + if not self._is_windows: + dir_blob = buffer(self.dir_path) + return '({0} = ?) || (instr({0}, ?) = 1)'.format(self.field), \ + (file_blob, dir_blob) + escape = lambda m: self.escape_char + m.group(0) dir_pattern = self.escape_re.sub(escape, self.dir_path) - dir_pattern = buffer(dir_pattern + b'%') - file_blob = buffer(self.file_path) + dir_blob = buffer(dir_pattern + b'%') return '({0} = ?) || ({0} LIKE ? ESCAPE ?)'.format(self.field), \ - (file_blob, dir_pattern, self.escape_char) + (file_blob, dir_blob, self.escape_char) # Library-specific field types. diff --git a/test/test_query.py b/test/test_query.py index a9b1058bd..c6aec6185 100644 --- a/test/test_query.py +++ b/test/test_query.py @@ -17,6 +17,8 @@ from __future__ import (division, absolute_import, print_function, unicode_literals) +from mock import patch + from test import _common from test._common import unittest from test import helper @@ -461,6 +463,17 @@ class PathQueryTest(_common.LibTestCase, TestHelper, AssertsMixin): results = self.lib.albums(q) self.assert_albums_matched(results, ['album with backslash']) + def test_case_sensitivity(self): + self.add_album(path='/A/B/C2.mp3', title='caps path') + q = b'path:/A/B' + with patch('beets.library.PathQuery._is_windows', False): + results = self.lib.items(q) + self.assert_items_matched(results, ['caps path']) + + with patch('beets.library.PathQuery._is_windows', True): + results = self.lib.items(q) + self.assert_items_matched(results, ['path item', 'caps path']) + class IntQueryTest(unittest.TestCase, TestHelper):