1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
|
from mediafile.utils import sc_decode, sc_encode
class StorageStyle:
"""A strategy for storing a value for a certain tag format (or set
of tag formats). This basic StorageStyle describes simple 1:1
mapping from raw values to keys in a Mutagen file object; subclasses
describe more sophisticated translations or format-specific access
strategies.
MediaFile uses a StorageStyle via three methods: ``get()``,
``set()``, and ``delete()``. It passes a Mutagen file object to
each.
Internally, the StorageStyle implements ``get()`` and ``set()``
using two steps that may be overridden by subtypes. To get a value,
the StorageStyle first calls ``fetch()`` to retrieve the value
corresponding to a key and then ``deserialize()`` to convert the raw
Mutagen value to a consumable Python value. Similarly, to set a
field, we call ``serialize()`` to encode the value and then
``store()`` to assign the result into the Mutagen object.
Each StorageStyle type has a class-level `formats` attribute that is
a list of strings indicating the formats that the style applies to.
MediaFile only uses StorageStyles that apply to the correct type for
a given audio file.
"""
formats = [
"FLAC",
"OggOpus",
"OggTheora",
"OggSpeex",
"OggVorbis",
"OggFlac",
"APEv2File",
"WavPack",
"Musepack",
"MonkeysAudio",
]
"""List of mutagen classes the StorageStyle can handle.
"""
def __init__(self, key, as_type=str, suffix=None, float_places=2, read_only=False):
"""Create a basic storage strategy. Parameters:
- `key`: The key on the Mutagen file object used to access the
field's data.
- `as_type`: The Python type that the value is stored as
internally (`unicode`, `int`, `bool`, or `bytes`).
- `suffix`: When `as_type` is a string type, append this before
storing the value.
- `float_places`: When the value is a floating-point number and
encoded as a string, the number of digits to store after the
decimal point.
- `read_only`: When true, writing to this field is disabled.
Primary use case is so wrongly named fields can be addressed
in a graceful manner. This does not block the delete method.
"""
self.key = key
self.as_type = as_type
self.suffix = suffix
self.float_places = float_places
self.read_only = read_only
# Convert suffix to correct string type.
if self.suffix and self.as_type is str and not isinstance(self.suffix, str):
self.suffix = self.suffix.decode("utf-8")
# Getter.
def get(self, mutagen_file):
"""Get the value for the field using this style."""
return self.deserialize(self.fetch(mutagen_file))
def fetch(self, mutagen_file):
"""Retrieve the raw value of for this tag from the Mutagen file
object.
"""
try:
return mutagen_file[self.key][0]
except (KeyError, IndexError):
return None
def deserialize(self, mutagen_value):
"""Given a raw value stored on a Mutagen object, decode and
return the represented value.
"""
if (
self.suffix
and isinstance(mutagen_value, str)
and mutagen_value.endswith(self.suffix)
):
return mutagen_value[: -len(self.suffix)]
else:
return mutagen_value
# Setter.
def set(self, mutagen_file, value):
"""Assign the value for the field using this style."""
self.store(mutagen_file, self.serialize(value))
def store(self, mutagen_file, value):
"""Store a serialized value in the Mutagen file object."""
mutagen_file[self.key] = [value]
def serialize(self, value):
"""Convert the external Python value to a type that is suitable for
storing in a Mutagen file object.
"""
if isinstance(value, float) and self.as_type is str:
value = "{0:.{1}f}".format(value, self.float_places)
value = self.as_type(value)
elif self.as_type is str:
if isinstance(value, bool):
# Store bools as 1/0 instead of True/False.
value = str(int(bool(value)))
elif isinstance(value, bytes):
value = value.decode("utf-8", "ignore")
else:
value = str(value)
else:
value = self.as_type(value)
if self.suffix:
value += self.suffix
return value
def delete(self, mutagen_file):
"""Remove the tag from the file."""
if self.key in mutagen_file:
del mutagen_file[self.key]
class ListStorageStyle(StorageStyle):
"""Abstract storage style that provides access to lists.
The ListMediaField descriptor uses a ListStorageStyle via two
methods: ``get_list()`` and ``set_list()``. It passes a Mutagen file
object to each.
Subclasses may overwrite ``fetch`` and ``store``. ``fetch`` must
return a (possibly empty) list or `None` if the tag does not exist.
``store`` receives a serialized list of values as the second argument.
The `serialize` and `deserialize` methods (from the base
`StorageStyle`) are still called with individual values. This class
handles packing and unpacking the values into lists.
"""
def get(self, mutagen_file):
"""Get the first value in the field's value list."""
values = self.get_list(mutagen_file)
if values is None:
return None
try:
return values[0]
except IndexError:
return None
def get_list(self, mutagen_file):
"""Get a list of all values for the field using this style."""
raw_values = self.fetch(mutagen_file)
if raw_values is None:
return None
return [self.deserialize(item) for item in raw_values]
def fetch(self, mutagen_file):
"""Get the list of raw (serialized) values."""
try:
return mutagen_file[self.key]
except KeyError:
return None
def set(self, mutagen_file, value):
"""Set an individual value as the only value for the field using
this style.
"""
if value is None:
self.store(mutagen_file, None)
else:
self.set_list(mutagen_file, [value])
def set_list(self, mutagen_file, values):
"""Set all values for the field using this style. `values`
should be an iterable.
"""
if values is None:
self.delete(mutagen_file)
else:
self.store(mutagen_file, [self.serialize(value) for value in values])
def store(self, mutagen_file, values):
"""Set the list of all raw (serialized) values for this field."""
mutagen_file[self.key] = values
class SoundCheckStorageStyleMixin:
"""A mixin for storage styles that read and write iTunes SoundCheck
analysis values. The object must have an `index` field that
indicates which half of the gain/peak pair---0 or 1---the field
represents.
"""
def get(self, mutagen_file):
data = self.fetch(mutagen_file)
if data is not None:
return sc_decode(data)[self.index]
def set(self, mutagen_file, value):
data = self.fetch(mutagen_file)
if data is None:
gain_peak = [0, 0]
else:
gain_peak = list(sc_decode(data))
gain_peak[self.index] = value or 0
data = self.serialize(sc_encode(*gain_peak))
self.store(mutagen_file, data)
|