mirror of
https://github.com/rembo10/headphones.git
synced 2026-05-11 06:09:29 +01:00
Merge remote-tracking branch 'andrzejc/pathrender' into develop
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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]"
|
||||
|
||||
100
headphones/pathrender_test.py
Normal file
100
headphones/pathrender_test.py
Normal 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')
|
||||
Reference in New Issue
Block a user