Merge remote-tracking branch 'andrzejc/pathrender' into develop

This commit is contained in:
rembo10
2016-04-05 12:02:59 +01:00
3 changed files with 152 additions and 22 deletions

View File

@@ -894,7 +894,7 @@
<div class="row">
as <input type="text" class="override-float" name="album_art_format" value="${config['album_art_format']}" size="10">.jpg
</div>
<small>Use $Artist/$artist, $Album/$album, $Year/$year, put optional variables in square brackets, use single-quote marks to escape square brackets literally ('[', ']').</small>
<small>Use $Artist/$artist, $Album/$album, $Year/$year, put optional variables in curly braces, use single-quote marks to escape curly braces literally ('{', '}').</small>
</div>
<div class="row checkbox left clearfix nopad">
<label>
@@ -1289,13 +1289,13 @@
<div class="row">
<label>Folder Format</label>
<input type="text" name="folder_format" value="${config['folder_format']}" size="43">
<small>Use: $Artist/$artist, $SortArtist/$sortartist, $Album/$album, $Year/$year, $Type/$type (release type) and $First/$first (first letter in artist name), $OriginalFolder/$originalfolder (downloaded directory name). Put optional variables in square brackets, use single-quote marks to escape square brackets literally ('[', ']').<br>E.g.: $Type/$First/$artist/$album '['$year']' = Album/G/girl talk/all day [2010]</small>
<small>Use: $Artist/$artist, $SortArtist/$sortartist, $Album/$album, $Year/$year, $Type/$type (release type) and $First/$first (first letter in artist name), $OriginalFolder/$originalfolder (downloaded directory name). Put optional variables in curly braces, use single-quote marks to escape curly braces literally ('{', '}').<br>E.g.: $Type/$First/$artist/$album{ '['$year']'} = Album/G/girl talk/all day [2010]</small>
</div>
<div class="row">
<label>File Format</label>
<input type="text" name="file_format" value="${config['file_format']}" size="43">
<small>Use: $Disc/$disc (disc #), $Track/$track (track #), $Title/$title, $Artist/$artist, $Album/$album and $Year/$year. Put optional variables in square brackets, use single-quote marks to escape square brackets literally ('[', ']').</small>
<small>Use: $Disc/$disc (disc #), $Track/$track (track #), $Title/$title, $Artist/$artist, $Album/$album and $Year/$year. Put optional variables in curly braces, use single-quote marks to escape curly braces literally ('{', '}').</small>
</div>
<div class="checkbox row clearfix">
<input type="checkbox" name="file_underscores" id="file_underscores" value="1" ${config['file_underscores']}/><label>Use underscores instead of spaces</label>

View File

@@ -13,22 +13,23 @@
#
# You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
'''Path pattern substitution module, see details below for syntax.
"""
Path pattern substitution module, see details below for syntax.
The pattern matching is loosely based on foobar2000 pattern syntax,
i.e. the notion of escaping characters with \' and optional elements
enclosed in square brackets [] is taken from there while the
substitution variable names are Perl-ish or sh-ish. The following
syntax elements are supported:
* escaped literal strings, that is everything that is enclosed
within single quotes (like \'this\');
* substitution variables, which start with dollar sign ($) and
extend until next non-alphanumeric+underscore character
(like $This and $5_that).
* optional elements enclosed in curly braces, which render
nonempty value only if any variable or optional inside returned
nonempty value, ignoring literals (like {\'[\'$That\']\'}).
'''
The pattern matching is loosely based on foobar2000 pattern syntax,
i.e. the notion of escaping characters with \' and optional elements
enclosed in square brackets [] is taken from there while the
substitution variable names are Perl-ish or sh-ish. The following
syntax elements are supported:
* escaped literal strings, that is everything that is enclosed
within single quotes (like 'this');
* substitution variables, which start with dollar sign ($) and
extend until next non-alphanumeric+underscore character
(like $This and $5_that).
* optional elements enclosed in curly braces, which render
nonempty value only if any variable or optional inside returned
nonempty value, ignoring literals (like {'{'$That'}'}).
"""
from __future__ import print_function
from enum import Enum
@@ -42,6 +43,9 @@ class _PatternElement(object):
'''Format this _PatternElement into string using provided substitution dictionary.'''
raise NotImplementedError()
def __ne__(self, other):
return not self == other
class _Generator(_PatternElement):
# pylint: disable=abstract-method
@@ -57,11 +61,23 @@ class _Replacement(_Generator):
def render(self, replacement):
# type: (Mapping[str,str]) -> str
return replacement.get(self._pattern, self._pattern)
res = replacement.get(self._pattern, self._pattern)
if res is None:
return ''
else:
return res
def __str__(self):
return self._pattern
@property
def pattern(self):
return self._pattern
def __eq__(self, other):
return isinstance(other, _Replacement) and \
self._pattern == other.pattern
class _LiteralText(_PatternElement):
'''Just a plain piece of text to be rendered "as is".'''
@@ -76,6 +92,13 @@ class _LiteralText(_PatternElement):
def __str__(self):
return self._text
@property
def text(self):
return self._text
def __eq__(self, other):
return isinstance(other, _LiteralText) and self._text == other.text
class _OptionalBlock(_Generator):
'''Optional block will render its contents only if any _Generator in its scope did return non-empty result.'''
@@ -87,11 +110,17 @@ class _OptionalBlock(_Generator):
def render(self, replacement):
# type: (Mapping[str,str]) -> str
res = [(isinstance(x, _Generator), x.render(replacement)) for x in self._scope]
if any((t[0] and len(t[1]) != 0) for t in res):
if any((t[0] and t[1] is not None and len(t[1]) != 0) for t in res):
return u"".join(t[1] for t in res)
else:
return u""
def __eq__(self, other):
"""
:type other: _OptionalBlock
"""
return isinstance(other, _OptionalBlock) and self._scope == other._scope
_OPTIONAL_START = u'{'
_OPTIONAL_END = u'}'
@@ -230,8 +259,9 @@ def render(pattern, replacement):
p = Pattern(pattern)
return p(replacement), p.warnings
if __name__ == "__main__":
# primitive test ;)
p = Pattern(u"[$Disc.]$Track - $Artist - $Title[ '['$Year']'")
p = Pattern(u"{$Disc.}$Track - $Artist - $Title{ [$Year]}")
d = {'$Disc': '', '$Track': '05', '$Artist': u'Grzegżółka', '$Title': u'Błona kapłona', '$Year': '2019'}
print(p(d).encode('utf8'), p.warnings)
assert p(d) == u"05 - Grzegżółka - Błona kapłona [2019]"

View File

@@ -0,0 +1,100 @@
# encoding=utf8
# This file is part of Headphones.
#
# Headphones is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Headphones is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Headphones. If not, see <http://www.gnu.org/licenses/>.
"""
Test module for pathrender.
"""
import headphones.pathrender as _pr
from headphones.pathrender import Pattern, Warnings
from unittestcompat import TestCase
__author__ = "Andrzej Ciarkowski <andrzej.ciarkowski@gmail.com>"
class PathRenderTest(TestCase):
"""
Tests for pathrender module.
"""
def test_parsing(self):
"""pathrender: pattern parsing"""
pattern = Pattern(u"{$Disc.}$Track - $Artist - $Title{ [$Year]}")
expected = [
_pr._OptionalBlock([
_pr._Replacement(u"$Disc"),
_pr._LiteralText(u".")
]),
_pr._Replacement(u"$Track"),
_pr._LiteralText(u" - "),
_pr._Replacement(u"$Artist"),
_pr._LiteralText(u" - "),
_pr._Replacement(u"$Title"),
_pr._OptionalBlock([
_pr._LiteralText(u" ["),
_pr._Replacement(u"$Year"),
_pr._LiteralText(u"]")
])
]
self.assertEqual(expected, pattern._pattern)
self.assertItemsEqual([], pattern.warnings)
def test_parsing_warnings(self):
"""pathrender: pattern parsing with warnings"""
pattern = Pattern(u"{$Disc.}$Track - $Artist - $Title{ [$Year]")
self.assertEqual(set([Warnings.UNCLOSED_OPTIONAL]), pattern.warnings)
pattern = Pattern(u"{$Disc.}$Track - $Artist - $Title{ [$Year]'}")
self.assertEqual(set([
Warnings.UNCLOSED_ESCAPE,
Warnings.UNCLOSED_OPTIONAL
]),
pattern.warnings)
def test_replacement(self):
"""pathrender: _Replacement variable substitution"""
r = _pr._Replacement(u"$Title")
subst = {'$Title': 'foo', '$Track': 'bar'}
res = r.render(subst)
self.assertEqual(res, u'foo', 'check valid replacement')
subst = {}
res = r.render(subst)
self.assertEqual(res, u'$Title', 'check missing replacement')
subst = {'$Title': None}
res = r.render(subst)
self.assertEqual(res, '', 'check render() works with None')
def test_literal(self):
"""pathrender: _Literal text rendering"""
l = _pr._LiteralText(u"foo")
subst = {'$foo': 'bar'}
res = l.render(subst)
self.assertEqual(res, 'foo')
def test_optional(self):
"""pathrender: _OptionalBlock element processing"""
o = _pr._OptionalBlock([
_pr._Replacement(u"$Title"),
_pr._LiteralText(u".foobar")
])
subst = {'$Title': 'foo', '$Track': 'bar'}
res = o.render(subst)
self.assertEqual(res, u'foo.foobar', 'check non-empty replacement')
subst = {'$Title': ''}
res = o.render(subst)
self.assertEqual(res, '', 'check empty replacement')
subst = {'$Title': None}
res = o.render(subst)
self.assertEqual(res, '', 'check render() works with None')