diff --git a/beet b/beet index a398c6939..c5699a5ac 100755 --- a/beet +++ b/beet @@ -10,7 +10,7 @@ # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: -# +# # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. @@ -18,4 +18,3 @@ import beets.ui if __name__ == '__main__': beets.ui.main() - diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 4c888302d..805f2cac9 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -104,8 +104,11 @@ class StringFieldQuery(FieldQuery): class SubstringQuery(StringFieldQuery): """A query that matches a substring in a specific item field.""" def col_clause(self): - search = '%' + (self.pattern.replace('\\','\\\\').replace('%','\\%') - .replace('_','\\_')) + '%' + pattern = (self.pattern + .replace('\\', '\\\\') + .replace('%', '\\%') + .replace('_', '\\_')) + search = '%' + pattern + '%' clause = self.field + " like ? escape '\\'" subvals = [search] return clause, subvals @@ -236,12 +239,16 @@ class CollectionQuery(Query): self.subqueries = subqueries # Act like a sequence. + def __len__(self): return len(self.subqueries) + def __getitem__(self, key): return self.subqueries[key] + def __iter__(self): return iter(self.subqueries) + def __contains__(self, item): return item in self.subqueries @@ -334,10 +341,8 @@ class FalseQuery(Query): return False - # Time/date queries. - def _to_epoch_time(date): """Convert a `datetime` object to an integer number of seconds since the (local) Unix epoch. diff --git a/beets/dbcore/types.py b/beets/dbcore/types.py index ccd260c0d..573775467 100644 --- a/beets/dbcore/types.py +++ b/beets/dbcore/types.py @@ -18,10 +18,8 @@ from . import query from beets.util import str2bool - # Abstract base. - class Type(object): """An object encapsulating the type of a model field. Includes information about how to store, query, format, and parse a given @@ -63,10 +61,8 @@ class Type(object): return value - # Reusable types. - class Integer(Type): """A basic integer type. """ diff --git a/beets/mediafile.py b/beets/mediafile.py index 7e76be2ff..29cb4f396 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -53,34 +53,8 @@ import enum __all__ = ['UnreadableFileError', 'FileTypeError', 'MediaFile'] - - -# Logger. log = logging.getLogger('beets') - - -# Exceptions. - -class UnreadableFileError(Exception): - """Indicates a file that MediaFile can't read. - """ - pass - -class FileTypeError(UnreadableFileError): - """Raised for files that don't seem to have a type MediaFile - supports. - """ - pass - -class MutagenError(UnreadableFileError): - """Raised when Mutagen fails unexpectedly---probably due to a bug. - """ - - - -# Constants. - # Human-readable type names. TYPES = { 'mp3': 'MP3', @@ -96,6 +70,25 @@ TYPES = { } +# Exceptions. + +class UnreadableFileError(Exception): + """Indicates a file that MediaFile can't read. + """ + pass + + +class FileTypeError(UnreadableFileError): + """Raised for files that don't seem to have a type MediaFile + supports. + """ + pass + + +class MutagenError(UnreadableFileError): + """Raised when Mutagen fails unexpectedly---probably due to a bug. + """ + # Utility. @@ -161,7 +154,6 @@ def _safe_cast(out_type, val): return val - # Image coding for ASF/WMA. def _unpack_asf_image(data): @@ -189,6 +181,7 @@ def _unpack_asf_image(data): return (mime.decode("utf-16-le"), image_data, type, description.decode("utf-16-le")) + def _pack_asf_image(mime, data, type=3, description=""): """Pack image data for a WM/Picture tag. """ @@ -199,7 +192,6 @@ def _pack_asf_image(mime, data, type=3, description=""): return tag_data - # iTunes Sound Check encoding. def _sc_decode(soundcheck): @@ -237,6 +229,7 @@ def _sc_decode(soundcheck): return round(gain, 2), round(peak, 6) + def _sc_encode(gain, peak): """Encode ReplayGain gain/peak values as a Sound Check string. """ @@ -261,10 +254,8 @@ def _sc_encode(gain, peak): return (u' %08X' * 10) % values - # Cover art and other images. - def _image_mime_type(data): """Return the MIME type of the image data (a bytestring). """ @@ -341,7 +332,6 @@ class Image(object): return self.type.value - # StorageStyle classes describe strategies for accessing values in # Mutagen file objects. @@ -417,7 +407,7 @@ class StorageStyle(object): return the represented value. """ if self.suffix and isinstance(mutagen_value, unicode) \ - and mutagen_value.endswith(self.suffix): + and mutagen_value.endswith(self.suffix): return mutagen_value[:-len(self.suffix)] else: return mutagen_value @@ -588,10 +578,11 @@ class MP4ListStorageStyle(ListStorageStyle, MP4StorageStyle): class MP4SoundCheckStorageStyle(SoundCheckStorageStyleMixin, MP4StorageStyle): - def __init__(self, index=0, **kwargs): - super(MP4SoundCheckStorageStyle, self).__init__(**kwargs) + def __init__(self, key, index=0, **kwargs): + super(MP4SoundCheckStorageStyle, self).__init__(key, **kwargs) self.index = index + class MP4BoolStorageStyle(MP4StorageStyle): """A style for booleans in MPEG-4 files. (MPEG-4 has an atom type specifically for representing booleans.) @@ -716,7 +707,10 @@ class MP3DescStorageStyle(MP3StorageStyle): # need to make a new frame? if not found: frame = mutagen.id3.Frames[self.key]( - desc=str(self.description), text=value, encoding=3) + desc=str(self.description), + text=value, + encoding=3 + ) if self.id3_lang: frame.lang = self.id3_lang mutagen_file.tags.add(frame) @@ -832,7 +826,8 @@ class VorbisImageStorageStyle(ListStorageStyle): def __init__(self): super(VorbisImageStorageStyle, self).__init__( - key='metadata_block_picture') + key='metadata_block_picture' + ) self.as_type = str def fetch(self, mutagen_file): @@ -849,7 +844,7 @@ class VorbisImageStorageStyle(ListStorageStyle): except (TypeError, AttributeError): continue images.append(Image(data=pic.data, desc=pic.desc, - type=pic.type)) + type=pic.type)) return images def store(self, mutagen_file, image_data): @@ -904,7 +899,6 @@ class FlacImageStorageStyle(ListStorageStyle): return pic - # MediaField is a descriptor that represents a single logical field. It # aggregates several StorageStyles describing how to access the data for # each file type. @@ -1141,10 +1135,8 @@ class ImageListField(MediaField): style.set_list(mediafile.mgfile, images) - # MediaFile is a collection of fields. - class MediaFile(object): """Represents a multimedia file on disk and provides access to its metadata. @@ -1186,10 +1178,11 @@ class MediaFile(object): log.error('uncaught Mutagen exception in open: {0}'.format(exc)) raise MutagenError('Mutagen raised an exception') - if self.mgfile is None: # Mutagen couldn't guess the type + if self.mgfile is None: + # Mutagen couldn't guess the type raise FileTypeError('file type unsupported by Mutagen') - elif type(self.mgfile).__name__ == 'M4A' or \ - type(self.mgfile).__name__ == 'MP4': + elif (type(self.mgfile).__name__ == 'M4A' or + type(self.mgfile).__name__ == 'MP4'): # This hack differentiates AAC and ALAC until we find a more # deterministic approach. Mutagen only sets the sample rate # for AAC files. See: @@ -1199,8 +1192,8 @@ class MediaFile(object): self.type = 'aac' else: self.type = 'alac' - elif type(self.mgfile).__name__ == 'ID3' or \ - type(self.mgfile).__name__ == 'MP3': + elif (type(self.mgfile).__name__ == 'ID3' or + type(self.mgfile).__name__ == 'MP3'): self.type = 'mp3' elif type(self.mgfile).__name__ == 'FLAC': self.type = 'flac' @@ -1261,7 +1254,6 @@ class MediaFile(object): for tag in self.mgfile.keys(): del self.mgfile[tag] - # Convenient access to the set of available fields. @classmethod @@ -1313,7 +1305,6 @@ class MediaFile(object): if field in dict: setattr(self, field, dict[field]) - # Field definitions. title = MediaField( @@ -1487,8 +1478,8 @@ class MediaFile(object): ) country = MediaField( MP3DescStorageStyle('MusicBrainz Album Release Country'), - MP4StorageStyle("----:com.apple.iTunes:MusicBrainz Album " - "Release Country"), + MP4StorageStyle("----:com.apple.iTunes:MusicBrainz " + "Album Release Country"), StorageStyle('RELEASECOUNTRY'), ASFStorageStyle('MusicBrainz/Album Release Country'), ) @@ -1603,63 +1594,101 @@ class MediaFile(object): # ReplayGain fields. rg_track_gain = MediaField( - MP3DescStorageStyle(u'REPLAYGAIN_TRACK_GAIN', - float_places=2, suffix=u' dB'), - MP3DescStorageStyle(u'replaygain_track_gain', - float_places=2, suffix=u' dB'), - MP3SoundCheckStorageStyle(key='COMM', index=0, desc=u'iTunNORM', - id3_lang='eng'), - MP4StorageStyle(key='----:com.apple.iTunes:replaygain_track_gain', - float_places=2, suffix=b' dB'), - MP4SoundCheckStorageStyle(key='----:com.apple.iTunes:iTunNORM', - index=0), - StorageStyle(u'REPLAYGAIN_TRACK_GAIN', - float_places=2, suffix=u' dB'), - ASFStorageStyle(u'replaygain_track_gain', - float_places=2, suffix=u' dB'), + MP3DescStorageStyle( + u'REPLAYGAIN_TRACK_GAIN', + float_places=2, suffix=u' dB' + ), + MP3DescStorageStyle( + u'replaygain_track_gain', + float_places=2, suffix=u' dB' + ), + MP3SoundCheckStorageStyle( + key='COMM', + index=0, desc=u'iTunNORM', + id3_lang='eng' + ), + MP4StorageStyle( + '----:com.apple.iTunes:replaygain_track_gain', + float_places=2, suffix=b' dB' + ), + MP4SoundCheckStorageStyle( + '----:com.apple.iTunes:iTunNORM', + index=0 + ), + StorageStyle( + u'REPLAYGAIN_TRACK_GAIN', + float_places=2, suffix=u' dB' + ), + ASFStorageStyle( + u'replaygain_track_gain', + float_places=2, suffix=u' dB' + ), out_type=float ) rg_album_gain = MediaField( - MP3DescStorageStyle(u'REPLAYGAIN_ALBUM_GAIN', - float_places=2, suffix=u' dB'), - MP3DescStorageStyle(u'replaygain_album_gain', - float_places=2, suffix=u' dB'), - MP4SoundCheckStorageStyle(key='----:com.apple.iTunes:iTunNORM', - index=1), - StorageStyle(u'REPLAYGAIN_ALBUM_GAIN', - float_places=2, suffix=u' dB'), - ASFStorageStyle(u'replaygain_album_gain', - float_places=2, suffix=u' dB'), + MP3DescStorageStyle( + u'REPLAYGAIN_ALBUM_GAIN', + float_places=2, suffix=u' dB' + ), + MP3DescStorageStyle( + u'replaygain_album_gain', + float_places=2, suffix=u' dB' + ), + MP4SoundCheckStorageStyle( + '----:com.apple.iTunes:iTunNORM', + index=1 + ), + StorageStyle( + u'REPLAYGAIN_ALBUM_GAIN', + float_places=2, suffix=u' dB' + ), + ASFStorageStyle( + u'replaygain_album_gain', + float_places=2, suffix=u' dB' + ), out_type=float ) rg_track_peak = MediaField( - MP3DescStorageStyle(u'REPLAYGAIN_TRACK_PEAK', - float_places=6), - MP3DescStorageStyle(u'replaygain_track_peak', - float_places=6), - MP3SoundCheckStorageStyle(key='COMM', index=1, desc=u'iTunNORM', - id3_lang='eng'), - MP4StorageStyle('----:com.apple.iTunes:replaygain_track_peak', - float_places=6), - MP4SoundCheckStorageStyle(key='----:com.apple.iTunes:iTunNORM', - index=1), - StorageStyle(u'REPLAYGAIN_TRACK_PEAK', - float_places=6), - ASFStorageStyle(u'replaygain_track_peak', - float_places=6), + MP3DescStorageStyle( + u'REPLAYGAIN_TRACK_PEAK', + float_places=6 + ), + MP3DescStorageStyle( + u'replaygain_track_peak', + float_places=6 + ), + MP3SoundCheckStorageStyle( + key=u'COMM', + index=1, desc=u'iTunNORM', + id3_lang='eng' + ), + MP4StorageStyle( + '----:com.apple.iTunes:replaygain_track_peak', + float_places=6 + ), + MP4SoundCheckStorageStyle( + '----:com.apple.iTunes:iTunNORM', + index=1 + ), + StorageStyle(u'REPLAYGAIN_TRACK_PEAK', float_places=6), + ASFStorageStyle(u'replaygain_track_peak', float_places=6), out_type=float, ) rg_album_peak = MediaField( - MP3DescStorageStyle(u'REPLAYGAIN_ALBUM_PEAK', - float_places=6), - MP3DescStorageStyle(u'replaygain_album_peak', - float_places=6), - MP4StorageStyle('----:com.apple.iTunes:replaygain_album_peak', - float_places=6), - StorageStyle(u'REPLAYGAIN_ALBUM_PEAK', - float_places=6), - ASFStorageStyle(u'replaygain_album_peak', - float_places=6), + MP3DescStorageStyle( + u'REPLAYGAIN_ALBUM_PEAK', + float_places=6 + ), + MP3DescStorageStyle( + u'replaygain_album_peak', + float_places=6 + ), + MP4StorageStyle( + '----:com.apple.iTunes:replaygain_album_peak', + float_places=6 + ), + StorageStyle(u'REPLAYGAIN_ALBUM_PEAK', float_places=6), + ASFStorageStyle(u'replaygain_album_peak', float_places=6), out_type=float, ) diff --git a/beets/util/__init__.py b/beets/util/__init__.py index 4dd229c91..ebc3806d5 100644 --- a/beets/util/__init__.py +++ b/beets/util/__init__.py @@ -143,7 +143,8 @@ def ancestry(path): break last_path = path - if path: # don't yield '' + if path: + # don't yield '' out.insert(0, path) return out @@ -477,6 +478,8 @@ CHAR_REPLACE = [ (re.compile(ur'\.$'), u'_'), # Trailing dots. (re.compile(ur'\s+$'), u''), # Trailing whitespace. ] + + def sanitize_path(path, replacements=None): """Takes a path (as a Unicode string) and makes sure that it is legal. Returns a new path. Only works with fragments; won't work diff --git a/beets/vfs.py b/beets/vfs.py index 235f36048..e940e21fe 100644 --- a/beets/vfs.py +++ b/beets/vfs.py @@ -20,6 +20,7 @@ from beets import util Node = namedtuple('Node', ['files', 'dirs']) + def _insert(node, path, itemid): """Insert an item into a virtual filesystem node.""" if len(path) == 1: @@ -33,6 +34,7 @@ def _insert(node, path, itemid): node.dirs[dirname] = Node({}, {}) _insert(node.dirs[dirname], rest, itemid) + def libtree(lib): """Generates a filesystem-like directory tree for the files contained in `lib`. Filesystem nodes are (files, dirs) named