import os
import re
import abc
from argparse import ArgumentTypeError
from eyed3 import id3
from eyed3.utils import console, formatSize, formatTime
from eyed3.plugins import LoaderPlugin
try:
from eyed3.plugins._display_parser import DisplayPatternParser
_have_grako = True
except ImportError:
_have_grako = False
[docs]class Pattern(object):
def __init__(self, text=None, sub_patterns=None):
self.__text = text
self.__sub_patterns = sub_patterns
[docs] def output_for(self, audio_file):
output = ""
for sub_pattern in self.sub_patterns or []:
output += sub_pattern.output_for(audio_file)
return output
def __get_sub_patterns(self):
if self.__sub_patterns is None and self.__text is not None:
self.__compile()
return self.__sub_patterns
def __set_sub_patterns(self, sub_patterns):
self.__sub_patterns = sub_patterns
sub_patterns = property(__get_sub_patterns, __set_sub_patterns)
def __compile(self):
# TODO: add support for comments in pattern
parser = DisplayPatternParser(whitespace='')
try:
asts = parser.parse(self.__text, rule_name='start')
self.sub_patterns = self.__compile_asts(asts)
self.__text = None
except BaseException as parsing_error:
raise PatternCompileException(str(parsing_error))
def __compile_asts(self, asts):
patterns = []
for ast in asts:
patterns.append(self.__compile_ast(ast))
return patterns
def __compile_ast(self, ast):
if ast is None:
return None
if "text" in ast:
return TextPattern(ast["text"])
if "tag" in ast:
parameters = self.__compile_parameters(ast["parameters"])
return self.__create_complex_pattern(TagPattern, ast["name"],
parameters)
if "function" in ast:
parameters = self.__compile_parameters(ast["parameters"])
if len(parameters) == 1 and parameters[0][0] is None and len(
parameters[0][1].sub_patterns) == 0:
parameters = []
return self.__create_complex_pattern(FunctionPattern, ast["name"],
parameters)
def __compile_parameters(self, parameter_asts):
parameters = []
for parameter_ast in parameter_asts:
sub_patterns = self.__compile_asts(parameter_ast["value"])
parameters.append((parameter_ast["name"],
Pattern(sub_patterns=sub_patterns)))
return parameters
def __create_complex_pattern(self, base_class, class_name, parameters):
pattern_class = self.__find_pattern_class(base_class, class_name)
if pattern_class is not None:
return pattern_class(class_name, parameters)
raise PatternCompileException("Unknown " + base_class.TYPE + " '" +
class_name + "'")
@staticmethod
def __find_pattern_class(base_class, class_name):
for pattern_class in Pattern.sub_pattern_classes(base_class):
if class_name in pattern_class.NAMES:
return pattern_class
[docs] @staticmethod
def sub_pattern_classes(base_class):
sub_classes = []
for pattern_class in base_class.__subclasses__():
if len(pattern_class.__subclasses__()) > 0:
sub_classes.extend(Pattern.sub_pattern_classes(pattern_class))
continue
sub_classes.append(pattern_class)
return sub_classes
[docs] @staticmethod
def pattern_class_parameters(pattern_class):
try:
return pattern_class.PARAMETERS
except AttributeError:
return []
def __repr__(self):
return self.__str__()
def __str__(self):
return str(self.__sub_patterns)
[docs]class TextPattern(Pattern):
SPECIAL_CHARACTERS = list("\\%$,()=nt")
SPECIAL_CHARACTERS_DESCRIPTIONS = list("\\%$,()=") + ["New line", "Tab"]
def __init__(self, text):
super(TextPattern, self).__init__(text)
self.__text = text
self.__replace_escapes()
def __replace_escapes(self):
escape_matches = list(re.compile('\\\\.').finditer(self.__text))
escape_matches.reverse()
for escape_match in escape_matches:
character = self.__text[escape_match.end() - 1]
if character not in TextPattern.SPECIAL_CHARACTERS:
raise PatternCompileException("Unknown escape character '" +
character + "'")
if character == 'n':
character = os.linesep
if character == 't':
character = '\t'
self.__text =\
f"{self.__text[:escape_match.start()]}{character}{self.__text[escape_match.end():]}"
[docs] def output_for(self, audio_file):
return self.__text
def __str__(self):
return "text:" + self.__text
[docs]class ComplexPattern(Pattern):
__metaclass__ = abc.ABCMeta
TYPE = "unknown"
NAMES = []
DESCRIPTION = ""
PARAMETERS = []
[docs] class ExpectedParameter(object):
def __init__(self, name, **kwargs):
self.name = name
if "default" in kwargs:
self.requried = False
self.default = kwargs["default"]
else:
self.requried = True
def __repr__(self):
return self.__str__()
def __str__(self):
if self.requried:
return self.name
return self.name + "(" + str(self.default) + ")"
[docs] class Parameter(object):
def __init__(self, value, provided):
self.value = value
self.provided = provided
def __repr__(self):
return self.__str__()
def __str__(self):
return str(self.value) + "(" + str(self.provided) + ")"
def __init__(self, name, parameters):
super(ComplexPattern, self).__init__()
self.__name = name
self.__import_parameters(parameters)
[docs] def output_for(self, audio_file):
output = self._get_output_for(audio_file)
return output or ""
@abc.abstractmethod
def _get_output_for(self, audio_file):
pass
def __import_parameters(self, parameters):
expected_parameters = Pattern.pattern_class_parameters(self.__class__)
self.__parameters = {}
for expected_parameter in expected_parameters:
found = False
for parameter in parameters:
if (parameter[0] is None or
parameter[0] == expected_parameter.name):
self.__parameters[expected_parameter.name] = \
ComplexPattern.Parameter(parameter[1], True)
parameters.remove(parameter)
found = True
break
if parameter[0] is not None:
break
if not expected_parameter.requried:
self.__parameters[expected_parameter.name] = \
ComplexPattern.Parameter(
Pattern(text=expected_parameter.default), True)
found = True
break
raise PatternCompileException(
self._error_message("Unexpected parameter"))
if not found:
if expected_parameter.requried:
raise PatternCompileException(self._error_message(
"Missing required parameter '" +
expected_parameter.name + "'"))
self.__parameters[expected_parameter.name] = \
ComplexPattern.Parameter(
Pattern(text=expected_parameter.default), False)
if len(parameters) > 0:
raise PatternCompileException(
self._error_message("Unexpected parameter"))
def __get_parameters(self):
return self.__parameters
parameters = property(__get_parameters)
def _parameter_value(self, name, audio_file):
return self.parameters[name].value.output_for(audio_file)
def _parameter_bool(self, name, audio_file):
value = self._parameter_value(name, audio_file)
return value.lower() in ("yes", "true", "y", "t", "1", "on")
def __get_name(self):
return self.__name
name = property(__get_name)
def _error_message(self, message):
return self.TYPE.capitalize() + " " + self.__name + ": " + message
def __str__(self):
return self.TYPE + ":" + self.name + str(self.parameters)
[docs]class PlaceholderUsagePattern(object):
__metaclass__ = abc.ABCMeta
def _replace_placeholders(self, text, replacements):
if len(replacements) == 0:
return text
replacement = replacements.pop(0)
subtexts = []
for subtext in text.split(replacement[0]):
subtexts.append(
self._replace_placeholders(subtext, list(replacements)))
return (str(replacement[1]) or "").join(subtexts)
[docs]class TagPattern(ComplexPattern):
__metaclass__ = abc.ABCMeta
TYPE = "tag"
[docs]class ArtistTagPattern(TagPattern):
NAMES = ["a", "artist"]
DESCRIPTION = "Artist"
def _get_output_for(self, audio_file):
return audio_file.tag.artist
[docs]class AlbumTagPattern(TagPattern):
NAMES = ["A", "album"]
DESCRIPTION = "Album"
def _get_output_for(self, audio_file):
return audio_file.tag.album
[docs]class AlbumArtistTagPattern(TagPattern):
NAMES = ["b", "album-artist"]
DESCRIPTION = "Album artist"
def _get_output_for(self, audio_file):
return audio_file.tag.album_artist
[docs]class ComposerTagPattern(TagPattern):
NAMES = ["C", "composer"]
DESCRIPTION = "Composer"
def _get_output_for(self, audio_file):
return audio_file.tag.composer
[docs]class TitleTagPattern(TagPattern):
NAMES = ["t", "title"]
DESCRIPTION = "Title"
def _get_output_for(self, audio_file):
return audio_file.tag.title
[docs]class TrackTagPattern(TagPattern):
NAMES = ["n", "track"]
DESCRIPTION = "Track number"
def _get_output_for(self, audio_file):
n = audio_file.tag.track_num[0]
return str(n or "")
[docs]class TrackTotalTagPattern(TagPattern):
NAMES = ["N", "track-total"]
DESCRIPTION = "Total track number"
def _get_output_for(self, audio_file):
n = audio_file.tag.track_num[1]
return str(n or "")
[docs]class DiscTagPattern(TagPattern):
NAMES = ["d", "disc", "disc-num"]
DESCRIPTION = "Disc number"
def _get_output_for(self, audio_file):
n = audio_file.tag.disc_num[0]
return str(n or "")
[docs]class DiscTotalTagPattern(TagPattern):
NAMES = ["D", "disc-total"]
DESCRIPTION = "Total disc number"
def _get_output_for(self, audio_file):
n = audio_file.tag.disc_num[1]
return str(n or "")
[docs]class GenreTagPattern(TagPattern):
NAMES = ["G", "genre"]
DESCRIPTION = "Genre"
def _get_output_for(self, audio_file):
return audio_file.tag.genre.name
[docs]class GenreIdTagPattern(TagPattern):
NAMES = ["genre-id"]
DESCRIPTION = "Genre ID"
def _get_output_for(self, audio_file):
return str(audio_file.tag.genre.id) if audio_file.tag.genre else None
[docs]class YearTagPattern(TagPattern):
NAMES = ["Y", "year"]
DESCRIPTION = "Release year"
def _get_output_for(self, audio_file):
return audio_file.tag.release_date.year
[docs]class DescriptableTagPattern(TagPattern):
__metaclass__ = abc.ABCMeta
PARAMETERS = [ComplexPattern.ExpectedParameter("description", default=None),
ComplexPattern.ExpectedParameter("language", default=None)]
def _get_matching_elements(self, elements, audio_file):
matching_elements = []
for element in elements:
if (self.__matches("description", element.description,
audio_file) and
self.__matches("language", element.lang, audio_file)):
matching_elements.append(element)
return matching_elements
def __matches(self, parameter_name, comment_attribute_value, audio_file):
if not self.parameters[parameter_name].provided:
return True
if self.parameters[parameter_name].value is None:
return (comment_attribute_value is None or
comment_attribute_value == "")
return (self._parameter_value(parameter_name, audio_file) ==
comment_attribute_value)
[docs]class AbstractDateTagPattern(TagPattern):
__metaclass__ = abc.ABCMeta
def _get_output_for(self, audio_file):
return str(self._get_date(audio_file) or "")
@abc.abstractmethod
def _get_date(self, audio_file):
pass
[docs]class ReleaseDateTagPattern(AbstractDateTagPattern):
NAMES = ["release-date"]
DESCRIPTION = "Relase date"
def _get_date(self, audio_file):
return audio_file.tag.release_date
[docs]class OriginalReleaseDateTagPattern(AbstractDateTagPattern):
NAMES = ["original-release-date"]
DESCRIPTION = "Original Relase date"
def _get_date(self, audio_file):
return audio_file.tag.original_release_date
[docs]class RecordingDateTagPattern(AbstractDateTagPattern):
NAMES = ["recording-date"]
DESCRIPTION = "Recording date"
def _get_date(self, audio_file):
return audio_file.tag.recording_date
[docs]class EncodingDateTagPattern(AbstractDateTagPattern):
NAMES = ["encoding-date"]
DESCRIPTION = "Encoding date"
def _get_date(self, audio_file):
return audio_file.tag.encoding_date
[docs]class TaggingDateTagPattern(AbstractDateTagPattern):
NAMES = ["tagging-date"]
DESCRIPTION = "Tagging date"
def _get_date(self, audio_file):
return audio_file.tag.tagging_date
[docs]class PlayCountTagPattern(TagPattern):
NAMES = ["play-count"]
DESCRIPTION = "Play count"
def _get_output_for(self, audio_file):
return audio_file.tag.play_count
[docs]class PopularitiesTagPattern(TagPattern, PlaceholderUsagePattern):
NAMES = ["popm", "popularities"]
PARAMETERS = [ComplexPattern.ExpectedParameter("output",
default="Popularity: [email: #e] [rating: #r] [play count: #c]"),
ComplexPattern.ExpectedParameter("separation", default="\\n")]
DESCRIPTION = "Popularities (with output placeholders #e as email, "\
"#r as rating & #c as count)"
def _get_output_for(self, audio_file):
output_pattern = self._parameter_value("output", audio_file)
separation = self._parameter_value("separation", audio_file)
outputs = []
for popularity in audio_file.tag.popularities:
replacements = [["#e", popularity.email],
["#r", popularity.rating],
["#c", popularity.count]]
outputs.append(self._replace_placeholders(output_pattern,
replacements))
return separation.join(outputs)
[docs]class BPMTagPattern(TagPattern):
NAMES = ["bpm"]
DESCRIPTION = "BPM"
def _get_output_for(self, audio_file):
return audio_file.tag.bpm
[docs]class PublisherTagPattern(TagPattern):
NAMES = ["publisher"]
DESCRIPTION = "Publisher"
def _get_output_for(self, audio_file):
return audio_file.tag.publisher
[docs]class UniqueFileIDTagPattern(TagPattern, PlaceholderUsagePattern):
NAMES = ["ufids", "unique-file-ids"]
PARAMETERS = [ComplexPattern.ExpectedParameter("output",
default="Unique File ID: [#o] : #i"),
ComplexPattern.ExpectedParameter("separation", default="\\n")]
DESCRIPTION = "Unique File IDs (with output placeholders #o as owner & #i "\
" as unique id)"
def _get_output_for(self, audio_file):
output_pattern = self._parameter_value("output", audio_file)
separation = self._parameter_value("separation", audio_file)
outputs = []
for ufid in audio_file.tag.unique_file_ids:
replacements = [["#o", ufid.owner_id],
["#i", ufid.uniq_id.encode("string_escape")]]
outputs.append(self._replace_placeholders(output_pattern,
replacements))
return separation.join(outputs)
[docs]class LyricsTagPattern(DescriptableTagPattern, PlaceholderUsagePattern):
NAMES = ["lyrics"]
PARAMETERS = DescriptableTagPattern.PARAMETERS + \
[ComplexPattern.ExpectedParameter(
"output",
default="Lyrics: [Description: #d] [Lang: #l]: #t"),
ComplexPattern.ExpectedParameter("separation", default="\\n")]
DESCRIPTION = "All lyrics that are matching description and language " + \
"(with output placeholders #d as description, #l as "\
"language & #t as text)."
def _get_output_for(self, audio_file):
output_pattern = self._parameter_value("output", audio_file)
separation = self._parameter_value("separation", audio_file)
outputs = []
for l in self._get_matching_elements(audio_file.tag.lyrics, audio_file):
replacements = [["#d", l.description],
["#l", l.lang.decode("ascii")],
["#t", l.text]]
outputs.append(self._replace_placeholders(output_pattern,
replacements))
return separation.join(outputs)
[docs]class TextsTagPattern(TagPattern, PlaceholderUsagePattern):
NAMES = ["txxx", "texts"]
PARAMETERS = [
ComplexPattern.ExpectedParameter(
"output", default="UserTextFrame: [Description: #d] #t"),
ComplexPattern.ExpectedParameter("separation", default="\\n")]
DESCRIPTION = "User text frames (with output placeholders #d as "\
"description & #t as text)"
def _get_output_for(self, audio_file):
output_pattern = self._parameter_value("output", audio_file)
separation = self._parameter_value("separation", audio_file)
outputs = []
for frame in audio_file.tag.user_text_frames:
replacements = [["#d", frame.description],
["#t", frame.text]]
outputs.append(self._replace_placeholders(output_pattern,
replacements))
return separation.join(outputs)
[docs]class ArtistURLTagPattern(TagPattern):
NAMES = ["artist-url"]
DESCRIPTION = "Artist URL"
def _get_output_for(self, audio_file):
return audio_file.tag.artist_url
[docs]class AudioSourceURLTagPattern(TagPattern):
NAMES = ["audio-source-url"]
DESCRIPTION = "Audio source URL"
def _get_output_for(self, audio_file):
return audio_file.tag.audio_source_url
[docs]class AudioFileURLTagPattern(TagPattern):
NAMES = ["audio-file-url"]
DESCRIPTION = "Audio file URL"
def _get_output_for(self, audio_file):
return audio_file.tag.audio_file_url
[docs]class InternetRadioURLTagPattern(TagPattern):
NAMES = ["internet-radio-url"]
DESCRIPTION = "Internet radio URL"
def _get_output_for(self, audio_file):
return audio_file.tag.internet_radio_url
[docs]class CommercialURLTagPattern(TagPattern):
NAMES = ["commercial-url"]
DESCRIPTION = "Comercial URL"
def _get_output_for(self, audio_file):
return audio_file.tag.copyright_url
[docs]class PaymentURLTagPattern(TagPattern):
NAMES = ["payment-url"]
DESCRIPTION = "Payment URL"
def _get_output_for(self, audio_file):
return audio_file.tag.payment_url
[docs]class PublisherURLTagPattern(TagPattern):
NAMES = ["publisher-url"]
DESCRIPTION = "Publisher URL"
def _get_output_for(self, audio_file):
return audio_file.tag.publisher_url
[docs]class CopyrightTagPattern(TagPattern):
NAMES = ["copyright-url"]
DESCRIPTION = "Copyright URL"
def _get_output_for(self, audio_file):
return audio_file.tag.copyright_url
[docs]class UserURLsTagPattern(TagPattern, PlaceholderUsagePattern):
NAMES = ["user-urls"]
PARAMETERS = [ComplexPattern.ExpectedParameter("output",
default="#i [Description: #d]: #u"),
ComplexPattern.ExpectedParameter("separation", default="\\n")]
DESCRIPTION = "User URL frames (with output placeholders #i as frame id, "\
"#d as description & #u as url)"
def _get_output_for(self, audio_file):
output_pattern = self._parameter_value("output", audio_file)
separation = self._parameter_value("separation", audio_file)
outputs = []
for frame in audio_file.tag.user_url_frames:
replacements = [["#i", frame.id],
["#d", frame.description],
["#u", frame.url]]
outputs.append(self._replace_placeholders(output_pattern,
replacements))
return separation.join(outputs)
[docs]class ImagesTagPattern(TagPattern, PlaceholderUsagePattern):
NAMES = ["images", "apic"]
PARAMETERS = [ComplexPattern.ExpectedParameter(
"output",
default="#t Image: [Type: #m] [Size: #s bytes] #d"),
ComplexPattern.ExpectedParameter("separation", default="\\n")]
DESCRIPTION = "Attached pictures (APIC)" \
"(with output placeholders #t as image type, "\
"#m as mime type, #s as size in bytes & #d as description)"
def _get_output_for(self, audio_file):
output_pattern = self._parameter_value("output", audio_file)
separation = self._parameter_value("separation", audio_file)
outputs = []
for img in audio_file.tag.images:
if img.mime_type not in id3.frames.ImageFrame.URL_MIME_TYPE_VALUES:
replacements = [["#t", img.picTypeToString(img.picture_type)],
["#m", img.mime_type],
["#s", len(img.image_data)],
["#d", img.description]]
outputs.append(self._replace_placeholders(output_pattern,
replacements))
return separation.join(outputs)
[docs]class ImageURLsTagPattern(TagPattern, PlaceholderUsagePattern):
NAMES = ["image-urls"]
PARAMETERS = [ComplexPattern.ExpectedParameter(
"output", default="#t Image: [Type: #m] [URL: #u] #d"),
ComplexPattern.ExpectedParameter("separation", default="\\n")]
DESCRIPTION = "Attached pictures URLs" \
"(with output placeholders #t as image type, "\
"#m as mime type, #u as URL & #d as description)"
def _get_output_for(self, audio_file):
output_pattern = self._parameter_value("output", audio_file)
separation = self._parameter_value("separation", audio_file)
outputs = []
for img in audio_file.tag.images:
if img.mime_type in id3.frames.ImageFrame.URL_MIME_TYPE_VALUES:
replacements = [["#t", img.picTypeToString(img.picture_type)],
["#m", img.mime_type],
["#u", img.image_url],
["#d", img.description]]
outputs.append(self._replace_placeholders(output_pattern,
replacements))
return separation.join(outputs)
[docs]class ObjectsTagPattern(TagPattern, PlaceholderUsagePattern):
NAMES = ["objects", "gobj"]
PARAMETERS = [ComplexPattern.ExpectedParameter("output",
default="GEOB: [Size: #s bytes] [Type: #t] "
"Description: #d | Filename: #f"),
ComplexPattern.ExpectedParameter("separation", default="\\n")]
DESCRIPTION = "Objects (GOBJ)" \
"(with output placeholders #s as size, #m as mime type, "\
"#d as description and #f as file name)"
def _get_output_for(self, audio_file):
output_pattern = self._parameter_value("output", audio_file)
separation = self._parameter_value("separation", audio_file)
outputs = []
for obj in audio_file.tag.objects:
replacements = [["#s", len(obj.object_data)],
["#m", obj.mime_type],
["#d", obj.description],
["#f", obj.filename]]
outputs.append(self._replace_placeholders(output_pattern,
replacements))
return separation.join(outputs)
[docs]class PrivatesTagPattern(TagPattern, PlaceholderUsagePattern):
NAMES = ["privates", "priv"]
PARAMETERS = [ComplexPattern.ExpectedParameter("output",
default="PRIV-Content: #b bytes | Owner: #o"),
ComplexPattern.ExpectedParameter("separation", default="\\n")]
DESCRIPTION = "Privates (APIC) (with output placeholders #c as content, "\
"#b as number of bytes & #o as owner)"
def _get_output_for(self, audio_file):
output_pattern = self._parameter_value("output", audio_file)
separation = self._parameter_value("separation", audio_file)
outputs = []
for private in audio_file.tag.privates:
replacements = [["#b", "%i" % len(private.data)],
["#c", private.data.decode("ascii")],
["#o", private.owner_id.decode("ascii")]]
outputs.append(self._replace_placeholders(output_pattern,
replacements))
return separation.join(outputs)
[docs]class MusicCDIdTagPattern(TagPattern):
NAMES = ["music-cd-id", "mcdi"]
DESCRIPTION = "Music CD Identification"
def _get_output_for(self, audio_file):
if audio_file.tag.cd_id is not None:
return audio_file.tag.cd_id.decode("ascii")
else:
return None
[docs]class TermsOfUseTagPattern(TagPattern):
NAMES = ["terms-of-use"]
DESCRIPTION = "Terms of use"
def _get_output_for(self, audio_file):
return audio_file.tag.terms_of_use
[docs]class FunctionPattern(ComplexPattern):
__metaclass__ = abc.ABCMeta
TYPE = "function"
[docs]class FunctionNumberPattern(FunctionPattern):
NAMES = ["num", "number-format"]
PARAMETERS = [ComplexPattern.ExpectedParameter("number"),
ComplexPattern.ExpectedParameter("digits")]
DESCRIPTION = "Appends leading zeros"
def _get_output_for(self, audio_file):
number = self._parameter_value("number", audio_file)
digits = self._parameter_value("digits", audio_file)
try:
number = int(number)
except ValueError:
raise DisplayException(self._error_message("'" + number +
"' is not a number."))
try:
digits = int(digits)
except ValueError:
raise DisplayException(self._error_message("'" + digits +
"' is not a number."))
output = str(number)
return ("0" * max(0, digits - len(output))) + output
[docs]class FunctionFilenamePattern(FunctionPattern):
NAMES = ["filename", "fn"]
PARAMETERS = [ComplexPattern.ExpectedParameter("basename", default=None)]
DESCRIPTION = "File name"
def _get_output_for(self, audio_file):
if self._parameter_bool("basename", audio_file):
return os.path.basename(audio_file.path)
return audio_file.path
[docs]class FunctionFilesizePattern(FunctionPattern):
NAMES = ["filesize"]
DESCRIPTION = "Size of file"
def _get_output_for(self, audio_file):
from stat import ST_SIZE
file_size = os.stat(audio_file.path)[ST_SIZE]
return formatSize(file_size)
[docs]class FunctionTagVersionPattern(FunctionPattern):
NAMES = ["tag-version"]
DESCRIPTION = "Tag version"
def _get_output_for(self, audio_file):
return id3.versionToString(audio_file.tag.version)
[docs]class FunctionLengthPattern(FunctionPattern):
NAMES = ["length"]
DESCRIPTION = "Length of aufio file"
def _get_output_for(self, audio_file):
return formatTime(audio_file.info.time_secs)
[docs]class FunctionMPEGVersionPattern(FunctionPattern, PlaceholderUsagePattern):
NAMES = ["mpeg-version"]
PARAMETERS = [ComplexPattern.ExpectedParameter("output",
default=r"MPEG#v\, Layer #l")]
DESCRIPTION = "MPEG version (with output placeholders #v as version & "\
"#l as layer)"
def _get_output_for(self, audio_file):
output = self._parameter_value("output", audio_file)
replacements = [["#v", str(audio_file.info.mp3_header.version)],
["#l", "I" * audio_file.info.mp3_header.layer]]
return self._replace_placeholders(output, replacements)
[docs]class FunctionBitRatePattern(FunctionPattern):
NAMES = ["bit-rate"]
DESCRIPTION = "Bit rate of aufio file"
def _get_output_for(self, audio_file):
return audio_file.info.bit_rate_str
[docs]class FunctionSampleFrequencePattern(FunctionPattern):
NAMES = ["sample-freq"]
DESCRIPTION = "Sample frequence of aufio file in Hz"
def _get_output_for(self, audio_file):
return str(audio_file.info.mp3_header.sample_freq)
[docs]class FunctionAudioModePattern(FunctionPattern):
NAMES = ["audio-mode"]
DESCRIPTION = "Mode of aufio file: mono/stereo"
def _get_output_for(self, audio_file):
return audio_file.info.mp3_header.mode
[docs]class FunctionNotEmptyPattern(FunctionPattern, PlaceholderUsagePattern):
NAMES = ["not-empty"]
PARAMETERS = [ComplexPattern.ExpectedParameter("text"),
ComplexPattern.ExpectedParameter("output", default="#t"),
ComplexPattern.ExpectedParameter("empty", default=None)]
DESCRIPTION = "If condition is not empty (with output placeholder #t as "\
"text)"
def _get_output_for(self, audio_file):
text = self._parameter_value("text", audio_file)
if len(text) > 0:
output = self._parameter_value("output", audio_file)
return self._replace_placeholders(output, [["#t", text]])
else:
return self._parameter_value("empty", audio_file)
[docs]class FunctionRepeatPattern(FunctionPattern):
NAMES = ["repeat"]
PARAMETERS = [ComplexPattern.ExpectedParameter("text"),
ComplexPattern.ExpectedParameter("count")]
DESCRIPTION = "Repeats text"
def _get_output_for(self, audio_file):
content = self._parameter_value("text", audio_file)
count = self._parameter_value("count", audio_file)
try:
count = int(count)
except ValueError:
raise DisplayException(self._error_message(f"'{count}' is not a number."))
return content * count
[docs]class DisplayPlugin(LoaderPlugin):
NAMES = ["display"]
SUMMARY = "Tag Display"
DESCRIPTION = """
Prints specific tag information.
"""
def __init__(self, arg_parser):
super(DisplayPlugin, self).__init__(arg_parser)
def filename(fn):
if not os.path.exists(fn):
raise ArgumentTypeError("The file %s does not exist!" % fn)
return fn
pattern_group = \
self.arg_group.add_mutually_exclusive_group(required=True)
pattern_group.add_argument("--pattern-help", action="store_true",
dest="pattern_help",
help=ARGS_HELP["--pattern-help"])
pattern_group.add_argument("-p", "--pattern", dest="pattern_string",
metavar="STRING",
help=ARGS_HELP["--pattern"])
pattern_group.add_argument("-f", "--pattern-file", dest="pattern_file",
metavar="FILE", type=filename,
help=ARGS_HELP["--pattern-file"])
self.arg_group.add_argument("--no-newline", action="store_true",
dest="no_newline",
help=ARGS_HELP["--no-newline"])
self.__pattern = None
self.__return_code = 0
self.__output_ending = None
[docs] def start(self, args, config):
super(DisplayPlugin, self).start(args, config)
if args.pattern_help:
self.__print_pattern_help()
return
if not _have_grako:
console.printError("Unknown module 'grako'" + os.linesep +
"Please install grako! " +
"E.g. $ pip install grako")
self.__return_code = 2
return
if args.pattern_string is not None:
self.__pattern = Pattern(args.pattern_string)
if args.pattern_file is not None:
pfile = open(args.pattern_file, "r")
self.__pattern = Pattern(''.join(pfile.read().splitlines()))
pfile.close()
self.__output_ending = "" if args.no_newline else os.linesep
[docs] def handleFile(self, f, *args, **kwargs):
if self.args.pattern_help:
return
if self.__return_code != 0:
return
super(DisplayPlugin, self).handleFile(f)
if not self.audio_file:
return
try:
print(self.__pattern.output_for(self.audio_file),
end=self.__output_ending)
except PatternCompileException as e:
self.__return_code = 1
console.printError(e.message)
except DisplayException as e:
self.__return_code = 1
console.printError(e.message)
[docs] def handleDone(self):
return self.__return_code
def __print_pattern_help(self):
print("\nAll pattern variable are of the form `%var%`\n")
# FIXME: Force some order
print(console.formatText("ID3 Tags:", b=True))
self.__print_complex_pattern_help(TagPattern)
print(os.linesep)
print(console.formatText("Functions:", b=True))
self.__print_complex_pattern_help(FunctionPattern)
print(os.linesep)
print(console.formatText("Special characters:", b=True))
print(console.formatText("\tescape seq. character"))
for i in range(len(TextPattern.SPECIAL_CHARACTERS)):
print(("\t\\%s" + (" " * 12) + "%s") %
(TextPattern.SPECIAL_CHARACTERS[i],
TextPattern.SPECIAL_CHARACTERS_DESCRIPTIONS[i]))
def __print_complex_pattern_help(self, base_class):
rows = []
# TODO line wrap for description
for pattern_class in Pattern.sub_pattern_classes(base_class):
rows.append([", ".join(pattern_class.NAMES),
pattern_class.DESCRIPTION])
parameters = Pattern.pattern_class_parameters(pattern_class)
if len(parameters) > 0:
rows.append(["", "Parameter" +
("s:" if len(parameters) > 1 else ":")])
for parameter in parameters:
parameter_desc = parameter.name
if not parameter.requried:
default = ", default='" + parameter.default + \
"'" if parameter.default else ""
parameter_desc += " (optional" + default + ")"
rows.append(["", " " + parameter_desc])
self.__print_rows(rows, "\t", " ")
@staticmethod
def __print_rows(rows, indent, spacing):
row_widths = []
for row in rows:
for n in range(len(row)):
width = len(row[n])
if len(row_widths) <= n:
row_widths.append(width)
else:
row_widths[n] = max(row_widths[n], width)
for row in rows:
out = indent
for n in range(len(row)):
out += row[n]
if n < len(row) - 1:
out += (" " * (row_widths[n] - len(row[n]))) + spacing
print(out)
[docs]class DisplayException(Exception):
def __init__(self, message):
self.__message = message
def __get_message(self):
return self.__message
message = property(__get_message)
[docs]class PatternCompileException(Exception):
def __init__(self, message):
self.__message = message
def __get_message(self):
return self.__message
message = property(__get_message)
ARGS_HELP = {
"--pattern-help": "Detailed pattern help",
"--pattern": "Pattern string",
"--pattern-file": "Pattern file",
"--no-newline": "Print no newline after each output"
}