mutagen-1.22/0000755000175000017500000000000012213137321013317 5ustar lazkalazka00000000000000mutagen-1.22/docs/0000755000175000017500000000000012213137321014247 5ustar lazkalazka00000000000000mutagen-1.22/docs/man/0000755000175000017500000000000012213137321015022 5ustar lazkalazka00000000000000mutagen-1.22/docs/man/moggsplit.rst0000644000175000017500000000177512213127600017572 0ustar lazkalazka00000000000000=========== moggsplit =========== ------------------------- split Ogg logical streams ------------------------- :Manual section: 1 :Date: Nov 14th, 2009 SYNOPSIS ======== **moggsplit** *filename* ... DESCRIPTION =========== **moggsplit** splits a multiplexed Ogg stream into separate files. For example, it can separate an OGM into separate Ogg DivX and Ogg Vorbis streams, or a chained Ogg Vorbis file into two separate files. OPTIONS ======= --extension Use the supplied extension when generating new files; the default is **ogg**. --pattern Use the supplied pattern when generating new files. This is a Python keyword format string with three variables, *base* for the original file's base name, *stream* for the stream's serial number, and ext for the extension give by **--extension**. The default is **%(base)s-%(stream)d.%(ext)s**. --m3u Generate an m3u playlist along with the newly generated files. Useful for large chained Oggs. AUTHOR ====== Joe Wreschnig mutagen-1.22/docs/man/mutagen-pony.rst0000644000175000017500000000077112213127600020203 0ustar lazkalazka00000000000000============== mutagen-pony ============== --------------------------------- scan a collection of MP3 files --------------------------------- :Manual section: 1 :Date: February 20th, 2006 SYNOPSIS ======== **mutagen-pony** *directory* ... DESCRIPTION =========== **mutagen-pony** scans any directories given and reports on the kinds of tags in the MP3s it finds in them. Ride the pony. It is primarily intended as a debugging tool for Mutagen. AUTHORS ======= Michael Urman and Joe Wreschnig mutagen-1.22/docs/man/index.rst0000644000175000017500000000017112213127600016661 0ustar lazkalazka00000000000000Tools ===== .. toctree:: :titlesonly: mid3iconv mid3v2 moggsplit mutagen-inspect mutagen-pony mutagen-1.22/docs/man/mutagen-inspect.rst0000644000175000017500000000100312213127600020650 0ustar lazkalazka00000000000000================= mutagen-inspect ================= --------------------------------- view Mutagen-supported audio tags --------------------------------- :Manual section: 1 :Date: May 27th, 2006 SYNOPSIS ======== **mutagen-inspect** *filename* ... DESCRIPTION =========== **mutagen-inspect** loads and prints information about an audio file and its tags. It is primarily intended as a debugging tool for Mutagen, but can be useful for extracting tags from the command line. AUTHOR ====== Joe Wreschnig mutagen-1.22/docs/man/mid3v2.rst0000644000175000017500000000662312213127600016666 0ustar lazkalazka00000000000000========= mid3v2 ========= ----------------------------------- audio tag editor similar to 'id3v2' ----------------------------------- :Manual section: 1 :Date: October 30th, 2010 SYNOPSIS ======== **mid3v2** [*options*] *filename* ... DESCRIPTION =========== mid3v2 is a Mutagen-based replacement for id3lib's **id3v2**. It supports ID3v2.4 and more frames; it also does not have the numerous bugs that plague **id3v2**. This program exists mostly for compatibility with programs that want to tag files using **id3v2**. For a more usable interface, we recommend Ex Falso. OPTIONS ======= -q, --quiet Be quiet: do not mention file operations that perform the user's request. Warnings will still be printed. -v, --verbose Be verbose: state all operations performed. This is the opposite of --quiet. This is the default. -e, --escape Enable interpretation of backslash escapes for tag values. Makes it possible to escape the colon-separator in TXXX, COMM values like '\\:' and insert escape sequences like '\\n', '\\t' etc. -f, --list-frames Display all supported ID3v2.3/2.4 frames and their meanings. -L, --list-genres List all ID3v1 numeric genres. These can be used to set TCON frames, but it is not recommended. -l, --list List all tags in the files. The output format is *not* the same as **id3v2**'s; instead, it is easily parsable and readable. Some tags may not have human-readable representations. --list-raw List all tags in the files, in raw format. Although this format is nominally human-readable, it may be very long if the tag contains embedded binary data. -d, --delete-v2 Delete ID3v2 tags. -s, --delete-v1 Delete ID3v1 tags. -D, --delete-all Delete all ID3 tags. --delete-frames=FID1,FID2,... Delete specific ID3v2 frames (or groups of frames) from the files. -C, --convert Convert ID3v1 tags to ID3v2 tags. This will also happen automatically during any editing. -a, --artist\=artist Set the artist information (TPE1). -A, --album\=album Set the album information (TALB). -t, --song\=title Set the title information (TIT2). -c, --comment=DESCRIPTION:COMMENT:LANGUAGE Set a comment (COMM). The language and description may be omitted, in which case the language defaults to English, and the description to an empty string. -g, --genre\=genre Set the genre information (TCON). -y, --year=, --date=YYYY-[MM-DD] Set the year/date information (TDRC). -Tnum/num, --track=num/num Set the track number (TRCK). Any text or URL frame (those beginning with T or W) can be modified or added by prefixing the name of the frame with "--". For example, **--TIT3 "Monkey!"** will set the TIT3 (subtitle) frame to **Monkey!**. The TXXX frame requires a colon-separated description key; many TXXX frames may be set in the file as long as they have different keys. To set this key, just separate the text with a colon, e.g. **--TXXX "ALBUMARTISTSORT:Examples, The"**. The special POPM frame can be set in a similar way: **--POPM "bob@example.com:128:2"** to set Bob's rating to 128/255 with 2 plays. BUGS ==== No sanity checking is done on the editing operations you perform, so mid3v2 will happily accept --TSIZ when editing an ID3v2.4 frame. However, it will also automatically throw it out during the next edit operation. AUTHOR ====== Joe Wreschnig is the author of mid3v2, but he doesn't like to admit it. mutagen-1.22/docs/man/Makefile0000644000175000017500000000026212213127600016461 0ustar lazkalazka00000000000000# create man pages in ./_man all: mid3iconv.1 mid3v2.1 moggsplit.1 mutagen-inspect.1 mutagen-pony.1 setup: mkdir -p _man %.1:%.rst setup rst2man $< > _man/$@ .PHONY: setup mutagen-1.22/docs/man/mid3iconv.rst0000644000175000017500000000147612213127600017456 0ustar lazkalazka00000000000000=========== mid3iconv =========== ------------------------- convert ID3 tag encodings ------------------------- :Manual section: 1 :Date: April 10th, 2006 SYNOPSIS ======== **mid3iconv** [*options*] *filename* ... DESCRIPTION =========== **mid3iconv** converts ID3 tags from legacy encodings to Unicode and stores them using the ID3v2 format. OPTIONS ======= --debug, -d Print updated tags --dry-run, -p Do not actually modify files --encoding, -e Convert from this encoding. By default, your locale's default encoding is used. --force-v1 Use an ID3v1 tag even if an ID3v2 tag is present --quiet, -q Only output errors --remove-v1 Remove any ID3v1 tag after processing the files AUTHOR ====== Emfox Zhou. Based on id3iconv (http://www.cs.berkeley.edu/~zf/id3iconv/) by Feng Zhou. mutagen-1.22/docs/index.rst0000644000175000017500000000755612213127600016124 0ustar lazkalazka00000000000000Mutagen Documentation ===================== .. toctree:: :titlesonly: :maxdepth: 2 tutorial changelog api_notes bugs api/index man/index .. note:: This documentation is still incomplete and it's recommended to read the `source `__ for the full details. What is Mutagen? ---------------- Mutagen is a Python module to handle audio metadata. It supports ASF, FLAC, M4A, Monkey's Audio, MP3, Musepack, Ogg Opus, Ogg FLAC, Ogg Speex, Ogg Theora, Ogg Vorbis, True Audio, WavPack and OptimFROG audio files. All versions of ID3v2 are supported, and all standard ID3v2.4 frames are parsed. It can read Xing headers to accurately calculate the bitrate and length of MP3s. ID3 and APEv2 tags can be edited regardless of audio format. It can also manipulate Ogg streams on an individual packet/page level. Mutagen works on Python 2.6+ / PyPy and has no dependencies outside the CPython standard library. There is a :doc:`brief tutorial with several API examples. ` Where do I get it? ------------------ Mutagen is hosted on `Google Code `_ and `Bitbucket `_. The `download page `_ will have the latest version or check out the Mercurial repository:: $ hg clone https://code.google.com/p/mutagen $ hg clone https://bitbucket.org/lazka/mutagen Why Mutagen? ------------ Quod Libet has more strenuous requirements in a tagging library than most programs that deal with tags. Furthermore, most tagging libraries suck. Therefore we felt it was necessary to write our own. * Mutagen has a simple API, that is roughly the same across all tag formats and versions and integrates into Python's builtin types and interfaces. * New frame types and file formats are easily added, and the behavior of the current formats can be changed by extending them. * Freeform keys, multiple values, Unicode, and other advanced features were considered from the start and are fully supported. * All ID3v2 versions and all ID3v2.4 frames are covered, including rare ones like POPM or RVA2. * We take automated testing very seriously. All bug fixes are commited with a test that prevents them from recurring, and new features are committed with a full test suite. Real World Use -------------- Mutagen can load nearly every MP3 we have thrown at it (when it hasn't, we make it do so). Scripts are included so you can run the same tests on your collection. The following software projects are using Mutagen for tagging: * `Ex Falso and Quod Libet `_, a flexible tagger and player * `Beets `_, a music library manager and MusicBrainz tagger * `Picard `_, cross-platform MusicBrainz tagger * `Puddletag `_, an audio tag editor * `Listen `_, a music player for GNOME * `Exaile `_, a media player aiming to be similar to KDE's AmaroK, but for GTK+ * `ZOMG `_, a command-line player for ZSH * `pytagsfs `_, virtual file system for organizing media files by metadata * Debian's version of `JACK `_, an audio CD ripper, uses Mutagen to tag FLACs * Amarok's replaygain `script `_ Contact ------- For historical and practical reasons, Mutagen shares a `mailing list `_ and IRC channel (#quodlibet on irc.oftc.net) with Quod Libet. If you need help using Mutagen or would like to discuss the library, please use the mailing list. Bugs and patches should go to the `issue tracker `_. mutagen-1.22/docs/tutorial.rst0000644000175000017500000000721612213127600016651 0ustar lazkalazka00000000000000Mutagen Tutorial ---------------- There are two different ways to load files in Mutagen, but both provide similar interfaces. The first is the :class:`Metadata ` API, which deals only in metadata tags. The second is the :class:`FileType ` API, which is a superset of the :class:`mutagen ` API, and contains information about the audio data itself. Both Metadata and FileType objects present a dict-like interface to edit tags. FileType objects also have an 'info' attribute that gives information about the song length, as well as per-format information. In addition, both support the load(filename), save(filename), and delete(filename) instance methods; if no filename is given to save or delete, the last loaded filename is used. This tutorial is only an outline of Mutagen's API. For the full details, you should read the docstrings (pydoc mutagen) or source code. Easy Examples ^^^^^^^^^^^^^ The following code loads a file, sets its title, prints all tag data, then saves the file, first on a FLAC file, then on a Musepack file. The code is almost identical. :: from mutagen.flac import FLAC audio = FLAC("example.flac") audio["title"] = "An example" audio.pprint() audio.save() :: from mutagen.apev2 import APEv2 audio = APEv2("example.mpc") audio["title"] = "An example" audio.pprint() audio.save() The following example gets the length and bitrate of an MP3 file:: from mutagen.mp3 import MP3 audio = MP3("example.mp3") print audio.info.length, audio.info.bitrate The following deletes an ID3 tag from an MP3 file:: from mutagen.id3 import ID3 audio = ID3("example.mp3") audio.delete() Hard Examples: ID3 ^^^^^^^^^^^^^^^^^^ Unlike Vorbis, FLAC, and APEv2 comments, ID3 data is highly structured. Because of this, the interface for ID3 tags is very different from the APEv2 or Vorbis/FLAC interface. For example, to set the title of an ID3 tag, you need to do the following:: from mutagen.id3 import ID3, TIT2 audio = ID3("example.mp3") audio.add(TIT2(encoding=3, text=u"An example")) audio.save() If you use the ID3 module, you should familiarize yourself with how ID3v2 tags are stored, by reading the the details of the ID3v2 standard at http://www.id3.org/develop.html. Easy ID3 ^^^^^^^^ Since reading standards is hard, Mutagen also provides a simpler ID3 interface. :: from mutagen.easyid3 import EasyID3 audio = EasyID3("example.mp3") audio["title"] = u"An example" audio.save() Because of the simpler interface, only a few keys can be edited by EasyID3; to see them, use:: from mutagen.easyid3 import EasyID3 print EasyID3.valid_keys.keys() By default, mutagen.mp3.MP3 uses the real ID3 class. You can make it use EasyID3 as follows:: from mutagen.easyid3 import EasyID3 from mutagen.mp3 import MP3 audio = MP3("example.mp3", ID3=EasyID3) audio.pprint() Unicode ^^^^^^^ Mutagen has full Unicode support for all formats. When you assign text strings, we strongly recommend using Python unicode objects rather than str objects. If you use str objects, Mutagen will assume they are in UTF-8. (This does not apply to strings that must be interpreted as bytes, for example filenames. Those should be passed as str objectss, and will remain str objects within Mutagen.) Multiple Values ^^^^^^^^^^^^^^^ Most tag formats support multiple values for each key, so when you access then (e.g. ``audio["title"]``) you will get a list of strings rather than a single one (``[u"An example"]`` rather than ``u"An example"``). Similarly, you can assign a list of strings rather than a single one. mutagen-1.22/docs/Makefile0000644000175000017500000000010512213127600015702 0ustar lazkalazka00000000000000all: sphinx-build -n . _build clean: rm -rf _build .PHONY: clean mutagen-1.22/docs/changelog.rst0000644000175000017500000000006112213134013016720 0ustar lazkalazka00000000000000Changelog ========= .. literalinclude:: ../NEWS mutagen-1.22/docs/id3_frames_gen.py0000755000175000017500000000254312213127600017474 0ustar lazkalazka00000000000000#!/usr/bin/python # Copyright 2013 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. """ ./id3_frames_gen.py > api/id3_frames.rst """ import sys import os sys.path.insert(0, os.path.abspath('../')) import mutagen.id3 from mutagen.id3 import Frames, Frames_2_2, Frame BaseFrames = dict([(k, v) for (k, v) in vars(mutagen.id3).items() if v not in Frames.values() and v not in Frames_2_2.values() and isinstance(v, type) and (issubclass(v, Frame) or v is Frame)]) def print_header(header, type_="-"): print header print type_ * len(header) print def print_frames(frames, sort_mro=False): if sort_mro: # less bases first, then by name sort_func = lambda x: (len(x[1].__mro__), x[0]) else: sort_func = lambda x: x for name, cls in sorted(frames.items(), key=sort_func): print """ .. autoclass:: mutagen.id3.%s :show-inheritance: :members: """ % repr(cls()) if __name__ == "__main__": print_header("Frame Base Classes") print_frames(BaseFrames, sort_mro=True) print_header("ID3v2.3/4 Frames") print_frames(Frames) print_header("ID3v2.2 Frames") print_frames(Frames_2_2) mutagen-1.22/docs/api_notes.rst0000644000175000017500000000522212213127600016762 0ustar lazkalazka00000000000000API Notes ========= This file documents deprecated parts of the Mutagen API. New code should not use these parts, and several months after being added here, they may be removed. Note that we do not intend to ever deprecate or remove large portions of the API. All of these are corner cases that arose from when Mutagen was still part of Quod Libet, and should never be encountered in normal use. General ------- FileType constructors require a filename. However, the 'delete' and 'save' methods should not be called with one. No modules, types, functions, or attributes beginning with '_' are considered public API. These can and do change drastically between Mutagen versions. This is the standard Python way of marking a function protected or private. Mutagen's goal is to adhere as closely as possible to published specifications. If you try to abuse Mutagen to make it write things in a non-standard fashion, Joe will update Mutagen to break your program. If you want to do nonstandard things, write your own broken library. FLAC ---- The 'vc' attribute predates the FileType API and has been deprecated since Mutagen 0.9; this also applies to the 'add_vc' method. The standard 'tags' attribute and 'add_tags' method should be used instead. ID3 --- None of the Spec objects are considered part of the public API. APEv2 ----- Python 2.5 forced an API change in the APEv2 reading code. Some things which were case-insensitive are now case-sensitive. For example, given:: tag = APEv2() tag["Foo"] = "Bar" print "foo" in tag.keys() Mutagen 1.7.1 and earlier would print "True", as the keys were a str subclass that compared case-insensitively. However, Mutagen 1.8 and above print "False", as the keys are normal strings. :: print "foo" in tag Still prints "True", however, as __getitem__, __delitem__, and __setitem__ (and so any operations on the dict itself) remain case-insensitive. As of 1.10.1, Mutagen no longer allows non-ASCII keys in APEv2 tags. This is in accordance with the APEv2 standard. A KeyError is raised if you try. M4A --- mutagen.m4a is deprecated. You should use mutagen.mp4 instead. MP4 --- There is no MPEG-4 iTunes metadata standard. Mutagen's features are known to lead to problems in other implementations. For example, FAAD will crash when reading a file with multiple "tmpo" atoms. iTunes itself is our main compatibility target. Python 2.6 forced an API change in the MP4 (and M4A) code, by introducing the str.format instance method. Previously the cover image format was available via the .format attribute; it is now available via the .imageformat attribute. On versions of Python prior to 2.6, it is also still available as .format. mutagen-1.22/docs/conf.py0000644000175000017500000000100212213127600015536 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os import sys sys.path.insert(0, os.path.abspath('../')) import mutagen extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] intersphinx_mapping = {'python': ('http://docs.python.org/2.7', None)} source_suffix = '.rst' master_doc = 'index' project = 'mutagen' copyright = u'2013, Joe Wreschnig, Michael Urman, Lukáš Lalinský, ' \ u'Christoph Reiter & others' version = mutagen.version_string release = mutagen.version_string exclude_patterns = ['_build'] mutagen-1.22/docs/bugs.rst0000644000175000017500000000357412213127600015751 0ustar lazkalazka00000000000000Compatibility / Bugs ==================== Mutagen writes ID3v2.4 tags which id3lib cannot read. If you enable ID3v1 tag saving (pass v1=2 to ID3.save), id3lib will read those. iTunes has a bug in its handling of very large ID3 tags (such as tags that contain an attached picture). Mutagen can read tags from iTunes, but iTunes may not be able to read tags written by Quod Libet. Mutagen has had several bugs in correct sync-safe parsing and writing of data length flags in ID3 tags. This will only affect files with very large or compressed ID3 frames (e.g. APIC). As of 1.10 we believe them all to be fixed. Prior to 1.10.1, Mutagen wrote an incorrect flag for APEv2 tags that claimed they did not have footers. This has been fixed, however it means that all APEv2 tags written before 1.10.1 are corrupt. Prior to 1.16, the MP4 cover atom used a .format attribute to indicate the image format (JPEG/PNG). Python 2.6 added a str.format method which conflicts with this. 1.17 provides .imageformat when running on any version, and still provides .format when running on a version before 2.6. Mutagen 1.18 moved EasyID3FileType to mutagen.easyid3, rather than mutagen.id3, which was used in 1.17. Keeping in mutagen.id3 caused circular import problems. To import EasyID3FileType correctly in 1.17 and 1.18 or later:: import mutagen.id3 try: from mutagen.easyid3 import EasyID3FileType except ImportError: # Mutagen 1.17. from mutagen.id3 import EasyID3FileType Mutagen 1.19 made it possible for POPM to have no 'count' attribute. Previously, files that generated POPM frames of this type would fail to load at all. When given date frames less than four characters long (which are already outside the ID3v2 specification), Mutagen 1.20 and earlier would write invalid ID3v1 tags that were too short. Mutagen 1.21 will parse these and fix them if it finds them while saving. mutagen-1.22/docs/api/0000755000175000017500000000000012213137321015020 5ustar lazkalazka00000000000000mutagen-1.22/docs/api/id3.rst0000644000175000017500000000230412213127600016227 0ustar lazkalazka00000000000000ID3v2 ===== .. automodule:: mutagen.id3 ID3 Frames ---------- .. toctree:: :titlesonly: id3_frames ID3 --- .. autoclass:: mutagen.id3.ID3() :show-inheritance: :members: :exclude-members: loaded_frame .. autoclass:: mutagen.id3.ID3FileType(filename, ID3=None) :members: :exclude-members: ID3 EasyID3 ------- .. automodule:: mutagen.easyid3 .. autoclass:: mutagen.easyid3.EasyID3 :show-inheritance: :members: .. autoclass:: mutagen.easyid3.EasyID3FileType :show-inheritance: :members: :exclude-members: ID3 MP3 --- .. automodule:: mutagen.mp3 .. autoclass:: mutagen.mp3.MP3(filename, ID3=None) :show-inheritance: :members: .. autoclass:: mutagen.mp3.MPEGInfo() :members: .. autoclass:: mutagen.mp3.EasyMP3(filename, ID3=None) :show-inheritance: :members: :exclude-members: ID3 TrueAudio --------- .. automodule:: mutagen.trueaudio .. autoclass:: mutagen.trueaudio.TrueAudio(filename, ID3=None) :show-inheritance: :members: .. autoclass:: mutagen.trueaudio.TrueAudioInfo() :members: .. autoclass:: mutagen.trueaudio.EasyTrueAudio(filename, ID3=None) :show-inheritance: :members: :exclude-members: ID3 mutagen-1.22/docs/api/base.rst0000644000175000017500000000104012213127600016456 0ustar lazkalazka00000000000000Main Module ----------- .. automodule:: mutagen :members: File, version, version_string Base Classes ~~~~~~~~~~~~ .. autoclass:: mutagen.FileType(filename) :members: pprint, add_tags, mime :show-inheritance: .. automethod:: delete() .. automethod:: save() .. autoclass:: mutagen.Metadata .. automethod:: delete() .. automethod:: save() Internal Classes ~~~~~~~~~~~~~~~~ .. automodule:: mutagen._util .. autoclass:: mutagen._util.DictMixin .. autoclass:: mutagen._util.DictProxy :show-inheritance: mutagen-1.22/docs/api/asf.rst0000644000175000017500000000030412213127600016317 0ustar lazkalazka00000000000000ASF === .. automodule:: mutagen.asf .. autoclass:: mutagen.asf.ASF :show-inheritance: :members: :undoc-members: .. autoclass:: mutagen.asf.ASFInfo :members: :undoc-members: mutagen-1.22/docs/api/ogg.rst0000644000175000017500000000362512213127600016333 0ustar lazkalazka00000000000000OGG === Ogg bitstreams and pages ------------------------ .. automodule:: mutagen.ogg .. autoexception:: mutagen.ogg.error .. autoclass:: mutagen.ogg.OggFileType(filename) :show-inheritance: .. autoclass:: mutagen.ogg.OggPage :members: Ogg Vorbis ---------- .. automodule:: mutagen.oggvorbis .. autoexception:: mutagen.oggvorbis.error :show-inheritance: .. autoexception:: mutagen.oggvorbis.OggVorbisHeaderError :show-inheritance: .. autoclass:: mutagen.oggvorbis.OggVorbis(filename) :show-inheritance: .. autoclass:: mutagen.oggvorbis.OggVorbisInfo :members: Ogg Opus -------- .. automodule:: mutagen.oggopus .. autoexception:: mutagen.oggopus.error :show-inheritance: .. autoexception:: mutagen.oggopus.OggOpusHeaderError :show-inheritance: .. autoclass:: mutagen.oggopus.OggOpus(filename) :show-inheritance: .. autoclass:: mutagen.oggopus.OggOpusInfo :members: Ogg Speex --------- .. automodule:: mutagen.oggspeex .. autoexception:: mutagen.oggspeex.error :show-inheritance: .. autoexception:: mutagen.oggspeex.OggSpeexHeaderError :show-inheritance: .. autoclass:: mutagen.oggspeex.OggSpeex(filename) :show-inheritance: .. autoclass:: mutagen.oggspeex.OggSpeexInfo :members: Ogg Theora ---------- .. automodule:: mutagen.oggtheora .. autoexception:: mutagen.oggtheora.error :show-inheritance: .. autoexception:: mutagen.oggtheora.OggTheoraHeaderError :show-inheritance: .. autoclass:: mutagen.oggtheora.OggTheora(filename) :show-inheritance: .. autoclass:: mutagen.oggtheora.OggTheoraInfo :members: Ogg FLAC -------- .. automodule:: mutagen.oggflac .. autoexception:: mutagen.oggflac.error :show-inheritance: .. autoexception:: mutagen.oggflac.OggFLACHeaderError :show-inheritance: .. autoclass:: mutagen.oggflac.OggFLAC(filename) :show-inheritance: .. autoclass:: mutagen.oggflac.OggFLACStreamInfo :members: mutagen-1.22/docs/api/flac.rst0000644000175000017500000000075412213127600016464 0ustar lazkalazka00000000000000FLAC ==== .. automodule:: mutagen.flac .. autoclass:: mutagen.flac.FLAC(filename) :show-inheritance: :members: :exclude-members: vc, METADATA_BLOCKS .. autoclass:: mutagen.flac.StreamInfo :members: .. autoclass:: mutagen.flac.Picture :members: .. autoclass:: mutagen.flac.SeekTable :members: .. autoclass:: mutagen.flac.CueSheet :members: .. autoclass:: mutagen.flac.CueSheetTrack :members: .. autoclass:: mutagen.flac.CueSheetTrackIndex :members: mutagen-1.22/docs/api/index.rst0000644000175000017500000000012112213127600016652 0ustar lazkalazka00000000000000API === .. toctree:: base id3 flac ogg ape mp4 asf mutagen-1.22/docs/api/ape.rst0000644000175000017500000000233312213127600016317 0ustar lazkalazka00000000000000APEv2 ===== .. automodule:: mutagen.apev2 APEv2 ----- .. autoexception:: mutagen.apev2.error .. autoexception:: mutagen.apev2.APENoHeaderError .. autoexception:: mutagen.apev2.APEUnsupportedVersionError .. autoexception:: mutagen.apev2.APEBadItemError .. autoclass:: mutagen.apev2.APEv2File(filename) :show-inheritance: :members: :undoc-members: .. autoclass:: mutagen.apev2.APEv2 :show-inheritance: :members: Musepack -------- .. automodule:: mutagen.musepack .. autoclass:: mutagen.musepack.Musepack :show-inheritance: :members: .. autoclass:: mutagen.musepack.MusepackInfo :members: WavPack ------- .. automodule:: mutagen.wavpack .. autoclass:: mutagen.wavpack.WavPack :show-inheritance: :members: .. autoclass:: mutagen.wavpack.WavPackInfo :members: Monkey's Audio -------------- .. automodule:: mutagen.monkeysaudio .. autoclass:: mutagen.monkeysaudio.MonkeysAudio :show-inheritance: :members: .. autoclass:: mutagen.monkeysaudio.MonkeysAudioInfo :members: OptimFROG --------- .. automodule:: mutagen.optimfrog .. autoclass:: mutagen.optimfrog.OptimFROG :show-inheritance: :members: .. autoclass:: mutagen.optimfrog.OptimFROGInfo :members: mutagen-1.22/docs/api/id3_frames.rst0000644000175000017500000004055112213127600017572 0ustar lazkalazka00000000000000Frame Base Classes ------------------ .. autoclass:: mutagen.id3.Frame() :show-inheritance: :members: .. autoclass:: mutagen.id3.BinaryFrame(data='None') :show-inheritance: :members: .. autoclass:: mutagen.id3.FrameOpt() :show-inheritance: :members: .. autoclass:: mutagen.id3.PairedTextFrame(encoding=None, people=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TextFrame(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.UrlFrame(url=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.NumericPartTextFrame(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.NumericTextFrame(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TimeStampTextFrame(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.UrlFrameU(url=u'None') :show-inheritance: :members: ID3v2.3/4 Frames ---------------- .. autoclass:: mutagen.id3.AENC(owner=u'None', preview_start=None, preview_length=None) :show-inheritance: :members: .. autoclass:: mutagen.id3.APIC(encoding=None, mime=u'None', type=None, desc=u'None', data='None') :show-inheritance: :members: .. autoclass:: mutagen.id3.ASPI(S=None, L=None, N=None, b=None, Fi=None) :show-inheritance: :members: .. autoclass:: mutagen.id3.COMM(encoding=None, lang=None, desc=u'None', text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.COMR(encoding=None, price=u'None', valid_until=None, contact=u'None', format=None, seller=u'None', desc=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.ENCR(owner=u'None', method=None, data='None') :show-inheritance: :members: .. autoclass:: mutagen.id3.EQU2(method=None, desc=u'None', adjustments=None) :show-inheritance: :members: .. autoclass:: mutagen.id3.ETCO(format=None, events=None) :show-inheritance: :members: .. autoclass:: mutagen.id3.GEOB(encoding=None, mime=u'None', filename=u'None', desc=u'None', data='None') :show-inheritance: :members: .. autoclass:: mutagen.id3.GRID(owner=u'None', group=None) :show-inheritance: :members: .. autoclass:: mutagen.id3.IPLS(encoding=None, people=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.LINK(frameid=None, url=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.MCDI(data='None') :show-inheritance: :members: .. autoclass:: mutagen.id3.MLLT(frames=None, bytes=None, milliseconds=None, bits_for_bytes=None, bits_for_milliseconds=None, data='None') :show-inheritance: :members: .. autoclass:: mutagen.id3.OWNE(encoding=None, price=u'None', date=None, seller=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.PCNT(count=None) :show-inheritance: :members: .. autoclass:: mutagen.id3.POPM(email=u'None', rating=None) :show-inheritance: :members: .. autoclass:: mutagen.id3.POSS(format=None, position=None) :show-inheritance: :members: .. autoclass:: mutagen.id3.PRIV(owner=u'None', data='None') :show-inheritance: :members: .. autoclass:: mutagen.id3.RBUF(size=None) :show-inheritance: :members: .. autoclass:: mutagen.id3.RVA2(desc=u'None', channel=None, gain=None, peak=None) :show-inheritance: :members: .. autoclass:: mutagen.id3.RVRB(left=None, right=None, bounce_left=None, bounce_right=None, feedback_ltl=None, feedback_ltr=None, feedback_rtr=None, feedback_rtl=None, premix_ltr=None, premix_rtl=None) :show-inheritance: :members: .. autoclass:: mutagen.id3.SEEK(offset=None) :show-inheritance: :members: .. autoclass:: mutagen.id3.SIGN(group=None, sig='None') :show-inheritance: :members: .. autoclass:: mutagen.id3.SYLT(encoding=None, lang=None, format=None, type=None, desc=u'None', text=None) :show-inheritance: :members: .. autoclass:: mutagen.id3.SYTC(format=None, data='None') :show-inheritance: :members: .. autoclass:: mutagen.id3.TALB(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TBPM(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TCMP(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TCOM(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TCON(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TCOP(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TDAT(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TDEN(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TDLY(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TDOR(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TDRC(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TDRL(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TDTG(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TENC(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TEXT(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TFLT(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TIME(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TIPL(encoding=None, people=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TIT1(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TIT2(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TIT3(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TKEY(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TLAN(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TLEN(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TMCL(encoding=None, people=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TMED(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TMOO(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TOAL(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TOFN(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TOLY(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TOPE(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TORY(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TOWN(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TPE1(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TPE2(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TPE3(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TPE4(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TPOS(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TPRO(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TPUB(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TRCK(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TRDA(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TRSN(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TRSO(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TSIZ(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TSO2(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TSOA(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TSOC(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TSOP(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TSOT(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TSRC(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TSSE(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TSST(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TXXX(encoding=None, desc=u'None', text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TYER(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.UFID(owner=u'None', data='None') :show-inheritance: :members: .. autoclass:: mutagen.id3.USER(encoding=None, lang=None, text=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.USLT(encoding=None, lang=None, desc=u'None', text=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.WCOM(url=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.WCOP(url=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.WOAF(url=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.WOAR(url=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.WOAS(url=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.WORS(url=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.WPAY(url=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.WPUB(url=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.WXXX(encoding=None, desc=u'None', url=u'None') :show-inheritance: :members: ID3v2.2 Frames -------------- .. autoclass:: mutagen.id3.BUF(size=None) :show-inheritance: :members: .. autoclass:: mutagen.id3.CNT(count=None) :show-inheritance: :members: .. autoclass:: mutagen.id3.COM(encoding=None, lang=None, desc=u'None', text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.CRA(owner=u'None', preview_start=None, preview_length=None) :show-inheritance: :members: .. autoclass:: mutagen.id3.CRM(owner=u'None', desc=u'None', data='None') :show-inheritance: :members: .. autoclass:: mutagen.id3.ETC(format=None, events=None) :show-inheritance: :members: .. autoclass:: mutagen.id3.GEO(encoding=None, mime=u'None', filename=u'None', desc=u'None', data='None') :show-inheritance: :members: .. autoclass:: mutagen.id3.IPL(encoding=None, people=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.LNK(frameid=None, url=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.MCI(data='None') :show-inheritance: :members: .. autoclass:: mutagen.id3.MLL(frames=None, bytes=None, milliseconds=None, bits_for_bytes=None, bits_for_milliseconds=None, data='None') :show-inheritance: :members: .. autoclass:: mutagen.id3.PIC(encoding=None, mime=None, type=None, desc=u'None', data='None') :show-inheritance: :members: .. autoclass:: mutagen.id3.POP(email=u'None', rating=None) :show-inheritance: :members: .. autoclass:: mutagen.id3.REV(left=None, right=None, bounce_left=None, bounce_right=None, feedback_ltl=None, feedback_ltr=None, feedback_rtr=None, feedback_rtl=None, premix_ltr=None, premix_rtl=None) :show-inheritance: :members: .. autoclass:: mutagen.id3.SLT(encoding=None, lang=None, format=None, type=None, desc=u'None', text=None) :show-inheritance: :members: .. autoclass:: mutagen.id3.STC(format=None, data='None') :show-inheritance: :members: .. autoclass:: mutagen.id3.TAL(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TBP(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TCM(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TCO(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TCP(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TCR(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TDA(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TDY(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TEN(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TFT(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TIM(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TKE(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TLA(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TLE(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TMT(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TOA(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TOF(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TOL(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TOR(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TOT(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TP1(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TP2(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TP3(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TP4(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TPA(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TPB(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TRC(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TRD(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TRK(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TSI(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TSS(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TT1(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TT2(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TT3(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TXT(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TXX(encoding=None, desc=u'None', text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TYE(encoding=None, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.UFI(owner=u'None', data='None') :show-inheritance: :members: .. autoclass:: mutagen.id3.ULT(encoding=None, lang=None, desc=u'None', text=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.WAF(url=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.WAR(url=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.WAS(url=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.WCM(url=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.WCP(url=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.WPB(url=u'None') :show-inheritance: :members: .. autoclass:: mutagen.id3.WXX(encoding=None, desc=u'None', url=u'None') :show-inheritance: :members: mutagen-1.22/docs/api/mp4.rst0000644000175000017500000000131712213127600016253 0ustar lazkalazka00000000000000MP4 === .. automodule:: mutagen.mp4 MP4 --- .. autoclass:: mutagen.mp4.MP4(filename) :show-inheritance: :members: :exclude-members: MP4Tags .. autoclass:: mutagen.mp4.MP4Tags() :show-inheritance: :members: .. autoclass:: mutagen.mp4.MP4Info() :members: .. autoclass:: mutagen.mp4.MP4Cover :members: .. autoclass:: mutagen.mp4.MP4FreeForm :members: .. function:: mutagen.mp4.Open(filename) .. autofunction:: mutagen.mp4.delete EasyMP4 ------- .. automodule:: mutagen.easymp4 .. autoclass:: mutagen.easymp4.EasyMP4(filename) :show-inheritance: :members: :exclude-members: MP4Tags .. autoclass:: mutagen.easymp4.EasyMP4Tags() :show-inheritance: :members: mutagen-1.22/mutagen/0000755000175000017500000000000012213137321014757 5ustar lazkalazka00000000000000mutagen-1.22/mutagen/monkeysaudio.py0000644000175000017500000000534112177421270020053 0ustar lazkalazka00000000000000# A Monkey's Audio (APE) reader/tagger # # Copyright 2006 Lukas Lalinsky # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. """Monkey's Audio streams with APEv2 tags. Monkey's Audio is a very efficient lossless audio compressor developed by Matt Ashland. For more information, see http://www.monkeysaudio.com/. """ __all__ = ["MonkeysAudio", "Open", "delete"] import struct from mutagen.apev2 import APEv2File, error, delete from mutagen._util import cdata class MonkeysAudioHeaderError(error): pass class MonkeysAudioInfo(object): """Monkey's Audio stream information. Attributes: * channels -- number of audio channels * length -- file length in seconds, as a float * sample_rate -- audio sampling rate in Hz * bits_per_sample -- bits per sample * version -- Monkey's Audio stream version, as a float (eg: 3.99) """ def __init__(self, fileobj): header = fileobj.read(76) if len(header) != 76 or not header.startswith("MAC "): raise MonkeysAudioHeaderError("not a Monkey's Audio file") self.version = cdata.ushort_le(header[4:6]) if self.version >= 3980: (blocks_per_frame, final_frame_blocks, total_frames, self.bits_per_sample, self.channels, self.sample_rate) = struct.unpack("= 3950: blocks_per_frame = 73728 * 4 elif self.version >= 3900 or (self.version >= 3800 and compression_level == 4): blocks_per_frame = 73728 else: blocks_per_frame = 9216 self.version /= 1000.0 self.length = 0.0 if self.sample_rate != 0 and total_frames > 0: total_blocks = ((total_frames - 1) * blocks_per_frame + final_frame_blocks) self.length = float(total_blocks) / self.sample_rate def pprint(self): return "Monkey's Audio %.2f, %.2f seconds, %d Hz" % ( self.version, self.length, self.sample_rate) class MonkeysAudio(APEv2File): _Info = MonkeysAudioInfo _mimes = ["audio/ape", "audio/x-ape"] @staticmethod def score(filename, fileobj, header): return header.startswith("MAC ") + filename.lower().endswith(".ape") Open = MonkeysAudio mutagen-1.22/mutagen/_util.py0000644000175000017500000002446312177421270016466 0ustar lazkalazka00000000000000# Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. """Utility classes for Mutagen. You should not rely on the interfaces here being stable. They are intended for internal use in Mutagen only. """ import struct from fnmatch import fnmatchcase class DictMixin(object): """Implement the dict API using keys() and __*item__ methods. Similar to UserDict.DictMixin, this takes a class that defines __getitem__, __setitem__, __delitem__, and keys(), and turns it into a full dict-like object. UserDict.DictMixin is not suitable for this purpose because it's an old-style class. This class is not optimized for very large dictionaries; many functions have linear memory requirements. I recommend you override some of these functions if speed is required. """ def __iter__(self): return iter(self.keys()) def has_key(self, key): try: self[key] except KeyError: return False else: return True __contains__ = has_key iterkeys = lambda self: iter(self.keys()) def values(self): return map(self.__getitem__, self.keys()) itervalues = lambda self: iter(self.values()) def items(self): return zip(self.keys(), self.values()) iteritems = lambda s: iter(s.items()) def clear(self): map(self.__delitem__, self.keys()) def pop(self, key, *args): if len(args) > 1: raise TypeError("pop takes at most two arguments") try: value = self[key] except KeyError: if args: return args[0] else: raise del(self[key]) return value def popitem(self): try: key = self.keys()[0] return key, self.pop(key) except IndexError: raise KeyError("dictionary is empty") def update(self, other=None, **kwargs): if other is None: self.update(kwargs) other = {} try: map(self.__setitem__, other.keys(), other.values()) except AttributeError: for key, value in other: self[key] = value def setdefault(self, key, default=None): try: return self[key] except KeyError: self[key] = default return default def get(self, key, default=None): try: return self[key] except KeyError: return default def __repr__(self): return repr(dict(self.items())) def __cmp__(self, other): if other is None: return 1 else: return cmp(dict(self.items()), other) __hash__ = object.__hash__ def __len__(self): return len(self.keys()) class DictProxy(DictMixin): def __init__(self, *args, **kwargs): self.__dict = {} super(DictProxy, self).__init__(*args, **kwargs) def __getitem__(self, key): return self.__dict[key] def __setitem__(self, key, value): self.__dict[key] = value def __delitem__(self, key): del(self.__dict[key]) def keys(self): return self.__dict.keys() class cdata(object): """C character buffer to Python numeric type conversions.""" from struct import error error = error short_le = staticmethod(lambda data: struct.unpack('h', data)[0]) ushort_be = staticmethod(lambda data: struct.unpack('>H', data)[0]) int_le = staticmethod(lambda data: struct.unpack('i', data)[0]) uint_be = staticmethod(lambda data: struct.unpack('>I', data)[0]) longlong_le = staticmethod(lambda data: struct.unpack('q', data)[0]) ulonglong_be = staticmethod(lambda data: struct.unpack('>Q', data)[0]) to_short_le = staticmethod(lambda data: struct.pack('h', data)) to_ushort_be = staticmethod(lambda data: struct.pack('>H', data)) to_int_le = staticmethod(lambda data: struct.pack('i', data)) to_uint_be = staticmethod(lambda data: struct.pack('>I', data)) to_longlong_le = staticmethod(lambda data: struct.pack('q', data)) to_ulonglong_be = staticmethod(lambda data: struct.pack('>Q', data)) bitswap = ''.join([chr(sum([((val >> i) & 1) << (7-i) for i in range(8)])) for val in range(256)]) del(i) del(val) test_bit = staticmethod(lambda value, n: bool((value >> n) & 1)) def lock(fileobj): """Lock a file object 'safely'. That means a failure to lock because the platform doesn't support fcntl or filesystem locks is not considered a failure. This call does block. Returns whether or not the lock was successful, or raises an exception in more extreme circumstances (full lock table, invalid file). """ try: import fcntl except ImportError: return False else: try: fcntl.lockf(fileobj, fcntl.LOCK_EX) except IOError: # FIXME: There's possibly a lot of complicated # logic that needs to go here in case the IOError # is EACCES or EAGAIN. return False else: return True def unlock(fileobj): """Unlock a file object. Don't call this on a file object unless a call to lock() returned true. """ # If this fails there's a mismatched lock/unlock pair, # so we definitely don't want to ignore errors. import fcntl fcntl.lockf(fileobj, fcntl.LOCK_UN) def insert_bytes(fobj, size, offset, BUFFER_SIZE=2**16): """Insert size bytes of empty space starting at offset. fobj must be an open file object, open rb+ or equivalent. Mutagen tries to use mmap to resize the file, but falls back to a significantly slower method if mmap fails. """ assert 0 < size assert 0 <= offset locked = False fobj.seek(0, 2) filesize = fobj.tell() movesize = filesize - offset fobj.write('\x00' * size) fobj.flush() try: try: import mmap map = mmap.mmap(fobj.fileno(), filesize + size) try: map.move(offset + size, offset, movesize) finally: map.close() except (ValueError, EnvironmentError, ImportError): # handle broken mmap scenarios locked = lock(fobj) fobj.truncate(filesize) fobj.seek(0, 2) padsize = size # Don't generate an enormous string if we need to pad # the file out several megs. while padsize: addsize = min(BUFFER_SIZE, padsize) fobj.write("\x00" * addsize) padsize -= addsize fobj.seek(filesize, 0) while movesize: # At the start of this loop, fobj is pointing at the end # of the data we need to move, which is of movesize length. thismove = min(BUFFER_SIZE, movesize) # Seek back however much we're going to read this frame. fobj.seek(-thismove, 1) nextpos = fobj.tell() # Read it, so we're back at the end. data = fobj.read(thismove) # Seek back to where we need to write it. fobj.seek(-thismove + size, 1) # Write it. fobj.write(data) # And seek back to the end of the unmoved data. fobj.seek(nextpos) movesize -= thismove fobj.flush() finally: if locked: unlock(fobj) def delete_bytes(fobj, size, offset, BUFFER_SIZE=2**16): """Delete size bytes of empty space starting at offset. fobj must be an open file object, open rb+ or equivalent. Mutagen tries to use mmap to resize the file, but falls back to a significantly slower method if mmap fails. """ locked = False assert 0 < size assert 0 <= offset fobj.seek(0, 2) filesize = fobj.tell() movesize = filesize - offset - size assert 0 <= movesize try: if movesize > 0: fobj.flush() try: import mmap map = mmap.mmap(fobj.fileno(), filesize) try: map.move(offset, offset + size, movesize) finally: map.close() except (ValueError, EnvironmentError, ImportError): # handle broken mmap scenarios locked = lock(fobj) fobj.seek(offset + size) buf = fobj.read(BUFFER_SIZE) while buf: fobj.seek(offset) fobj.write(buf) offset += len(buf) fobj.seek(offset + size) buf = fobj.read(BUFFER_SIZE) fobj.truncate(filesize - size) fobj.flush() finally: if locked: unlock(fobj) def utf8(data): """Convert a basestring to a valid UTF-8 str.""" if isinstance(data, str): return data.decode("utf-8", "replace").encode("utf-8") elif isinstance(data, unicode): return data.encode("utf-8") else: raise TypeError("only unicode/str types can be converted to UTF-8") def dict_match(d, key, default=None): try: return d[key] except KeyError: for pattern, value in d.iteritems(): if fnmatchcase(key, pattern): return value return default mutagen-1.22/mutagen/musepack.py0000644000175000017500000002105412177421270017153 0ustar lazkalazka00000000000000# A Musepack reader/tagger # # Copyright 2006 Lukas Lalinsky # Copyright 2012 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. """Musepack audio streams with APEv2 tags. Musepack is an audio format originally based on the MPEG-1 Layer-2 algorithms. Stream versions 4 through 7 are supported. For more information, see http://www.musepack.net/. """ __all__ = ["Musepack", "Open", "delete"] import struct from mutagen.apev2 import APEv2File, error, delete from mutagen.id3 import BitPaddedInt from mutagen._util import cdata class MusepackHeaderError(error): pass RATES = [44100, 48000, 37800, 32000] def _parse_sv8_int(fileobj, limit=9): """Reads (max limit) bytes from fileobj until the MSB is zero. All 7 LSB will be merged to a big endian uint. Raises ValueError in case not MSB is zero, or EOFError in case the file ended before limit is reached. Returns (parsed number, number of bytes read) """ num = 0 for i in xrange(limit): c = fileobj.read(1) if len(c) != 1: raise EOFError num = (num << 7) | (ord(c) & 0x7F) if not ord(c) & 0x80: return num, i + 1 if limit > 0: raise ValueError return 0, 0 def _calc_sv8_gain(gain): # 64.82 taken from mpcdec return 64.82 - gain / 256.0 def _calc_sv8_peak(peak): return (10 ** (peak / (256.0 * 20.0)) / 65535.0) class MusepackInfo(object): """Musepack stream information. Attributes: * channels -- number of audio channels * length -- file length in seconds, as a float * sample_rate -- audio sampling rate in Hz * bitrate -- audio bitrate, in bits per second * version -- Musepack stream version Optional Attributes: * title_gain, title_peak -- Replay Gain and peak data for this song * album_gain, album_peak -- Replay Gain and peak data for this album These attributes are only available in stream version 7/8. The gains are a float, +/- some dB. The peaks are a percentage [0..1] of the maximum amplitude. This means to get a number comparable to VorbisGain, you must multiply the peak by 2. """ def __init__(self, fileobj): header = fileobj.read(4) if len(header) != 4: raise MusepackHeaderError("not a Musepack file") # Skip ID3v2 tags if header[:3] == "ID3": header = fileobj.read(6) if len(header) != 6: raise MusepackHeaderError("not a Musepack file") size = 10 + BitPaddedInt(header[2:6]) fileobj.seek(size) header = fileobj.read(4) if len(header) != 4: raise MusepackHeaderError("not a Musepack file") if header.startswith("MPCK"): self.__parse_sv8(fileobj) else: self.__parse_sv467(fileobj) if not self.bitrate and self.length != 0: fileobj.seek(0, 2) self.bitrate = int(round(fileobj.tell() * 8 / self.length)) def __parse_sv8(self, fileobj): #SV8 http://trac.musepack.net/trac/wiki/SV8Specification key_size = 2 mandatory_packets = ["SH", "RG"] def check_frame_key(key): if len(frame_type) != key_size or not 'AA' <= frame_type <= 'ZZ': raise MusepackHeaderError("Invalid frame key.") frame_type = fileobj.read(key_size) check_frame_key(frame_type) while frame_type not in ("AP", "SE") and mandatory_packets: try: frame_size, slen = _parse_sv8_int(fileobj) except (EOFError, ValueError): raise MusepackHeaderError("Invalid packet size.") data_size = frame_size - key_size - slen if frame_type == "SH": mandatory_packets.remove(frame_type) self.__parse_stream_header(fileobj, data_size) elif frame_type == "RG": mandatory_packets.remove(frame_type) self.__parse_replaygain_packet(fileobj, data_size) else: fileobj.seek(data_size, 1) frame_type = fileobj.read(key_size) check_frame_key(frame_type) if mandatory_packets: raise MusepackHeaderError("Missing mandatory packets: %s." % ", ".join(mandatory_packets)) self.length = float(self.samples) / self.sample_rate self.bitrate = 0 def __parse_stream_header(self, fileobj, data_size): fileobj.seek(4, 1) try: self.version = ord(fileobj.read(1)) except TypeError: raise MusepackHeaderError("SH packet ended unexpectedly.") try: samples, l1 = _parse_sv8_int(fileobj) samples_skip, l2 = _parse_sv8_int(fileobj) except (EOFError, ValueError): raise MusepackHeaderError( "SH packet: Invalid sample counts.") left_size = data_size - 5 - l1 - l2 if left_size != 2: raise MusepackHeaderError("Invalid SH packet size.") data = fileobj.read(left_size) if len(data) != left_size: raise MusepackHeaderError("SH packet ended unexpectedly.") self.sample_rate = RATES[ord(data[-2]) >> 5] self.channels = (ord(data[-1]) >> 4) + 1 self.samples = samples - samples_skip def __parse_replaygain_packet(self, fileobj, data_size): data = fileobj.read(data_size) if data_size != 9: raise MusepackHeaderError("Invalid RG packet size.") if len(data) != data_size: raise MusepackHeaderError("RG packet ended unexpectedly.") title_gain = cdata.short_be(data[1:3]) title_peak = cdata.short_be(data[3:5]) album_gain = cdata.short_be(data[5:7]) album_peak = cdata.short_be(data[7:9]) if title_gain: self.title_gain = _calc_sv8_gain(title_gain) if title_peak: self.title_peak = _calc_sv8_peak(title_peak) if album_gain: self.album_gain = _calc_sv8_gain(album_gain) if album_peak: self.album_peak = _calc_sv8_peak(album_peak) def __parse_sv467(self, fileobj): fileobj.seek(-4, 1) header = fileobj.read(32) if len(header) != 32: raise MusepackHeaderError("not a Musepack file") # SV7 if header.startswith("MP+"): self.version = ord(header[3]) & 0xF if self.version < 7: raise MusepackHeaderError("not a Musepack file") frames = cdata.uint_le(header[4:8]) flags = cdata.uint_le(header[8:12]) self.title_peak, self.title_gain = struct.unpack( "> 16) & 0x0003] self.bitrate = 0 # SV4-SV6 else: header_dword = cdata.uint_le(header[0:4]) self.version = (header_dword >> 11) & 0x03FF if self.version < 4 or self.version > 6: raise MusepackHeaderError("not a Musepack file") self.bitrate = (header_dword >> 23) & 0x01FF self.sample_rate = 44100 if self.version >= 5: frames = cdata.uint_le(header[4:8]) else: frames = cdata.ushort_le(header[6:8]) if self.version < 6: frames -= 1 self.channels = 2 self.length = float(frames * 1152 - 576) / self.sample_rate def pprint(self): rg_data = [] if hasattr(self, "title_gain"): rg_data.append("%+0.2f (title)" % self.title_gain) if hasattr(self, "album_gain"): rg_data.append("%+0.2f (album)" % self.album_gain) rg_data = (rg_data and ", Gain: " + ", ".join(rg_data)) or "" return "Musepack SV%d, %.2f seconds, %d Hz, %d bps%s" % ( self.version, self.length, self.sample_rate, self.bitrate, rg_data) class Musepack(APEv2File): _Info = MusepackInfo _mimes = ["audio/x-musepack", "audio/x-mpc"] @staticmethod def score(filename, fileobj, header): return (header.startswith("MP+") + header.startswith("MPCK") + filename.lower().endswith(".mpc")) Open = Musepack mutagen-1.22/mutagen/flac.py0000644000175000017500000006766212212322576016266 0ustar lazkalazka00000000000000# FLAC comment support for Mutagen # Copyright 2005 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. """Read and write FLAC Vorbis comments and stream information. Read more about FLAC at http://flac.sourceforge.net. FLAC supports arbitrary metadata blocks. The two most interesting ones are the FLAC stream information block, and the Vorbis comment block; these are also the only ones Mutagen can currently read. This module does not handle Ogg FLAC files. Based off documentation available at http://flac.sourceforge.net/format.html """ __all__ = ["FLAC", "Open", "delete"] import struct from cStringIO import StringIO from _vorbis import VCommentDict from mutagen import FileType from mutagen._util import insert_bytes from mutagen.id3 import BitPaddedInt import sys if sys.version_info >= (2, 6): from functools import reduce class error(IOError): pass class FLACNoHeaderError(error): pass class FLACVorbisError(ValueError, error): pass def to_int_be(string): """Convert an arbitrarily-long string to a long using big-endian byte order.""" return reduce(lambda a, b: (a << 8) + ord(b), string, 0L) class StrictFileObject(object): """Wraps a file-like object and raises an exception if the requested amount of data to read isn't returned.""" def __init__(self, fileobj): self._fileobj = fileobj for m in ["close", "tell", "seek", "write", "name"]: if hasattr(fileobj, m): setattr(self, m, getattr(fileobj, m)) def read(self, size=-1): data = self._fileobj.read(size) if size >= 0 and len(data) != size: raise error("file said %d bytes, read %d bytes" % ( size, len(data))) return data def tryread(self, *args): return self._fileobj.read(*args) class MetadataBlock(object): """A generic block of FLAC metadata. This class is extended by specific used as an ancestor for more specific blocks, and also as a container for data blobs of unknown blocks. Attributes: * data -- raw binary data for this block """ _distrust_size = False def __init__(self, data): """Parse the given data string or file-like as a metadata block. The metadata header should not be included.""" if data is not None: if not isinstance(data, StrictFileObject): if isinstance(data, str): data = StringIO(data) elif not hasattr(data, 'read'): raise TypeError( "StreamInfo requires string data or a file-like") data = StrictFileObject(data) self.load(data) def load(self, data): self.data = data.read() def write(self): return self.data @staticmethod def writeblocks(blocks): """Render metadata block as a byte string.""" data = [] codes = [[block.code, block.write()] for block in blocks] codes[-1][0] |= 128 for code, datum in codes: byte = chr(code) if len(datum) > 2**24: raise error("block is too long to write") length = struct.pack(">I", len(datum))[-3:] data.append(byte + length + datum) return "".join(data) @staticmethod def group_padding(blocks): """Consolidate FLAC padding metadata blocks. The overall size of the rendered blocks does not change, so this adds several bytes of padding for each merged block.""" paddings = filter(lambda x: isinstance(x, Padding), blocks) map(blocks.remove, paddings) # total padding size is the sum of padding sizes plus 4 bytes # per removed header. size = sum([padding.length for padding in paddings]) padding = Padding() padding.length = size + 4 * (len(paddings) - 1) blocks.append(padding) class StreamInfo(MetadataBlock): """FLAC stream information. This contains information about the audio data in the FLAC file. Unlike most stream information objects in Mutagen, changes to this one will rewritten to the file when it is saved. Unless you are actually changing the audio stream itself, don't change any attributes of this block. Attributes: * min_blocksize -- minimum audio block size * max_blocksize -- maximum audio block size * sample_rate -- audio sample rate in Hz * channels -- audio channels (1 for mono, 2 for stereo) * bits_per_sample -- bits per sample * total_samples -- total samples in file * length -- audio length in seconds """ code = 0 def __eq__(self, other): try: return (self.min_blocksize == other.min_blocksize and self.max_blocksize == other.max_blocksize and self.sample_rate == other.sample_rate and self.channels == other.channels and self.bits_per_sample == other.bits_per_sample and self.total_samples == other.total_samples) except: return False __hash__ = MetadataBlock.__hash__ def load(self, data): self.min_blocksize = int(to_int_be(data.read(2))) self.max_blocksize = int(to_int_be(data.read(2))) self.min_framesize = int(to_int_be(data.read(3))) self.max_framesize = int(to_int_be(data.read(3))) # first 16 bits of sample rate sample_first = to_int_be(data.read(2)) # last 4 bits of sample rate, 3 of channels, first 1 of bits/sample sample_channels_bps = to_int_be(data.read(1)) # last 4 of bits/sample, 36 of total samples bps_total = to_int_be(data.read(5)) sample_tail = sample_channels_bps >> 4 self.sample_rate = int((sample_first << 4) + sample_tail) if not self.sample_rate: raise error("A sample rate value of 0 is invalid") self.channels = int(((sample_channels_bps >> 1) & 7) + 1) bps_tail = bps_total >> 36 bps_head = (sample_channels_bps & 1) << 4 self.bits_per_sample = int(bps_head + bps_tail + 1) self.total_samples = bps_total & 0xFFFFFFFFFL self.length = self.total_samples / float(self.sample_rate) self.md5_signature = to_int_be(data.read(16)) def write(self): f = StringIO() f.write(struct.pack(">I", self.min_blocksize)[-2:]) f.write(struct.pack(">I", self.max_blocksize)[-2:]) f.write(struct.pack(">I", self.min_framesize)[-3:]) f.write(struct.pack(">I", self.max_framesize)[-3:]) # first 16 bits of sample rate f.write(struct.pack(">I", self.sample_rate >> 4)[-2:]) # 4 bits sample, 3 channel, 1 bps byte = (self.sample_rate & 0xF) << 4 byte += ((self.channels - 1) & 7) << 1 byte += ((self.bits_per_sample - 1) >> 4) & 1 f.write(chr(byte)) # 4 bits of bps, 4 of sample count byte = ((self.bits_per_sample - 1) & 0xF) << 4 byte += (self.total_samples >> 32) & 0xF f.write(chr(byte)) # last 32 of sample count f.write(struct.pack(">I", self.total_samples & 0xFFFFFFFFL)) # MD5 signature sig = self.md5_signature f.write(struct.pack( ">4I", (sig >> 96) & 0xFFFFFFFFL, (sig >> 64) & 0xFFFFFFFFL, (sig >> 32) & 0xFFFFFFFFL, sig & 0xFFFFFFFFL)) return f.getvalue() def pprint(self): return "FLAC, %.2f seconds, %d Hz" % (self.length, self.sample_rate) class SeekPoint(tuple): """A single seek point in a FLAC file. Placeholder seek points have first_sample of 0xFFFFFFFFFFFFFFFFL, and byte_offset and num_samples undefined. Seek points must be sorted in ascending order by first_sample number. Seek points must be unique by first_sample number, except for placeholder points. Placeholder points must occur last in the table and there may be any number of them. Attributes: * first_sample -- sample number of first sample in the target frame * byte_offset -- offset from first frame to target frame * num_samples -- number of samples in target frame """ def __new__(cls, first_sample, byte_offset, num_samples): return super(cls, SeekPoint).__new__( cls, (first_sample, byte_offset, num_samples)) first_sample = property(lambda self: self[0]) byte_offset = property(lambda self: self[1]) num_samples = property(lambda self: self[2]) class SeekTable(MetadataBlock): """Read and write FLAC seek tables. Attributes: * seekpoints -- list of SeekPoint objects """ __SEEKPOINT_FORMAT = '>QQH' __SEEKPOINT_SIZE = struct.calcsize(__SEEKPOINT_FORMAT) code = 3 def __init__(self, data): self.seekpoints = [] super(SeekTable, self).__init__(data) def __eq__(self, other): try: return (self.seekpoints == other.seekpoints) except (AttributeError, TypeError): return False __hash__ = MetadataBlock.__hash__ def load(self, data): self.seekpoints = [] sp = data.tryread(self.__SEEKPOINT_SIZE) while len(sp) == self.__SEEKPOINT_SIZE: self.seekpoints.append(SeekPoint( *struct.unpack(self.__SEEKPOINT_FORMAT, sp))) sp = data.tryread(self.__SEEKPOINT_SIZE) def write(self): f = StringIO() for seekpoint in self.seekpoints: packed = struct.pack( self.__SEEKPOINT_FORMAT, seekpoint.first_sample, seekpoint.byte_offset, seekpoint.num_samples) f.write(packed) return f.getvalue() def __repr__(self): return "<%s seekpoints=%r>" % (type(self).__name__, self.seekpoints) class VCFLACDict(VCommentDict): """Read and write FLAC Vorbis comments. FLACs don't use the framing bit at the end of the comment block. So this extends VCommentDict to not use the framing bit. """ code = 4 _distrust_size = True def load(self, data, errors='replace', framing=False): super(VCFLACDict, self).load(data, errors=errors, framing=framing) def write(self, framing=False): return super(VCFLACDict, self).write(framing=framing) class CueSheetTrackIndex(tuple): """Index for a track in a cuesheet. For CD-DA, an index_number of 0 corresponds to the track pre-gap. The first index in a track must have a number of 0 or 1, and subsequently, index_numbers must increase by 1. Index_numbers must be unique within a track. And index_offset must be evenly divisible by 588 samples. Attributes: * index_number -- index point number * index_offset -- offset in samples from track start """ def __new__(cls, index_number, index_offset): return super(cls, CueSheetTrackIndex).__new__( cls, (index_number, index_offset)) index_number = property(lambda self: self[0]) index_offset = property(lambda self: self[1]) class CueSheetTrack(object): """A track in a cuesheet. For CD-DA, track_numbers must be 1-99, or 170 for the lead-out. Track_numbers must be unique within a cue sheet. There must be atleast one index in every track except the lead-out track which must have none. Attributes: * track_number -- track number * start_offset -- track offset in samples from start of FLAC stream * isrc -- ISRC code * type -- 0 for audio, 1 for digital data * pre_emphasis -- true if the track is recorded with pre-emphasis * indexes -- list of CueSheetTrackIndex objects """ def __init__(self, track_number, start_offset, isrc='', type_=0, pre_emphasis=False): self.track_number = track_number self.start_offset = start_offset self.isrc = isrc self.type = type_ self.pre_emphasis = pre_emphasis self.indexes = [] def __eq__(self, other): try: return (self.track_number == other.track_number and self.start_offset == other.start_offset and self.isrc == other.isrc and self.type == other.type and self.pre_emphasis == other.pre_emphasis and self.indexes == other.indexes) except (AttributeError, TypeError): return False __hash__ = object.__hash__ def __repr__(self): return ("<%s number=%r, offset=%d, isrc=%r, type=%r, " "pre_emphasis=%r, indexes=%r)>") % ( type(self).__name__, self.track_number, self.start_offset, self.isrc, self.type, self.pre_emphasis, self.indexes) class CueSheet(MetadataBlock): """Read and write FLAC embedded cue sheets. Number of tracks should be from 1 to 100. There should always be exactly one lead-out track and that track must be the last track in the cue sheet. Attributes: * media_catalog_number -- media catalog number in ASCII * lead_in_samples -- number of lead-in samples * compact_disc -- true if the cuesheet corresponds to a compact disc * tracks -- list of CueSheetTrack objects * lead_out -- lead-out as CueSheetTrack or None if lead-out was not found """ __CUESHEET_FORMAT = '>128sQB258xB' __CUESHEET_SIZE = struct.calcsize(__CUESHEET_FORMAT) __CUESHEET_TRACK_FORMAT = '>QB12sB13xB' __CUESHEET_TRACK_SIZE = struct.calcsize(__CUESHEET_TRACK_FORMAT) __CUESHEET_TRACKINDEX_FORMAT = '>QB3x' __CUESHEET_TRACKINDEX_SIZE = struct.calcsize(__CUESHEET_TRACKINDEX_FORMAT) code = 5 media_catalog_number = '' lead_in_samples = 88200 compact_disc = True def __init__(self, data): self.tracks = [] super(CueSheet, self).__init__(data) def __eq__(self, other): try: return (self.media_catalog_number == other.media_catalog_number and self.lead_in_samples == other.lead_in_samples and self.compact_disc == other.compact_disc and self.tracks == other.tracks) except (AttributeError, TypeError): return False __hash__ = MetadataBlock.__hash__ def load(self, data): header = data.read(self.__CUESHEET_SIZE) media_catalog_number, lead_in_samples, flags, num_tracks = \ struct.unpack(self.__CUESHEET_FORMAT, header) self.media_catalog_number = media_catalog_number.rstrip('\0') self.lead_in_samples = lead_in_samples self.compact_disc = bool(flags & 0x80) self.tracks = [] for i in range(num_tracks): track = data.read(self.__CUESHEET_TRACK_SIZE) start_offset, track_number, isrc_padded, flags, num_indexes = \ struct.unpack(self.__CUESHEET_TRACK_FORMAT, track) isrc = isrc_padded.rstrip('\0') type_ = (flags & 0x80) >> 7 pre_emphasis = bool(flags & 0x40) val = CueSheetTrack( track_number, start_offset, isrc, type_, pre_emphasis) for j in range(num_indexes): index = data.read(self.__CUESHEET_TRACKINDEX_SIZE) index_offset, index_number = struct.unpack( self.__CUESHEET_TRACKINDEX_FORMAT, index) val.indexes.append( CueSheetTrackIndex(index_number, index_offset)) self.tracks.append(val) def write(self): f = StringIO() flags = 0 if self.compact_disc: flags |= 0x80 packed = struct.pack( self.__CUESHEET_FORMAT, self.media_catalog_number, self.lead_in_samples, flags, len(self.tracks)) f.write(packed) for track in self.tracks: track_flags = 0 track_flags |= (track.type & 1) << 7 if track.pre_emphasis: track_flags |= 0x40 track_packed = struct.pack( self.__CUESHEET_TRACK_FORMAT, track.start_offset, track.track_number, track.isrc, track_flags, len(track.indexes)) f.write(track_packed) for index in track.indexes: index_packed = struct.pack( self.__CUESHEET_TRACKINDEX_FORMAT, index.index_offset, index.index_number) f.write(index_packed) return f.getvalue() def __repr__(self): return ("<%s media_catalog_number=%r, lead_in=%r, compact_disc=%r, " "tracks=%r>") % ( type(self).__name__, self.media_catalog_number, self.lead_in_samples, self.compact_disc, self.tracks) class Picture(MetadataBlock): """Read and write FLAC embed pictures. Attributes: * type -- picture type (same as types for ID3 APIC frames) * mime -- MIME type of the picture * desc -- picture's description * width -- width in pixels * height -- height in pixels * depth -- color depth in bits-per-pixel * colors -- number of colors for indexed palettes (like GIF), 0 for non-indexed * data -- picture data """ code = 6 _distrust_size = True def __init__(self, data=None): self.type = 0 self.mime = u'' self.desc = u'' self.width = 0 self.height = 0 self.depth = 0 self.colors = 0 self.data = '' super(Picture, self).__init__(data) def __eq__(self, other): try: return (self.type == other.type and self.mime == other.mime and self.desc == other.desc and self.width == other.width and self.height == other.height and self.depth == other.depth and self.colors == other.colors and self.data == other.data) except (AttributeError, TypeError): return False __hash__ = MetadataBlock.__hash__ def load(self, data): self.type, length = struct.unpack('>2I', data.read(8)) self.mime = data.read(length).decode('UTF-8', 'replace') length, = struct.unpack('>I', data.read(4)) self.desc = data.read(length).decode('UTF-8', 'replace') (self.width, self.height, self.depth, self.colors, length) = struct.unpack('>5I', data.read(20)) self.data = data.read(length) def write(self): f = StringIO() mime = self.mime.encode('UTF-8') f.write(struct.pack('>2I', self.type, len(mime))) f.write(mime) desc = self.desc.encode('UTF-8') f.write(struct.pack('>I', len(desc))) f.write(desc) f.write(struct.pack('>5I', self.width, self.height, self.depth, self.colors, len(self.data))) f.write(self.data) return f.getvalue() def __repr__(self): return "<%s '%s' (%d bytes)>" % (type(self).__name__, self.mime, len(self.data)) class Padding(MetadataBlock): """Empty padding space for metadata blocks. To avoid rewriting the entire FLAC file when editing comments, metadata is often padded. Padding should occur at the end, and no more than one padding block should be in any FLAC file. Mutagen handles this with MetadataBlock.group_padding. """ code = 1 def __init__(self, data=""): super(Padding, self).__init__(data) def load(self, data): self.length = len(data.read()) def write(self): try: return "\x00" * self.length # On some 64 bit platforms this won't generate a MemoryError # or OverflowError since you might have enough RAM, but it # still generates a ValueError. On other 64 bit platforms, # this will still succeed for extremely large values. # Those should never happen in the real world, and if they # do, writeblocks will catch it. except (OverflowError, ValueError, MemoryError): raise error("cannot write %d bytes" % self.length) def __eq__(self, other): return isinstance(other, Padding) and self.length == other.length __hash__ = MetadataBlock.__hash__ def __repr__(self): return "<%s (%d bytes)>" % (type(self).__name__, self.length) class FLAC(FileType): """A FLAC audio file. Attributes: * info -- stream information (length, bitrate, sample rate) * tags -- metadata tags, if any * cuesheet -- CueSheet object, if any * seektable -- SeekTable object, if any * pictures -- list of embedded pictures """ _mimes = ["audio/x-flac", "application/x-flac"] METADATA_BLOCKS = [StreamInfo, Padding, None, SeekTable, VCFLACDict, CueSheet, Picture] """Known metadata block types, indexed by ID.""" @staticmethod def score(filename, fileobj, header): return (header.startswith("fLaC") + filename.lower().endswith(".flac") * 3) def __read_metadata_block(self, fileobj): byte = ord(fileobj.read(1)) size = to_int_be(fileobj.read(3)) code = byte & 0x7F last_block = bool(byte & 0x80) try: block_type = self.METADATA_BLOCKS[code] or MetadataBlock except IndexError: block_type = MetadataBlock if block_type._distrust_size: # Some jackass is writing broken Metadata block length # for Vorbis comment blocks, and the FLAC reference # implementaton can parse them (mostly by accident), # so we have to too. Instead of parsing the size # given, parse an actual Vorbis comment, leaving # fileobj in the right position. # http://code.google.com/p/mutagen/issues/detail?id=52 # ..same for the Picture block: # http://code.google.com/p/mutagen/issues/detail?id=106 block = block_type(fileobj) else: data = fileobj.read(size) block = block_type(data) block.code = code if block.code == VCFLACDict.code: if self.tags is None: self.tags = block else: raise FLACVorbisError("> 1 Vorbis comment block found") elif block.code == CueSheet.code: if self.cuesheet is None: self.cuesheet = block else: raise error("> 1 CueSheet block found") elif block.code == SeekTable.code: if self.seektable is None: self.seektable = block else: raise error("> 1 SeekTable block found") self.metadata_blocks.append(block) return not last_block def add_tags(self): """Add a Vorbis comment block to the file.""" if self.tags is None: self.tags = VCFLACDict() self.metadata_blocks.append(self.tags) else: raise FLACVorbisError("a Vorbis comment already exists") add_vorbiscomment = add_tags def delete(self, filename=None): """Remove Vorbis comments from a file. If no filename is given, the one most recently loaded is used. """ if filename is None: filename = self.filename for s in list(self.metadata_blocks): if isinstance(s, VCFLACDict): self.metadata_blocks.remove(s) self.tags = None self.save() break vc = property(lambda s: s.tags, doc="Alias for tags; don't use this.") def load(self, filename): """Load file information from a filename.""" self.metadata_blocks = [] self.tags = None self.cuesheet = None self.seektable = None self.filename = filename fileobj = StrictFileObject(open(filename, "rb")) try: self.__check_header(fileobj) while self.__read_metadata_block(fileobj): pass finally: fileobj.close() try: self.metadata_blocks[0].length except (AttributeError, IndexError): raise FLACNoHeaderError("Stream info block not found") @property def info(self): return self.metadata_blocks[0] def add_picture(self, picture): """Add a new picture to the file.""" self.metadata_blocks.append(picture) def clear_pictures(self): """Delete all pictures from the file.""" self.metadata_blocks = filter(lambda b: b.code != Picture.code, self.metadata_blocks) @property def pictures(self): """List of embedded pictures""" return filter(lambda b: b.code == Picture.code, self.metadata_blocks) def save(self, filename=None, deleteid3=False): """Save metadata blocks to a file. If no filename is given, the one most recently loaded is used. """ if filename is None: filename = self.filename f = open(filename, 'rb+') try: # Ensure we've got padding at the end, and only at the end. # If adding makes it too large, we'll scale it down later. self.metadata_blocks.append(Padding('\x00' * 1020)) MetadataBlock.group_padding(self.metadata_blocks) header = self.__check_header(f) # "fLaC" and maybe ID3 available = self.__find_audio_offset(f) - header data = MetadataBlock.writeblocks(self.metadata_blocks) # Delete ID3v2 if deleteid3 and header > 4: available += header - 4 header = 4 if len(data) > available: # If we have too much data, see if we can reduce padding. padding = self.metadata_blocks[-1] newlength = padding.length - (len(data) - available) if newlength > 0: padding.length = newlength data = MetadataBlock.writeblocks(self.metadata_blocks) assert len(data) == available elif len(data) < available: # If we have too little data, increase padding. self.metadata_blocks[-1].length += (available - len(data)) data = MetadataBlock.writeblocks(self.metadata_blocks) assert len(data) == available if len(data) != available: # We couldn't reduce the padding enough. diff = (len(data) - available) insert_bytes(f, diff, header) f.seek(header - 4) f.write("fLaC" + data) # Delete ID3v1 if deleteid3: try: f.seek(-128, 2) except IOError: pass else: if f.read(3) == "TAG": f.seek(-128, 2) f.truncate() finally: f.close() def __find_audio_offset(self, fileobj): byte = 0x00 while not (byte & 0x80): byte = ord(fileobj.read(1)) size = to_int_be(fileobj.read(3)) try: block_type = self.METADATA_BLOCKS[byte & 0x7F] except IndexError: block_type = None if block_type and block_type._distrust_size: # See comments in read_metadata_block; the size can't # be trusted for Vorbis comment blocks and Picture block block_type(fileobj) else: fileobj.read(size) return fileobj.tell() def __check_header(self, fileobj): size = 4 header = fileobj.read(4) if header != "fLaC": size = None if header[:3] == "ID3": size = 14 + BitPaddedInt(fileobj.read(6)[2:]) fileobj.seek(size - 4) if fileobj.read(4) != "fLaC": size = None if size is None: raise FLACNoHeaderError( "%r is not a valid FLAC file" % fileobj.name) return size Open = FLAC def delete(filename): """Remove tags from a file.""" FLAC(filename).delete() mutagen-1.22/mutagen/trueaudio.py0000644000175000017500000000411012177421270017336 0ustar lazkalazka00000000000000# True Audio support for Mutagen # Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. """True Audio audio stream information and tags. True Audio is a lossless format designed for real-time encoding and decoding. This module is based on the documentation at http://www.true-audio.com/TTA_Lossless_Audio_Codec\_-_Format_Description True Audio files use ID3 tags. """ __all__ = ["TrueAudio", "Open", "delete", "EasyTrueAudio"] from mutagen.id3 import ID3FileType, delete from mutagen._util import cdata class error(RuntimeError): pass class TrueAudioHeaderError(error, IOError): pass class TrueAudioInfo(object): """True Audio stream information. Attributes: * length - audio length, in seconds * sample_rate - audio sample rate, in Hz """ def __init__(self, fileobj, offset): fileobj.seek(offset or 0) header = fileobj.read(18) if len(header) != 18 or not header.startswith("TTA"): raise TrueAudioHeaderError("TTA header not found") self.sample_rate = cdata.int_le(header[10:14]) samples = cdata.uint_le(header[14:18]) self.length = float(samples) / self.sample_rate def pprint(self): return "True Audio, %.2f seconds, %d Hz." % ( self.length, self.sample_rate) class TrueAudio(ID3FileType): """A True Audio file. :ivar info: :class:`TrueAudioInfo` :ivar tags: :class:`ID3 ` """ _Info = TrueAudioInfo _mimes = ["audio/x-tta"] @staticmethod def score(filename, fileobj, header): return (header.startswith("ID3") + header.startswith("TTA") + filename.lower().endswith(".tta") * 2) Open = TrueAudio class EasyTrueAudio(TrueAudio): """Like MP3, but uses EasyID3 for tags. :ivar info: :class:`TrueAudioInfo` :ivar tags: :class:`EasyID3 ` """ from mutagen.easyid3 import EasyID3 as ID3 ID3 = ID3 mutagen-1.22/mutagen/mp4.py0000644000175000017500000006723212211635335016051 0ustar lazkalazka00000000000000# Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. """Read and write MPEG-4 audio files with iTunes metadata. This module will read MPEG-4 audio information and metadata, as found in Apple's MP4 (aka M4A, M4B, M4P) files. There is no official specification for this format. The source code for TagLib, FAAD, and various MPEG specifications at * http://developer.apple.com/documentation/QuickTime/QTFF/ * http://www.geocities.com/xhelmboyx/quicktime/formats/mp4-layout.txt * http://standards.iso.org/ittf/PubliclyAvailableStandards/\ c041828_ISO_IEC_14496-12_2005(E).zip * http://wiki.multimedia.cx/index.php?title=Apple_QuickTime were all consulted. """ import struct import sys from mutagen import FileType, Metadata from mutagen._constants import GENRES from mutagen._util import cdata, insert_bytes, DictProxy, utf8 class error(IOError): pass class MP4MetadataError(error): pass class MP4StreamInfoError(error): pass class MP4MetadataValueError(ValueError, MP4MetadataError): pass # This is not an exhaustive list of container atoms, but just the # ones this module needs to peek inside. _CONTAINERS = ["moov", "udta", "trak", "mdia", "meta", "ilst", "stbl", "minf", "moof", "traf"] _SKIP_SIZE = {"meta": 4} __all__ = ['MP4', 'Open', 'delete', 'MP4Cover', 'MP4FreeForm'] class MP4Cover(str): """A cover artwork. Attributes: * imageformat -- format of the image (either FORMAT_JPEG or FORMAT_PNG) """ FORMAT_JPEG = 0x0D FORMAT_PNG = 0x0E def __new__(cls, data, *args, **kwargs): return str.__new__(cls, data) def __init__(self, data, imageformat=FORMAT_JPEG): self.imageformat = imageformat try: self.format except AttributeError: self.format = imageformat class MP4FreeForm(str): """A freeform value. Attributes: * dataformat -- format of the data (either FORMAT_TEXT or FORMAT_DATA) """ FORMAT_DATA = 0x0 FORMAT_TEXT = 0x1 def __new__(cls, data, *args, **kwargs): return str.__new__(cls, data) def __init__(self, data, dataformat=FORMAT_TEXT): self.dataformat = dataformat class Atom(object): """An individual atom. Attributes: children -- list child atoms (or None for non-container atoms) length -- length of this atom, including length and name name -- four byte name of the atom, as a str offset -- location in the constructor-given fileobj of this atom This structure should only be used internally by Mutagen. """ children = None def __init__(self, fileobj, level=0): self.offset = fileobj.tell() self.length, self.name = struct.unpack(">I4s", fileobj.read(8)) if self.length == 1: self.length, = struct.unpack(">Q", fileobj.read(8)) if self.length < 16: raise MP4MetadataError( "64 bit atom length can only be 16 and higher") elif self.length == 0: if level != 0: raise MP4MetadataError( "only a top-level atom can have zero length") # Only the last atom is supposed to have a zero-length, meaning it # extends to the end of file. fileobj.seek(0, 2) self.length = fileobj.tell() - self.offset fileobj.seek(self.offset + 8, 0) elif self.length < 8: raise MP4MetadataError( "atom length can only be 0, 1 or 8 and higher") if self.name in _CONTAINERS: self.children = [] fileobj.seek(_SKIP_SIZE.get(self.name, 0), 1) while fileobj.tell() < self.offset + self.length: self.children.append(Atom(fileobj, level + 1)) else: fileobj.seek(self.offset + self.length, 0) @staticmethod def render(name, data): """Render raw atom data.""" # this raises OverflowError if Py_ssize_t can't handle the atom data size = len(data) + 8 if size <= 0xFFFFFFFF: return struct.pack(">I4s", size, name) + data else: return struct.pack(">I4sQ", 1, name, size + 8) + data def findall(self, name, recursive=False): """Recursively find all child atoms by specified name.""" if self.children is not None: for child in self.children: if child.name == name: yield child if recursive: for atom in child.findall(name, True): yield atom def __getitem__(self, remaining): """Look up a child atom, potentially recursively. e.g. atom['udta', 'meta'] => """ if not remaining: return self elif self.children is None: raise KeyError("%r is not a container" % self.name) for child in self.children: if child.name == remaining[0]: return child[remaining[1:]] else: raise KeyError("%r not found" % remaining[0]) def __repr__(self): klass = self.__class__.__name__ if self.children is None: return "<%s name=%r length=%r offset=%r>" % ( klass, self.name, self.length, self.offset) else: children = "\n".join([" " + line for child in self.children for line in repr(child).splitlines()]) return "<%s name=%r length=%r offset=%r\n%s>" % ( klass, self.name, self.length, self.offset, children) class Atoms(object): """Root atoms in a given file. Attributes: atoms -- a list of top-level atoms as Atom objects This structure should only be used internally by Mutagen. """ def __init__(self, fileobj): self.atoms = [] fileobj.seek(0, 2) end = fileobj.tell() fileobj.seek(0) while fileobj.tell() + 8 <= end: self.atoms.append(Atom(fileobj)) def path(self, *names): """Look up and return the complete path of an atom. For example, atoms.path('moov', 'udta', 'meta') will return a list of three atoms, corresponding to the moov, udta, and meta atoms. """ path = [self] for name in names: path.append(path[-1][name, ]) return path[1:] def __contains__(self, names): try: self[names] except KeyError: return False return True def __getitem__(self, names): """Look up a child atom. 'names' may be a list of atoms (['moov', 'udta']) or a string specifying the complete path ('moov.udta'). """ if isinstance(names, basestring): names = names.split(".") for child in self.atoms: if child.name == names[0]: return child[names[1:]] else: raise KeyError("%s not found" % names[0]) def __repr__(self): return "\n".join([repr(child) for child in self.atoms]) class MP4Tags(DictProxy, Metadata): r"""Dictionary containing Apple iTunes metadata list key/values. Keys are four byte identifiers, except for freeform ('----') keys. Values are usually unicode strings, but some atoms have a special structure: Text values (multiple values per key are supported): * '\\xa9nam' -- track title * '\\xa9alb' -- album * '\\xa9ART' -- artist * 'aART' -- album artist * '\\xa9wrt' -- composer * '\\xa9day' -- year * '\\xa9cmt' -- comment * 'desc' -- description (usually used in podcasts) * 'purd' -- purchase date * '\\xa9grp' -- grouping * '\\xa9gen' -- genre * '\\xa9lyr' -- lyrics * 'purl' -- podcast URL * 'egid' -- podcast episode GUID * 'catg' -- podcast category * 'keyw' -- podcast keywords * '\\xa9too' -- encoded by * 'cprt' -- copyright * 'soal' -- album sort order * 'soaa' -- album artist sort order * 'soar' -- artist sort order * 'sonm' -- title sort order * 'soco' -- composer sort order * 'sosn' -- show sort order * 'tvsh' -- show name Boolean values: * 'cpil' -- part of a compilation * 'pgap' -- part of a gapless album * 'pcst' -- podcast (iTunes reads this only on import) Tuples of ints (multiple values per key are supported): * 'trkn' -- track number, total tracks * 'disk' -- disc number, total discs Others: * 'tmpo' -- tempo/BPM, 16 bit int * 'covr' -- cover artwork, list of MP4Cover objects (which are tagged strs) * 'gnre' -- ID3v1 genre. Not supported, use '\\xa9gen' instead. The freeform '----' frames use a key in the format '----:mean:name' where 'mean' is usually 'com.apple.iTunes' and 'name' is a unique identifier for this frame. The value is a str, but is probably text that can be decoded as UTF-8. Multiple values per key are supported. MP4 tag data cannot exist outside of the structure of an MP4 file, so this class should not be manually instantiated. Unknown non-text tags are removed. """ def load(self, atoms, fileobj): try: ilst = atoms["moov.udta.meta.ilst"] except KeyError, key: raise MP4MetadataError(key) for atom in ilst.children: fileobj.seek(atom.offset + 8) data = fileobj.read(atom.length - 8) if len(data) != atom.length - 8: raise MP4MetadataError("Not enough data") if atom.name in self.__atoms: info = self.__atoms[atom.name] info[0](self, atom, data, *info[2:]) else: # unknown atom, try as text and skip if it fails # FIXME: keep them somehow try: self.__parse_text(atom, data) except MP4MetadataError: continue @classmethod def _can_load(cls, atoms): return "moov.udta.meta.ilst" in atoms @staticmethod def __key_sort(item1, item2): (key1, v1) = item1 (key2, v2) = item2 # iTunes always writes the tags in order of "relevance", try # to copy it as closely as possible. order = ["\xa9nam", "\xa9ART", "\xa9wrt", "\xa9alb", "\xa9gen", "gnre", "trkn", "disk", "\xa9day", "cpil", "pgap", "pcst", "tmpo", "\xa9too", "----", "covr", "\xa9lyr"] order = dict(zip(order, range(len(order)))) last = len(order) # If there's no key-based way to distinguish, order by length. # If there's still no way, go by string comparison on the # values, so we at least have something determinstic. return (cmp(order.get(key1[:4], last), order.get(key2[:4], last)) or cmp(len(v1), len(v2)) or cmp(v1, v2)) def save(self, filename): """Save the metadata to the given filename.""" values = [] items = self.items() items.sort(self.__key_sort) for key, value in items: info = self.__atoms.get(key[:4], (None, type(self).__render_text)) try: values.append(info[1](self, key, value, *info[2:])) except (TypeError, ValueError), s: raise MP4MetadataValueError, s, sys.exc_info()[2] data = Atom.render("ilst", "".join(values)) # Find the old atoms. fileobj = open(filename, "rb+") try: atoms = Atoms(fileobj) try: path = atoms.path("moov", "udta", "meta", "ilst") except KeyError: self.__save_new(fileobj, atoms, data) else: self.__save_existing(fileobj, atoms, path, data) finally: fileobj.close() def __pad_ilst(self, data, length=None): if length is None: length = ((len(data) + 1023) & ~1023) - len(data) return Atom.render("free", "\x00" * length) def __save_new(self, fileobj, atoms, ilst): hdlr = Atom.render("hdlr", "\x00" * 8 + "mdirappl" + "\x00" * 9) meta = Atom.render( "meta", "\x00\x00\x00\x00" + hdlr + ilst + self.__pad_ilst(ilst)) try: path = atoms.path("moov", "udta") except KeyError: # moov.udta not found -- create one path = atoms.path("moov") meta = Atom.render("udta", meta) offset = path[-1].offset + 8 insert_bytes(fileobj, len(meta), offset) fileobj.seek(offset) fileobj.write(meta) self.__update_parents(fileobj, path, len(meta)) self.__update_offsets(fileobj, atoms, len(meta), offset) def __save_existing(self, fileobj, atoms, path, data): # Replace the old ilst atom. ilst = path.pop() offset = ilst.offset length = ilst.length # Check for padding "free" atoms meta = path[-1] index = meta.children.index(ilst) try: prev = meta.children[index-1] if prev.name == "free": offset = prev.offset length += prev.length except IndexError: pass try: next = meta.children[index+1] if next.name == "free": length += next.length except IndexError: pass delta = len(data) - length if delta > 0 or (delta < 0 and delta > -8): data += self.__pad_ilst(data) delta = len(data) - length insert_bytes(fileobj, delta, offset) elif delta < 0: data += self.__pad_ilst(data, -delta - 8) delta = 0 fileobj.seek(offset) fileobj.write(data) self.__update_parents(fileobj, path, delta) self.__update_offsets(fileobj, atoms, delta, offset) def __update_parents(self, fileobj, path, delta): """Update all parent atoms with the new size.""" for atom in path: fileobj.seek(atom.offset) size = cdata.uint_be(fileobj.read(4)) if size == 1: # 64bit # skip name (4B) and read size (8B) size = cdata.ulonglong_be(fileobj.read(12)[4:]) fileobj.seek(atom.offset + 8) fileobj.write(cdata.to_ulonglong_be(size + delta)) else: # 32bit fileobj.seek(atom.offset) fileobj.write(cdata.to_uint_be(size + delta)) def __update_offset_table(self, fileobj, fmt, atom, delta, offset): """Update offset table in the specified atom.""" if atom.offset > offset: atom.offset += delta fileobj.seek(atom.offset + 12) data = fileobj.read(atom.length - 12) fmt = fmt % cdata.uint_be(data[:4]) offsets = struct.unpack(fmt, data[4:]) offsets = [o + (0, delta)[offset < o] for o in offsets] fileobj.seek(atom.offset + 16) fileobj.write(struct.pack(fmt, *offsets)) def __update_tfhd(self, fileobj, atom, delta, offset): if atom.offset > offset: atom.offset += delta fileobj.seek(atom.offset + 9) data = fileobj.read(atom.length - 9) flags = cdata.uint_be("\x00" + data[:3]) if flags & 1: o = cdata.ulonglong_be(data[7:15]) if o > offset: o += delta fileobj.seek(atom.offset + 16) fileobj.write(cdata.to_ulonglong_be(o)) def __update_offsets(self, fileobj, atoms, delta, offset): """Update offset tables in all 'stco' and 'co64' atoms.""" if delta == 0: return moov = atoms["moov"] for atom in moov.findall('stco', True): self.__update_offset_table(fileobj, ">%dI", atom, delta, offset) for atom in moov.findall('co64', True): self.__update_offset_table(fileobj, ">%dQ", atom, delta, offset) try: for atom in atoms["moof"].findall('tfhd', True): self.__update_tfhd(fileobj, atom, delta, offset) except KeyError: pass def __parse_data(self, atom, data): pos = 0 while pos < atom.length - 8: length, name, flags = struct.unpack(">I4sI", data[pos:pos+12]) if name != "data": raise MP4MetadataError( "unexpected atom %r inside %r" % (name, atom.name)) yield flags, data[pos+16:pos+length] pos += length def __render_data(self, key, flags, value): return Atom.render(key, "".join([ Atom.render("data", struct.pack(">2I", flags, 0) + data) for data in value])) def __parse_freeform(self, atom, data): length = cdata.uint_be(data[:4]) mean = data[12:length] pos = length length = cdata.uint_be(data[pos:pos+4]) name = data[pos+12:pos+length] pos += length value = [] while pos < atom.length - 8: length, atom_name = struct.unpack(">I4s", data[pos:pos+8]) if atom_name != "data": raise MP4MetadataError( "unexpected atom %r inside %r" % (atom_name, atom.name)) version = ord(data[pos+8]) if version != 0: raise MP4MetadataError("Unsupported version: %r" % version) flags = struct.unpack(">I", "\x00" + data[pos+9:pos+12])[0] value.append(MP4FreeForm(data[pos+16:pos+length], dataformat=flags)) pos += length if value: self["%s:%s:%s" % (atom.name, mean, name)] = value def __render_freeform(self, key, value): dummy, mean, name = key.split(":", 2) mean = struct.pack(">I4sI", len(mean) + 12, "mean", 0) + mean name = struct.pack(">I4sI", len(name) + 12, "name", 0) + name if isinstance(value, basestring): value = [value] data = "" for v in value: flags = MP4FreeForm.FORMAT_TEXT if isinstance(v, MP4FreeForm): flags = v.dataformat data += struct.pack(">I4s2I", len(v) + 16, "data", flags, 0) data += v return Atom.render("----", mean + name + data) def __parse_pair(self, atom, data): self[atom.name] = [struct.unpack(">2H", d[2:6]) for flags, d in self.__parse_data(atom, data)] def __render_pair(self, key, value): data = [] for (track, total) in value: if 0 <= track < 1 << 16 and 0 <= total < 1 << 16: data.append(struct.pack(">4H", 0, track, total, 0)) else: raise MP4MetadataValueError( "invalid numeric pair %r" % ((track, total),)) return self.__render_data(key, 0, data) def __render_pair_no_trailing(self, key, value): data = [] for (track, total) in value: if 0 <= track < 1 << 16 and 0 <= total < 1 << 16: data.append(struct.pack(">3H", 0, track, total)) else: raise MP4MetadataValueError( "invalid numeric pair %r" % ((track, total),)) return self.__render_data(key, 0, data) def __parse_genre(self, atom, data): # Translate to a freeform genre. genre = cdata.short_be(data[16:18]) if "\xa9gen" not in self: try: self["\xa9gen"] = [GENRES[genre - 1]] except IndexError: pass def __parse_tempo(self, atom, data): self[atom.name] = [cdata.ushort_be(value[1]) for value in self.__parse_data(atom, data)] def __render_tempo(self, key, value): try: if len(value) == 0: return self.__render_data(key, 0x15, "") if min(value) < 0 or max(value) >= 2**16: raise MP4MetadataValueError( "invalid 16 bit integers: %r" % value) except TypeError: raise MP4MetadataValueError( "tmpo must be a list of 16 bit integers") values = map(cdata.to_ushort_be, value) return self.__render_data(key, 0x15, values) def __parse_bool(self, atom, data): try: self[atom.name] = bool(ord(data[16:17])) except TypeError: self[atom.name] = False def __render_bool(self, key, value): return self.__render_data(key, 0x15, [chr(bool(value))]) def __parse_cover(self, atom, data): self[atom.name] = [] pos = 0 while pos < atom.length - 8: length, name, imageformat = struct.unpack(">I4sI", data[pos:pos+12]) if name != "data": if name == "name": pos += length continue raise MP4MetadataError( "unexpected atom %r inside 'covr'" % name) if imageformat not in (MP4Cover.FORMAT_JPEG, MP4Cover.FORMAT_PNG): imageformat = MP4Cover.FORMAT_JPEG cover = MP4Cover(data[pos+16:pos+length], imageformat) self[atom.name].append(cover) pos += length def __render_cover(self, key, value): atom_data = [] for cover in value: try: imageformat = cover.imageformat except AttributeError: imageformat = MP4Cover.FORMAT_JPEG atom_data.append(Atom.render( "data", struct.pack(">2I", imageformat, 0) + cover)) return Atom.render(key, "".join(atom_data)) def __parse_text(self, atom, data, expected_flags=1): value = [text.decode('utf-8', 'replace') for flags, text in self.__parse_data(atom, data) if flags == expected_flags] if value: self[atom.name] = value def __render_text(self, key, value, flags=1): if isinstance(value, basestring): value = [value] return self.__render_data( key, flags, map(utf8, value)) def delete(self, filename): """Remove the metadata from the given filename.""" self.clear() self.save(filename) __atoms = { "----": (__parse_freeform, __render_freeform), "trkn": (__parse_pair, __render_pair), "disk": (__parse_pair, __render_pair_no_trailing), "gnre": (__parse_genre, None), "tmpo": (__parse_tempo, __render_tempo), "cpil": (__parse_bool, __render_bool), "pgap": (__parse_bool, __render_bool), "pcst": (__parse_bool, __render_bool), "covr": (__parse_cover, __render_cover), "purl": (__parse_text, __render_text, 0), "egid": (__parse_text, __render_text, 0), } # the text atoms we know about which should make loading fail if parsing # any of them fails for name in ["\xa9nam", "\xa9alb", "\xa9ART", "aART", "\xa9wrt", "\xa9day", "\xa9cmt", "desc", "purd", "\xa9grp", "\xa9gen", "\xa9lyr", "catg", "keyw", "\xa9too", "cprt", "soal", "soaa", "soar", "sonm", "soco", "sosn", "tvsh"]: __atoms[name] = (__parse_text, __render_text) def pprint(self): values = [] for key, value in self.iteritems(): key = key.decode('latin1') if key == "covr": values.append("%s=%s" % (key, ", ".join( ["[%d bytes of data]" % len(data) for data in value]))) elif isinstance(value, list): values.append("%s=%s" % (key, " / ".join(map(unicode, value)))) else: values.append("%s=%s" % (key, value)) return "\n".join(values) class MP4Info(object): """MPEG-4 stream information. Attributes: * bitrate -- bitrate in bits per second, as an int * length -- file length in seconds, as a float * channels -- number of audio channels * sample_rate -- audio sampling rate in Hz * bits_per_sample -- bits per sample """ bitrate = 0 channels = 0 sample_rate = 0 bits_per_sample = 0 def __init__(self, atoms, fileobj): for trak in list(atoms["moov"].findall("trak")): hdlr = trak["mdia", "hdlr"] fileobj.seek(hdlr.offset) data = fileobj.read(hdlr.length) if data[16:20] == "soun": break else: raise MP4StreamInfoError("track has no audio data") mdhd = trak["mdia", "mdhd"] fileobj.seek(mdhd.offset) data = fileobj.read(mdhd.length) if ord(data[8]) == 0: offset = 20 fmt = ">2I" else: offset = 28 fmt = ">IQ" end = offset + struct.calcsize(fmt) unit, length = struct.unpack(fmt, data[offset:end]) self.length = float(length) / unit try: atom = trak["mdia", "minf", "stbl", "stsd"] fileobj.seek(atom.offset) data = fileobj.read(atom.length) if data[20:24] == "mp4a": length = cdata.uint_be(data[16:20]) (self.channels, self.bits_per_sample, _, self.sample_rate) = struct.unpack(">3HI", data[40:50]) # ES descriptor type if data[56:60] == "esds" and ord(data[64:65]) == 0x03: pos = 65 # skip extended descriptor type tag, length, ES ID # and stream priority if data[pos:pos+3] == "\x80\x80\x80": pos += 3 pos += 4 # decoder config descriptor type if ord(data[pos]) == 0x04: pos += 1 # skip extended descriptor type tag, length, # object type ID, stream type, buffer size # and maximum bitrate if data[pos:pos+3] == "\x80\x80\x80": pos += 3 pos += 10 # average bitrate self.bitrate = cdata.uint_be(data[pos:pos+4]) except (ValueError, KeyError): # stsd atoms are optional pass def pprint(self): return "MPEG-4 audio, %.2f seconds, %d bps" % ( self.length, self.bitrate) class MP4(FileType): """An MPEG-4 audio file, probably containing AAC. If more than one track is present in the file, the first is used. Only audio ('soun') tracks will be read. :ivar info: :class:`MP4Info` :ivar tags: :class:`MP4Tags` """ MP4Tags = MP4Tags _mimes = ["audio/mp4", "audio/x-m4a", "audio/mpeg4", "audio/aac"] def load(self, filename): self.filename = filename fileobj = open(filename, "rb") try: atoms = Atoms(fileobj) # ftyp is always the first atom in a valid MP4 file if not atoms.atoms or atoms.atoms[0].name != "ftyp": raise error("Not a MP4 file") try: self.info = MP4Info(atoms, fileobj) except StandardError, err: raise MP4StreamInfoError, err, sys.exc_info()[2] if not MP4Tags._can_load(atoms): self.tags = None else: try: self.tags = self.MP4Tags(atoms, fileobj) except StandardError, err: raise MP4MetadataError, err, sys.exc_info()[2] finally: fileobj.close() def add_tags(self): if self.tags is None: self.tags = self.MP4Tags() else: raise error("an MP4 tag already exists") @staticmethod def score(filename, fileobj, header): return ("ftyp" in header) + ("mp4" in header) Open = MP4 def delete(filename): """Remove tags from a file.""" MP4(filename).delete() mutagen-1.22/mutagen/oggspeex.py0000644000175000017500000000776612177421270017202 0ustar lazkalazka00000000000000# Ogg Speex support. # # Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. """Read and write Ogg Speex comments. This module handles Speex files wrapped in an Ogg bitstream. The first Speex stream found is used. Read more about Ogg Speex at http://www.speex.org/. This module is based on the specification at http://www.speex.org/manual2/node7.html and clarifications after personal communication with Jean-Marc, http://lists.xiph.org/pipermail/speex-dev/2006-July/004676.html. """ __all__ = ["OggSpeex", "Open", "delete"] from mutagen._vorbis import VCommentDict from mutagen.ogg import OggPage, OggFileType, error as OggError from mutagen._util import cdata class error(OggError): pass class OggSpeexHeaderError(error): pass class OggSpeexInfo(object): """Ogg Speex stream information. Attributes: * bitrate - nominal bitrate in bits per second * channels - number of channels * length - file length in seconds, as a float The reference encoder does not set the bitrate; in this case, the bitrate will be 0. """ length = 0 def __init__(self, fileobj): page = OggPage(fileobj) while not page.packets[0].startswith("Speex "): page = OggPage(fileobj) if not page.first: raise OggSpeexHeaderError( "page has ID header, but doesn't start a stream") self.sample_rate = cdata.uint_le(page.packets[0][36:40]) self.channels = cdata.uint_le(page.packets[0][48:52]) self.bitrate = max(0, cdata.int_le(page.packets[0][52:56])) self.serial = page.serial def _post_tags(self, fileobj): page = OggPage.find_last(fileobj, self.serial) self.length = page.position / float(self.sample_rate) def pprint(self): return "Ogg Speex, %.2f seconds" % self.length class OggSpeexVComment(VCommentDict): """Speex comments embedded in an Ogg bitstream.""" def __init__(self, fileobj, info): pages = [] complete = False while not complete: page = OggPage(fileobj) if page.serial == info.serial: pages.append(page) complete = page.complete or (len(page.packets) > 1) data = OggPage.to_packets(pages)[0] + "\x01" super(OggSpeexVComment, self).__init__(data, framing=False) def _inject(self, fileobj): """Write tag data into the Speex comment packet/page.""" fileobj.seek(0) # Find the first header page, with the stream info. # Use it to get the serial number. page = OggPage(fileobj) while not page.packets[0].startswith("Speex "): page = OggPage(fileobj) # Look for the next page with that serial number, it'll start # the comment packet. serial = page.serial page = OggPage(fileobj) while page.serial != serial: page = OggPage(fileobj) # Then find all the pages with the comment packet. old_pages = [page] while not (old_pages[-1].complete or len(old_pages[-1].packets) > 1): page = OggPage(fileobj) if page.serial == old_pages[0].serial: old_pages.append(page) packets = OggPage.to_packets(old_pages, strict=False) # Set the new comment packet. packets[0] = self.write(framing=False) new_pages = OggPage.from_packets(packets, old_pages[0].sequence) OggPage.replace(fileobj, old_pages, new_pages) class OggSpeex(OggFileType): """An Ogg Speex file.""" _Info = OggSpeexInfo _Tags = OggSpeexVComment _Error = OggSpeexHeaderError _mimes = ["audio/x-speex"] @staticmethod def score(filename, fileobj, header): return (header.startswith("OggS") * ("Speex " in header)) Open = OggSpeex def delete(filename): """Remove tags from a file.""" OggSpeex(filename).delete() mutagen-1.22/mutagen/_id3specs.py0000644000175000017500000003223512212062144017211 0ustar lazkalazka00000000000000# Copyright (C) 2005 Michael Urman # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. import struct from struct import unpack, pack from warnings import warn from mutagen._id3util import ID3JunkFrameError, ID3Warning, BitPaddedInt class Spec(object): def __init__(self, name): self.name = name def __hash__(self): raise TypeError("Spec objects are unhashable") def _validate23(self, frame, value, **kwargs): """Return a possibly modified value which, if written, results in valid id3v2.3 data. """ return value class ByteSpec(Spec): def read(self, frame, data): return ord(data[0]), data[1:] def write(self, frame, value): return chr(value) def validate(self, frame, value): if value is not None: chr(value) return value class IntegerSpec(Spec): def read(self, frame, data): return int(BitPaddedInt(data, bits=8)), '' def write(self, frame, value): return BitPaddedInt.to_str(value, bits=8, width=-1) def validate(self, frame, value): return value class SizedIntegerSpec(Spec): def __init__(self, name, size): self.name, self.__sz = name, size def read(self, frame, data): return int(BitPaddedInt(data[:self.__sz], bits=8)), data[self.__sz:] def write(self, frame, value): return BitPaddedInt.to_str(value, bits=8, width=self.__sz) def validate(self, frame, value): return value class EncodingSpec(ByteSpec): def read(self, frame, data): enc, data = super(EncodingSpec, self).read(frame, data) if enc < 16: return enc, data else: return 0, chr(enc)+data def validate(self, frame, value): if 0 <= value <= 3: return value if value is None: return None raise ValueError('Invalid Encoding: %r' % value) def _validate23(self, frame, value, **kwargs): # only 0, 1 are valid in v2.3, default to utf-16 return min(1, value) class StringSpec(Spec): def __init__(self, name, length): super(StringSpec, self).__init__(name) self.len = length def read(s, frame, data): return data[:s.len], data[s.len:] def write(s, frame, value): if value is None: return '\x00' * s.len else: return (str(value) + '\x00' * s.len)[:s.len] def validate(s, frame, value): if value is None: return None if isinstance(value, basestring) and len(value) == s.len: return value raise ValueError('Invalid StringSpec[%d] data: %r' % (s.len, value)) class BinaryDataSpec(Spec): def read(self, frame, data): return data, '' def write(self, frame, value): return str(value) def validate(self, frame, value): return str(value) class EncodedTextSpec(Spec): # Okay, seriously. This is private and defined explicitly and # completely by the ID3 specification. You can't just add # encodings here however you want. _encodings = ( ('latin1', '\x00'), ('utf16', '\x00\x00'), ('utf_16_be', '\x00\x00'), ('utf8', '\x00') ) def read(self, frame, data): enc, term = self._encodings[frame.encoding] ret = '' if len(term) == 1: if term in data: data, ret = data.split(term, 1) else: offset = -1 try: while True: offset = data.index(term, offset+1) if offset & 1: continue data, ret = data[0:offset], data[offset+2:] break except ValueError: pass if len(data) < len(term): return u'', ret return data.decode(enc), ret def write(self, frame, value): enc, term = self._encodings[frame.encoding] return value.encode(enc) + term def validate(self, frame, value): return unicode(value) class MultiSpec(Spec): def __init__(self, name, *specs, **kw): super(MultiSpec, self).__init__(name) self.specs = specs self.sep = kw.get('sep') def read(self, frame, data): values = [] while data: record = [] for spec in self.specs: value, data = spec.read(frame, data) record.append(value) if len(self.specs) != 1: values.append(record) else: values.append(record[0]) return values, data def write(self, frame, value): data = [] if len(self.specs) == 1: for v in value: data.append(self.specs[0].write(frame, v)) else: for record in value: for v, s in zip(record, self.specs): data.append(s.write(frame, v)) return ''.join(data) def validate(self, frame, value): if value is None: return [] if self.sep and isinstance(value, basestring): value = value.split(self.sep) if isinstance(value, list): if len(self.specs) == 1: return [self.specs[0].validate(frame, v) for v in value] else: return [ [s.validate(frame, v) for (v, s) in zip(val, self.specs)] for val in value] raise ValueError('Invalid MultiSpec data: %r' % value) def _validate23(self, frame, value, **kwargs): if len(self.specs) != 1: return [[s._validate23(frame, v, **kwargs) for (v, s) in zip(val, self.specs)] for val in value] spec = self.specs[0] # Merge single text spec multispecs only. # (TimeStampSpec beeing the exception, but it's not a valid v2.3 frame) if not isinstance(spec, EncodedTextSpec) or \ isinstance(spec, TimeStampSpec): return value value = [spec._validate23(frame, v, **kwargs) for v in value] if kwargs.get("sep") is not None: return [spec.validate(frame, kwargs["sep"].join(value))] return value class EncodedNumericTextSpec(EncodedTextSpec): pass class EncodedNumericPartTextSpec(EncodedTextSpec): pass class Latin1TextSpec(EncodedTextSpec): def read(self, frame, data): if '\x00' in data: data, ret = data.split('\x00', 1) else: ret = '' return data.decode('latin1'), ret def write(self, data, value): return value.encode('latin1') + '\x00' def validate(self, frame, value): return unicode(value) class ID3TimeStamp(object): """A time stamp in ID3v2 format. This is a restricted form of the ISO 8601 standard; time stamps take the form of: YYYY-MM-DD HH:MM:SS Or some partial form (YYYY-MM-DD HH, YYYY, etc.). The 'text' attribute contains the raw text data of the time stamp. """ import re def __init__(self, text): if isinstance(text, ID3TimeStamp): text = text.text self.text = text __formats = ['%04d'] + ['%02d'] * 5 __seps = ['-', '-', ' ', ':', ':', 'x'] def get_text(self): parts = [self.year, self.month, self.day, self.hour, self.minute, self.second] pieces = [] for i, part in enumerate(iter(iter(parts).next, None)): pieces.append(self.__formats[i] % part + self.__seps[i]) return u''.join(pieces)[:-1] def set_text(self, text, splitre=re.compile('[-T:/.]|\s+')): year, month, day, hour, minute, second = \ splitre.split(text + ':::::')[:6] for a in 'year month day hour minute second'.split(): try: v = int(locals()[a]) except ValueError: v = None setattr(self, a, v) text = property(get_text, set_text, doc="ID3v2.4 date and time.") def __str__(self): return self.text def __repr__(self): return repr(self.text) def __cmp__(self, other): return cmp(self.text, other.text) __hash__ = object.__hash__ def encode(self, *args): return self.text.encode(*args) class TimeStampSpec(EncodedTextSpec): def read(self, frame, data): value, data = super(TimeStampSpec, self).read(frame, data) return self.validate(frame, value), data def write(self, frame, data): return super(TimeStampSpec, self).write(frame, data.text.replace(' ', 'T')) def validate(self, frame, value): try: return ID3TimeStamp(value) except TypeError: raise ValueError("Invalid ID3TimeStamp: %r" % value) class ChannelSpec(ByteSpec): (OTHER, MASTER, FRONTRIGHT, FRONTLEFT, BACKRIGHT, BACKLEFT, FRONTCENTRE, BACKCENTRE, SUBWOOFER) = range(9) class VolumeAdjustmentSpec(Spec): def read(self, frame, data): value, = unpack('>h', data[0:2]) return value/512.0, data[2:] def write(self, frame, value): return pack('>h', int(round(value * 512))) def validate(self, frame, value): if value is not None: try: self.write(frame, value) except struct.error: raise ValueError("out of range") return value class VolumePeakSpec(Spec): def read(self, frame, data): # http://bugs.xmms.org/attachment.cgi?id=113&action=view peak = 0 bits = ord(data[0]) bytes = min(4, (bits + 7) >> 3) # not enough frame data if bytes + 1 > len(data): raise ID3JunkFrameError shift = ((8 - (bits & 7)) & 7) + (4 - bytes) * 8 for i in range(1, bytes+1): peak *= 256 peak += ord(data[i]) peak *= 2 ** shift return (float(peak) / (2**31-1)), data[1+bytes:] def write(self, frame, value): # always write as 16 bits for sanity. return "\x10" + pack('>H', int(round(value * 32768))) def validate(self, frame, value): if value is not None: try: self.write(frame, value) except struct.error: raise ValueError("out of range") return value class SynchronizedTextSpec(EncodedTextSpec): def read(self, frame, data): texts = [] encoding, term = self._encodings[frame.encoding] while data: l = len(term) try: value_idx = data.index(term) except ValueError: raise ID3JunkFrameError value = data[:value_idx].decode(encoding) if len(data) < value_idx + l + 4: raise ID3JunkFrameError time, = struct.unpack(">I", data[value_idx+l:value_idx+l+4]) texts.append((value, time)) data = data[value_idx+l+4:] return texts, "" def write(self, frame, value): data = [] encoding, term = self._encodings[frame.encoding] for text, time in frame.text: text = text.encode(encoding) + term data.append(text + struct.pack(">I", time)) return "".join(data) def validate(self, frame, value): return value class KeyEventSpec(Spec): def read(self, frame, data): events = [] while len(data) >= 5: events.append(struct.unpack(">bI", data[:5])) data = data[5:] return events, data def write(self, frame, value): return "".join([struct.pack(">bI", *event) for event in value]) def validate(self, frame, value): return value class VolumeAdjustmentsSpec(Spec): # Not to be confused with VolumeAdjustmentSpec. def read(self, frame, data): adjustments = {} while len(data) >= 4: freq, adj = struct.unpack(">Hh", data[:4]) data = data[4:] freq /= 2.0 adj /= 512.0 adjustments[freq] = adj adjustments = adjustments.items() adjustments.sort() return adjustments, data def write(self, frame, value): value.sort() return "".join([struct.pack(">Hh", int(freq * 2), int(adj * 512)) for (freq, adj) in value]) def validate(self, frame, value): return value class ASPIIndexSpec(Spec): def read(self, frame, data): if frame.b == 16: format = "H" size = 2 elif frame.b == 8: format = "B" size = 1 else: warn("invalid bit count in ASPI (%d)" % frame.b, ID3Warning) return [], data indexes = data[:frame.N * size] data = data[frame.N * size:] return list(struct.unpack(">" + format * frame.N, indexes)), data def write(self, frame, values): if frame.b == 16: format = "H" elif frame.b == 8: format = "B" else: raise ValueError("frame.b must be 8 or 16") return struct.pack(">" + format * frame.N, *values) def validate(self, frame, values): return values mutagen-1.22/mutagen/asf.py0000644000175000017500000005204512177421270016120 0ustar lazkalazka00000000000000# Copyright 2006-2007 Lukas Lalinsky # Copyright 2005-2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. """Read and write ASF (Window Media Audio) files.""" __all__ = ["ASF", "Open"] import struct from mutagen import FileType, Metadata from mutagen._util import insert_bytes, delete_bytes, DictMixin class error(IOError): pass class ASFError(error): pass class ASFHeaderError(error): pass class ASFInfo(object): """ASF stream information.""" def __init__(self): self.length = 0.0 self.sample_rate = 0 self.bitrate = 0 self.channels = 0 def pprint(self): s = "Windows Media Audio %d bps, %s Hz, %d channels, %.2f seconds" % ( self.bitrate, self.sample_rate, self.channels, self.length) return s class ASFTags(list, DictMixin, Metadata): """Dictionary containing ASF attributes.""" def pprint(self): return "\n".join(["%s=%s" % (k, v) for k, v in self]) def __getitem__(self, key): """A list of values for the key. This is a copy, so comment['title'].append('a title') will not work. """ values = [value for (k, value) in self if k == key] if not values: raise KeyError(key) else: return values def __delitem__(self, key): """Delete all values associated with the key.""" to_delete = filter(lambda x: x[0] == key, self) if not to_delete: raise KeyError(key) else: map(self.remove, to_delete) def __contains__(self, key): """Return true if the key has any values.""" for k, value in self: if k == key: return True else: return False def __setitem__(self, key, values): """Set a key's value or values. Setting a value overwrites all old ones. The value may be a list of Unicode or UTF-8 strings, or a single Unicode or UTF-8 string. """ if not isinstance(values, list): values = [values] try: del(self[key]) except KeyError: pass for value in values: if key in _standard_attribute_names: value = unicode(value) elif not isinstance(value, ASFBaseAttribute): if isinstance(value, basestring): value = ASFUnicodeAttribute(value) elif isinstance(value, bool): value = ASFBoolAttribute(value) elif isinstance(value, int): value = ASFDWordAttribute(value) elif isinstance(value, long): value = ASFQWordAttribute(value) self.append((key, value)) def keys(self): """Return all keys in the comment.""" return self and set(zip(*self)[0]) def as_dict(self): """Return a copy of the comment data in a real dict.""" d = {} for key, value in self: d.setdefault(key, []).append(value) return d class ASFBaseAttribute(object): """Generic attribute.""" TYPE = None def __init__(self, value=None, data=None, language=None, stream=None, **kwargs): self.language = language self.stream = stream if data: self.value = self.parse(data, **kwargs) else: self.value = value def data_size(self): raise NotImplementedError def __repr__(self): name = "%s(%r" % (type(self).__name__, self.value) if self.language: name += ", language=%d" % self.language if self.stream: name += ", stream=%d" % self.stream name += ")" return name def render(self, name): name = name.encode("utf-16-le") + "\x00\x00" data = self._render() return (struct.pack(" 0: texts.append(data[pos:end].decode("utf-16-le").strip("\x00")) else: texts.append(None) pos = end title, author, copyright, desc, rating = texts for key, value in dict( Title=title, Author=author, Copyright=copyright, Description=desc, Rating=rating ).items(): if value is not None: asf.tags[key] = value def render(self, asf): def render_text(name): value = asf.tags.get(name, []) if value: return value[0].encode("utf-16-le") + "\x00\x00" else: return "" texts = map(render_text, _standard_attribute_names) data = struct.pack(" 0xFFFF or value.TYPE == GUID) if (value.language is None and value.stream is None and name not in self.to_extended_content_description and not library_only): self.to_extended_content_description[name] = value elif (value.language is None and value.stream is not None and name not in self.to_metadata and not library_only): self.to_metadata[name] = value else: self.to_metadata_library.append((name, value)) # Add missing objects if not self.content_description_obj: self.content_description_obj = \ ContentDescriptionObject() self.objects.append(self.content_description_obj) if not self.extended_content_description_obj: self.extended_content_description_obj = \ ExtendedContentDescriptionObject() self.objects.append(self.extended_content_description_obj) if not self.header_extension_obj: self.header_extension_obj = \ HeaderExtensionObject() self.objects.append(self.header_extension_obj) if not self.metadata_obj: self.metadata_obj = \ MetadataObject() self.header_extension_obj.objects.append(self.metadata_obj) if not self.metadata_library_obj: self.metadata_library_obj = \ MetadataLibraryObject() self.header_extension_obj.objects.append(self.metadata_library_obj) # Render the header data = "".join([obj.render(self) for obj in self.objects]) data = (HeaderObject.GUID + struct.pack(" self.size: insert_bytes(fileobj, size - self.size, self.size) if size < self.size: delete_bytes(fileobj, self.size - size, 0) fileobj.seek(0) fileobj.write(data) finally: fileobj.close() self.size = size self.num_objects = len(self.objects) def __read_file(self, fileobj): header = fileobj.read(30) if len(header) != 30 or header[:16] != HeaderObject.GUID: raise ASFHeaderError("Not an ASF file.") self.extended_content_description_obj = None self.content_description_obj = None self.header_extension_obj = None self.metadata_obj = None self.metadata_library_obj = None self.size, self.num_objects = struct.unpack("= ' ' and max(key) <= '~' and key not in ["OggS", "TAG", "ID3", "MP+"]) # There are three different kinds of APE tag values. # "0: Item contains text information coded in UTF-8 # 1: Item contains binary information # 2: Item is a locator of external stored information [e.g. URL] # 3: reserved" TEXT, BINARY, EXTERNAL = range(3) HAS_HEADER = 1L << 31 HAS_NO_FOOTER = 1L << 30 IS_HEADER = 1L << 29 class error(IOError): pass class APENoHeaderError(error, ValueError): pass class APEUnsupportedVersionError(error, ValueError): pass class APEBadItemError(error, ValueError): pass class _APEv2Data(object): # Store offsets of the important parts of the file. start = header = data = footer = end = None # Footer or header; seek here and read 32 to get version/size/items/flags metadata = None # Actual tag data tag = None version = None size = None items = None flags = 0 # The tag is at the start rather than the end. A tag at both # the start and end of the file (i.e. the tag is the whole file) # is not considered to be at the start. is_at_start = False def __init__(self, fileobj): self.__find_metadata(fileobj) self.metadata = max(self.header, self.footer) if self.metadata is None: return self.__fill_missing(fileobj) self.__fix_brokenness(fileobj) if self.data is not None: fileobj.seek(self.data) self.tag = fileobj.read(self.size) def __find_metadata(self, fileobj): # Try to find a header or footer. # Check for a simple footer. try: fileobj.seek(-32, 2) except IOError: fileobj.seek(0, 2) return if fileobj.read(8) == "APETAGEX": fileobj.seek(-8, 1) self.footer = self.metadata = fileobj.tell() return # Check for an APEv2 tag followed by an ID3v1 tag at the end. try: fileobj.seek(-128, 2) if fileobj.read(3) == "TAG": fileobj.seek(-35, 1) # "TAG" + header length if fileobj.read(8) == "APETAGEX": fileobj.seek(-8, 1) self.footer = fileobj.tell() return # ID3v1 tag at the end, maybe preceded by Lyrics3v2. # (http://www.id3.org/lyrics3200.html) # (header length - "APETAGEX") - "LYRICS200" fileobj.seek(15, 1) if fileobj.read(9) == 'LYRICS200': fileobj.seek(-15, 1) # "LYRICS200" + size tag try: offset = int(fileobj.read(6)) except ValueError: raise IOError fileobj.seek(-32 - offset - 6, 1) if fileobj.read(8) == "APETAGEX": fileobj.seek(-8, 1) self.footer = fileobj.tell() return except IOError: pass # Check for a tag at the start. fileobj.seek(0, 0) if fileobj.read(8) == "APETAGEX": self.is_at_start = True self.header = 0 def __fill_missing(self, fileobj): fileobj.seek(self.metadata + 8) self.version = fileobj.read(4) self.size = cdata.uint_le(fileobj.read(4)) self.items = cdata.uint_le(fileobj.read(4)) self.flags = cdata.uint_le(fileobj.read(4)) if self.header is not None: self.data = self.header + 32 # If we're reading the header, the size is the header # offset + the size, which includes the footer. self.end = self.data + self.size fileobj.seek(self.end - 32, 0) if fileobj.read(8) == "APETAGEX": self.footer = self.end - 32 elif self.footer is not None: self.end = self.footer + 32 self.data = self.end - self.size if self.flags & HAS_HEADER: self.header = self.data - 32 else: self.header = self.data else: raise APENoHeaderError("No APE tag found") # exclude the footer from size if self.footer is not None: self.size -= 32 def __fix_brokenness(self, fileobj): # Fix broken tags written with PyMusepack. if self.header is not None: start = self.header else: start = self.data fileobj.seek(start) while start > 0: # Clean up broken writing from pre-Mutagen PyMusepack. # It didn't remove the first 24 bytes of header. try: fileobj.seek(-24, 1) except IOError: break else: if fileobj.read(8) == "APETAGEX": fileobj.seek(-8, 1) start = fileobj.tell() else: break self.start = start class APEv2(DictMixin, Metadata): """A file with an APEv2 tag. ID3v1 tags are silently ignored and overwritten. """ filename = None def __init__(self, *args, **kwargs): self.__casemap = {} self.__dict = {} super(APEv2, self).__init__(*args, **kwargs) # Internally all names are stored as lowercase, but the case # they were set with is remembered and used when saving. This # is roughly in line with the standard, which says that keys # are case-sensitive but two keys differing only in case are # not allowed, and recommends case-insensitive # implementations. def pprint(self): """Return tag key=value pairs in a human-readable format.""" items = self.items() items.sort() return "\n".join(["%s=%s" % (k, v.pprint()) for k, v in items]) def load(self, filename): """Load tags from a filename.""" self.filename = filename fileobj = open(filename, "rb") try: data = _APEv2Data(fileobj) finally: fileobj.close() if data.tag: self.clear() self.__casemap.clear() self.__parse_tag(data.tag, data.items) else: raise APENoHeaderError("No APE tag found") def __parse_tag(self, tag, count): fileobj = StringIO(tag) for i in range(count): size_data = fileobj.read(4) # someone writes wrong item counts if not size_data: break size = cdata.uint_le(size_data) flags = cdata.uint_le(fileobj.read(4)) # Bits 1 and 2 bits are flags, 0-3 # Bit 0 is read/write flag, ignored kind = (flags & 6) >> 1 if kind == 3: raise APEBadItemError("value type must be 0, 1, or 2") key = value = fileobj.read(1) while key[-1:] != '\x00' and value: value = fileobj.read(1) key += value if key[-1:] == "\x00": key = key[:-1] value = fileobj.read(size) self[key] = APEValue(value, kind) def __getitem__(self, key): if not is_valid_apev2_key(key): raise KeyError("%r is not a valid APEv2 key" % key) key = key.encode('ascii') return self.__dict[key.lower()] def __delitem__(self, key): if not is_valid_apev2_key(key): raise KeyError("%r is not a valid APEv2 key" % key) key = key.encode('ascii') del(self.__dict[key.lower()]) def __setitem__(self, key, value): """'Magic' value setter. This function tries to guess at what kind of value you want to store. If you pass in a valid UTF-8 or Unicode string, it treats it as a text value. If you pass in a list, it treats it as a list of string/Unicode values. If you pass in a string that is not valid UTF-8, it assumes it is a binary value. If you need to force a specific type of value (e.g. binary data that also happens to be valid UTF-8, or an external reference), use the APEValue factory and set the value to the result of that:: from mutagen.apev2 import APEValue, EXTERNAL tag['Website'] = APEValue('http://example.org', EXTERNAL) """ if not is_valid_apev2_key(key): raise KeyError("%r is not a valid APEv2 key" % key) key = key.encode('ascii') if not isinstance(value, _APEValue): # let's guess at the content if we're not already a value... if isinstance(value, unicode): # unicode? we've got to be text. value = APEValue(utf8(value), TEXT) elif isinstance(value, list): # list? text. value = APEValue("\0".join(map(utf8, value)), TEXT) else: try: value.decode("utf-8") except UnicodeError: # invalid UTF8 text, probably binary value = APEValue(value, BINARY) else: # valid UTF8, probably text value = APEValue(value, TEXT) self.__casemap[key.lower()] = key self.__dict[key.lower()] = value def keys(self): return [self.__casemap.get(key, key) for key in self.__dict.keys()] def save(self, filename=None): """Save changes to a file. If no filename is given, the one most recently loaded is used. Tags are always written at the end of the file, and include a header and a footer. """ filename = filename or self.filename try: fileobj = open(filename, "r+b") except IOError: fileobj = open(filename, "w+b") data = _APEv2Data(fileobj) if data.is_at_start: delete_bytes(fileobj, data.end - data.start, data.start) elif data.start is not None: fileobj.seek(data.start) # Delete an ID3v1 tag if present, too. fileobj.truncate() fileobj.seek(0, 2) # "APE tags items should be sorted ascending by size... This is # not a MUST, but STRONGLY recommended. Actually the items should # be sorted by importance/byte, but this is not feasible." tags = [v._internal(k) for k, v in self.items()] tags.sort(lambda a, b: cmp(len(a), len(b))) num_tags = len(tags) tags = "".join(tags) header = "APETAGEX%s%s" % ( # version, tag size, item count, flags struct.pack("<4I", 2000, len(tags) + 32, num_tags, HAS_HEADER | IS_HEADER), "\0" * 8) fileobj.write(header) fileobj.write(tags) footer = "APETAGEX%s%s" % ( # version, tag size, item count, flags struct.pack("<4I", 2000, len(tags) + 32, num_tags, HAS_HEADER), "\0" * 8) fileobj.write(footer) fileobj.close() def delete(self, filename=None): """Remove tags from a file.""" filename = filename or self.filename fileobj = open(filename, "r+b") try: data = _APEv2Data(fileobj) if data.start is not None and data.size is not None: delete_bytes(fileobj, data.end - data.start, data.start) finally: fileobj.close() self.clear() Open = APEv2 def delete(filename): """Remove tags from a file.""" try: APEv2(filename).delete() except APENoHeaderError: pass def APEValue(value, kind): """APEv2 tag value factory. Use this if you need to specify the value's type manually. Binary and text data are automatically detected by APEv2.__setitem__. """ if kind == TEXT: return APETextValue(value, kind) elif kind == BINARY: return APEBinaryValue(value, kind) elif kind == EXTERNAL: return APEExtValue(value, kind) else: raise ValueError("kind must be TEXT, BINARY, or EXTERNAL") class _APEValue(object): def __init__(self, value, kind): self.kind = kind self.value = value def __len__(self): return len(self.value) def __str__(self): return self.value # Packed format for an item: # 4B: Value length # 4B: Value type # Key name # 1B: Null # Key value def _internal(self, key): return "%s%s\0%s" % ( struct.pack("<2I", len(self.value), self.kind << 1), key, self.value) def __repr__(self): return "%s(%r, %d)" % (type(self).__name__, self.value, self.kind) class APETextValue(_APEValue): """An APEv2 text value. Text values are Unicode/UTF-8 strings. They can be accessed like strings (with a null seperating the values), or arrays of strings.""" def __unicode__(self): return unicode(str(self), "utf-8") def __iter__(self): """Iterate over the strings of the value (not the characters)""" return iter(unicode(self).split("\0")) def __getitem__(self, index): return unicode(self).split("\0")[index] def __len__(self): return self.value.count("\0") + 1 def __cmp__(self, other): return cmp(unicode(self), other) __hash__ = _APEValue.__hash__ def __setitem__(self, index, value): values = list(self) values[index] = value.encode("utf-8") self.value = "\0".join(values).encode("utf-8") def pprint(self): return " / ".join(self) class APEBinaryValue(_APEValue): """An APEv2 binary value.""" def pprint(self): return "[%d bytes]" % len(self) class APEExtValue(_APEValue): """An APEv2 external value. External values are usually URI or IRI strings. """ def pprint(self): return "[External] %s" % unicode(self) class APEv2File(FileType): class _Info(object): length = 0 bitrate = 0 def __init__(self, fileobj): pass @staticmethod def pprint(): return "Unknown format with APEv2 tag." def load(self, filename): self.filename = filename self.info = self._Info(open(filename, "rb")) try: self.tags = APEv2(filename) except error: self.tags = None def add_tags(self): if self.tags is None: self.tags = APEv2() else: raise ValueError("%r already has tags: %r" % (self, self.tags)) @staticmethod def score(filename, fileobj, header): try: fileobj.seek(-160, 2) except IOError: fileobj.seek(0) footer = fileobj.read() filename = filename.lower() return (("APETAGEX" in footer) - header.startswith("ID3")) mutagen-1.22/mutagen/_vorbis.py0000644000175000017500000002004712211424154017000 0ustar lazkalazka00000000000000# Vorbis comment support for Mutagen # Copyright 2005-2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. """Read and write Vorbis comment data. Vorbis comments are freeform key/value pairs; keys are case-insensitive ASCII and values are Unicode strings. A key may have multiple values. The specification is at http://www.xiph.org/vorbis/doc/v-comment.html. """ import sys from cStringIO import StringIO import mutagen from mutagen._util import DictMixin, cdata def is_valid_key(key): """Return true if a string is a valid Vorbis comment key. Valid Vorbis comment keys are printable ASCII between 0x20 (space) and 0x7D ('}'), excluding '='. """ for c in key: if c < " " or c > "}" or c == "=": return False else: return bool(key) istag = is_valid_key class error(IOError): pass class VorbisUnsetFrameError(error): pass class VorbisEncodingError(error): pass class VComment(mutagen.Metadata, list): """A Vorbis comment parser, accessor, and renderer. All comment ordering is preserved. A VComment is a list of key/value pairs, and so any Python list method can be used on it. Vorbis comments are always wrapped in something like an Ogg Vorbis bitstream or a FLAC metadata block, so this loads string data or a file-like object, not a filename. Attributes: vendor -- the stream 'vendor' (i.e. writer); default 'Mutagen' """ vendor = u"Mutagen " + mutagen.version_string def __init__(self, data=None, *args, **kwargs): # Collect the args to pass to load, this lets child classes # override just load and get equivalent magic for the # constructor. if data is not None: if isinstance(data, str): data = StringIO(data) elif not hasattr(data, 'read'): raise TypeError("VComment requires string data or a file-like") self.load(data, *args, **kwargs) def load(self, fileobj, errors='replace', framing=True): """Parse a Vorbis comment from a file-like object. Keyword arguments: errors: 'strict', 'replace', or 'ignore'. This affects Unicode decoding and how other malformed content is interpreted. framing -- if true, fail if a framing bit is not present Framing bits are required by the Vorbis comment specification, but are not used in FLAC Vorbis comment blocks. """ try: vendor_length = cdata.uint_le(fileobj.read(4)) self.vendor = fileobj.read(vendor_length).decode('utf-8', errors) count = cdata.uint_le(fileobj.read(4)) for i in xrange(count): length = cdata.uint_le(fileobj.read(4)) try: string = fileobj.read(length).decode('utf-8', errors) except (OverflowError, MemoryError): raise error("cannot read %d bytes, too large" % length) try: tag, value = string.split('=', 1) except ValueError, err: if errors == "ignore": continue elif errors == "replace": tag, value = u"unknown%d" % i, string else: raise VorbisEncodingError, err, sys.exc_info()[2] try: tag = tag.encode('ascii', errors) except UnicodeEncodeError: raise VorbisEncodingError("invalid tag name %r" % tag) else: if is_valid_key(tag): self.append((tag, value)) if framing and not ord(fileobj.read(1)) & 0x01: raise VorbisUnsetFrameError("framing bit was unset") except (cdata.error, TypeError): raise error("file is not a valid Vorbis comment") def validate(self): """Validate keys and values. Check to make sure every key used is a valid Vorbis key, and that every value used is a valid Unicode or UTF-8 string. If any invalid keys or values are found, a ValueError is raised. """ if not isinstance(self.vendor, unicode): try: self.vendor.decode('utf-8') except UnicodeDecodeError: raise ValueError for key, value in self: try: if not is_valid_key(key): raise ValueError except: raise ValueError("%r is not a valid key" % key) if not isinstance(value, unicode): try: value.encode("utf-8") except: raise ValueError("%r is not a valid value" % value) else: return True def clear(self): """Clear all keys from the comment.""" del(self[:]) def write(self, framing=True): """Return a string representation of the data. Validation is always performed, so calling this function on invalid data may raise a ValueError. Keyword arguments: framing -- if true, append a framing bit (see load) """ self.validate() f = StringIO() f.write(cdata.to_uint_le(len(self.vendor.encode('utf-8')))) f.write(self.vendor.encode('utf-8')) f.write(cdata.to_uint_le(len(self))) for tag, value in self: comment = "%s=%s" % (tag, value.encode('utf-8')) f.write(cdata.to_uint_le(len(comment))) f.write(comment) if framing: f.write("\x01") return f.getvalue() def pprint(self): return "\n".join(["%s=%s" % (k.lower(), v) for k, v in self]) class VCommentDict(VComment, DictMixin): """A VComment that looks like a dictionary. This object differs from a dictionary in two ways. First, len(comment) will still return the number of values, not the number of keys. Secondly, iterating through the object will iterate over (key, value) pairs, not keys. Since a key may have multiple values, the same value may appear multiple times while iterating. Since Vorbis comment keys are case-insensitive, all keys are normalized to lowercase ASCII. """ def __getitem__(self, key): """A list of values for the key. This is a copy, so comment['title'].append('a title') will not work. """ key = key.lower().encode('ascii') values = [value for (k, value) in self if k.lower() == key] if not values: raise KeyError(key) else: return values def __delitem__(self, key): """Delete all values associated with the key.""" key = key.lower().encode('ascii') to_delete = filter(lambda x: x[0].lower() == key, self) if not to_delete: raise KeyError(key) else: map(self.remove, to_delete) def __contains__(self, key): """Return true if the key has any values.""" key = key.lower().encode('ascii') for k, value in self: if k.lower() == key: return True else: return False def __setitem__(self, key, values): """Set a key's value or values. Setting a value overwrites all old ones. The value may be a list of Unicode or UTF-8 strings, or a single Unicode or UTF-8 string. """ key = key.encode('ascii') if not isinstance(values, list): values = [values] try: del(self[key]) except KeyError: pass for value in values: self.append((key, value)) def keys(self): """Return all keys in the comment.""" return self and list(set([k.lower() for k, v in self])) def as_dict(self): """Return a copy of the comment data in a real dict.""" return dict([(key, self[key]) for key in self.keys()]) mutagen-1.22/mutagen/optimfrog.py0000644000175000017500000000423412177421270017352 0ustar lazkalazka00000000000000# OptimFROG reader/tagger # # Copyright 2006 Lukas Lalinsky # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. """OptimFROG audio streams with APEv2 tags. OptimFROG is a lossless audio compression program. Its main goal is to reduce at maximum the size of audio files, while permitting bit identical restoration for all input. It is similar with the ZIP compression, but it is highly specialized to compress audio data. Only versions 4.5 and higher are supported. For more information, see http://www.losslessaudio.org/ """ __all__ = ["OptimFROG", "Open", "delete"] import struct from mutagen.apev2 import APEv2File, error, delete class OptimFROGHeaderError(error): pass class OptimFROGInfo(object): """OptimFROG stream information. Attributes: * channels - number of audio channels * length - file length in seconds, as a float * sample_rate - audio sampling rate in Hz """ def __init__(self, fileobj): header = fileobj.read(76) if (len(header) != 76 or not header.startswith("OFR ") or struct.unpack("` instead of :class:`MP3 `. """ if options is None: from mutagen.asf import ASF from mutagen.apev2 import APEv2File from mutagen.flac import FLAC if easy: from mutagen.easyid3 import EasyID3FileType as ID3FileType else: from mutagen.id3 import ID3FileType if easy: from mutagen.mp3 import EasyMP3 as MP3 else: from mutagen.mp3 import MP3 from mutagen.oggflac import OggFLAC from mutagen.oggspeex import OggSpeex from mutagen.oggtheora import OggTheora from mutagen.oggvorbis import OggVorbis from mutagen.oggopus import OggOpus if easy: from mutagen.trueaudio import EasyTrueAudio as TrueAudio else: from mutagen.trueaudio import TrueAudio from mutagen.wavpack import WavPack if easy: from mutagen.easymp4 import EasyMP4 as MP4 else: from mutagen.mp4 import MP4 from mutagen.musepack import Musepack from mutagen.monkeysaudio import MonkeysAudio from mutagen.optimfrog import OptimFROG options = [MP3, TrueAudio, OggTheora, OggSpeex, OggVorbis, OggFLAC, FLAC, APEv2File, MP4, ID3FileType, WavPack, Musepack, MonkeysAudio, OptimFROG, ASF, OggOpus] if not options: return None fileobj = open(filename, "rb") try: header = fileobj.read(128) # Sort by name after score. Otherwise import order affects # Kind sort order, which affects treatment of things with # equals scores. results = [(Kind.score(filename, fileobj, header), Kind.__name__) for Kind in options] finally: fileobj.close() results = zip(results, options) results.sort() (score, name), Kind = results[-1] if score > 0: return Kind(filename) else: return None mutagen-1.22/mutagen/id3.py0000644000175000017500000007560312212323761016027 0ustar lazkalazka00000000000000# id3 support for mutagen # Copyright (C) 2005 Michael Urman # 2006 Lukas Lalinsky # 2013 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. """ID3v2 reading and writing. This is based off of the following references: * http://id3.org/id3v2.4.0-structure * http://id3.org/id3v2.4.0-frames * http://id3.org/id3v2.3.0 * http://id3.org/id3v2-00 * http://id3.org/ID3v1 Its largest deviation from the above (versions 2.3 and 2.2) is that it will not interpret the / characters as a separator, and will almost always accept null separators to generate multi-valued text frames. Because ID3 frame structure differs between frame types, each frame is implemented as a different class (e.g. TIT2 as mutagen.id3.TIT2). Each frame's documentation contains a list of its attributes. Since this file's documentation is a little unwieldy, you are probably interested in the :class:`ID3` class to start with. """ __all__ = ['ID3', 'ID3FileType', 'Frames', 'Open', 'delete'] import struct from struct import unpack, pack, error as StructError import mutagen from mutagen._util import insert_bytes, delete_bytes, DictProxy from mutagen._id3util import * from mutagen._id3frames import * from mutagen._id3specs import * class ID3(DictProxy, mutagen.Metadata): """A file with an ID3v2 tag. Attributes: * version -- ID3 tag version as a tuple * unknown_frames -- raw frame data of any unknown frames found * size -- the total size of the ID3 tag, including the header """ PEDANTIC = True version = (2, 4, 0) filename = None size = 0 __flags = 0 __readbytes = 0 __crc = None __unknown_version = None _V24 = (2, 4, 0) _V23 = (2, 3, 0) _V22 = (2, 2, 0) _V11 = (1, 1) def __init__(self, *args, **kwargs): self.unknown_frames = [] super(ID3, self).__init__(*args, **kwargs) def __fullread(self, size): try: if size < 0: raise ValueError('Requested bytes (%s) less than zero' % size) if size > self.__filesize: raise EOFError('Requested %#x of %#x (%s)' % ( long(size), long(self.__filesize), self.filename)) except AttributeError: pass data = self.__fileobj.read(size) if len(data) != size: raise EOFError self.__readbytes += size return data def load(self, filename, known_frames=None, translate=True, v2_version=4): """Load tags from a filename. Keyword arguments: * filename -- filename to load tag data from * known_frames -- dict mapping frame IDs to Frame objects * translate -- Update all tags to ID3v2.3/4 internally. If you intend to save, this must be true or you have to call update_to_v23() / update_to_v24() manually. * v2_version -- if update_to_v23 or update_to_v24 get called (3 or 4) Example of loading a custom frame:: my_frames = dict(mutagen.id3.Frames) class XMYF(Frame): ... my_frames["XMYF"] = XMYF mutagen.id3.ID3(filename, known_frames=my_frames) """ if not v2_version in (3, 4): raise ValueError("Only 3 and 4 possible for v2_version") from os.path import getsize self.filename = filename self.__known_frames = known_frames self.__fileobj = open(filename, 'rb') self.__filesize = getsize(filename) try: try: self.__load_header() except EOFError: self.size = 0 raise ID3NoHeaderError("%s: too small (%d bytes)" % ( filename, self.__filesize)) except (ID3NoHeaderError, ID3UnsupportedVersionError), err: self.size = 0 import sys stack = sys.exc_info()[2] try: self.__fileobj.seek(-128, 2) except EnvironmentError: raise err, None, stack else: frames = ParseID3v1(self.__fileobj.read(128)) if frames is not None: self.version = self._V11 map(self.add, frames.values()) else: raise err, None, stack else: frames = self.__known_frames if frames is None: if self._V23 <= self.version: frames = Frames elif self._V22 <= self.version: frames = Frames_2_2 data = self.__fullread(self.size - 10) for frame in self.__read_frames(data, frames=frames): if isinstance(frame, Frame): self.add(frame) else: self.unknown_frames.append(frame) self.__unknown_version = self.version finally: self.__fileobj.close() del self.__fileobj del self.__filesize if translate: if v2_version == 3: self.update_to_v23() else: self.update_to_v24() def getall(self, key): """Return all frames with a given name (the list may be empty). This is best explained by examples:: id3.getall('TIT2') == [id3['TIT2']] id3.getall('TTTT') == [] id3.getall('TXXX') == [TXXX(desc='woo', text='bar'), TXXX(desc='baz', text='quuuux'), ...] Since this is based on the frame's HashKey, which is colon-separated, you can use it to do things like ``getall('COMM:MusicMatch')`` or ``getall('TXXX:QuodLibet:')``. """ if key in self: return [self[key]] else: key = key + ":" return [v for s, v in self.items() if s.startswith(key)] def delall(self, key): """Delete all tags of a given kind; see getall.""" if key in self: del(self[key]) else: key = key + ":" for k in filter(lambda s: s.startswith(key), self.keys()): del(self[k]) def setall(self, key, values): """Delete frames of the given type and add frames in 'values'.""" self.delall(key) for tag in values: self[tag.HashKey] = tag def pprint(self): """Return tags in a human-readable format. "Human-readable" is used loosely here. The format is intended to mirror that used for Vorbis or APEv2 output, e.g. ``TIT2=My Title`` However, ID3 frames can have multiple keys: ``POPM=user@example.org=3 128/255`` """ frames = list(map(Frame.pprint, self.values())) frames.sort() return "\n".join(frames) def loaded_frame(self, tag): """Deprecated; use the add method.""" # turn 2.2 into 2.3/2.4 tags if len(type(tag).__name__) == 3: tag = type(tag).__base__(tag) self[tag.HashKey] = tag # add = loaded_frame (and vice versa) break applications that # expect to be able to override loaded_frame (e.g. Quod Libet), # as does making loaded_frame call add. def add(self, frame): """Add a frame to the tag.""" return self.loaded_frame(frame) def __load_header(self): fn = self.filename data = self.__fullread(10) id3, vmaj, vrev, flags, size = unpack('>3sBBB4s', data) self.__flags = flags self.size = BitPaddedInt(size) + 10 self.version = (2, vmaj, vrev) if id3 != 'ID3': raise ID3NoHeaderError("'%s' doesn't start with an ID3 tag" % fn) if vmaj not in [2, 3, 4]: raise ID3UnsupportedVersionError("'%s' ID3v2.%d not supported" % (fn, vmaj)) if self.PEDANTIC: if not BitPaddedInt.has_valid_padding(size): raise ValueError("Header size not synchsafe") if self._V24 <= self.version and (flags & 0x0f): raise ValueError("'%s' has invalid flags %#02x" % (fn, flags)) elif self._V23 <= self.version < self._V24 and (flags & 0x1f): raise ValueError("'%s' has invalid flags %#02x" % (fn, flags)) if self.f_extended: extsize = self.__fullread(4) if extsize in Frames: # Some tagger sets the extended header flag but # doesn't write an extended header; in this case, the # ID3 data follows immediately. Since no extended # header is going to be long enough to actually match # a frame, and if it's *not* a frame we're going to be # completely lost anyway, this seems to be the most # correct check. # http://code.google.com/p/quodlibet/issues/detail?id=126 self.__flags ^= 0x40 self.__extsize = 0 self.__fileobj.seek(-4, 1) self.__readbytes -= 4 elif self.version >= self._V24: # "Where the 'Extended header size' is the size of the whole # extended header, stored as a 32 bit synchsafe integer." self.__extsize = BitPaddedInt(extsize) - 4 if self.PEDANTIC: if not BitPaddedInt.has_valid_padding(extsize): raise ValueError("Extended header size not synchsafe") else: # "Where the 'Extended header size', currently 6 or 10 bytes, # excludes itself." self.__extsize = unpack('>L', extsize)[0] if self.__extsize: self.__extdata = self.__fullread(self.__extsize) else: self.__extdata = "" def __determine_bpi(self, data, frames, EMPTY="\x00" * 10): if self.version < self._V24: return int # have to special case whether to use bitpaddedints here # spec says to use them, but iTunes has it wrong # count number of tags found as BitPaddedInt and how far past o = 0 asbpi = 0 while o < len(data) - 10: part = data[o:o + 10] if part == EMPTY: bpioff = -((len(data) - o) % 10) break name, size, flags = unpack('>4sLH', part) size = BitPaddedInt(size) o += 10 + size if name in frames: asbpi += 1 else: bpioff = o - len(data) # count number of tags found as int and how far past o = 0 asint = 0 while o < len(data) - 10: part = data[o:o + 10] if part == EMPTY: intoff = -((len(data) - o) % 10) break name, size, flags = unpack('>4sLH', part) o += 10 + size if name in frames: asint += 1 else: intoff = o - len(data) # if more tags as int, or equal and bpi is past and int is not if asint > asbpi or (asint == asbpi and (bpioff >= 1 and intoff <= 1)): return int return BitPaddedInt def __read_frames(self, data, frames): if self.version < self._V24 and self.f_unsynch: try: data = unsynch.decode(data) except ValueError: pass if self._V23 <= self.version: bpi = self.__determine_bpi(data, frames) while data: header = data[:10] try: name, size, flags = unpack('>4sLH', header) except struct.error: return # not enough header if name.strip('\x00') == '': return size = bpi(size) framedata = data[10:10+size] data = data[10+size:] if size == 0: continue # drop empty frames try: tag = frames[name] except KeyError: if is_valid_frame_id(name): yield header + framedata else: try: yield self.__load_framedata(tag, flags, framedata) except NotImplementedError: yield header + framedata except ID3JunkFrameError: pass elif self._V22 <= self.version: while data: header = data[0:6] try: name, size = unpack('>3s3s', header) except struct.error: return # not enough header size, = struct.unpack('>L', '\x00'+size) if name.strip('\x00') == '': return framedata = data[6:6+size] data = data[6+size:] if size == 0: continue # drop empty frames try: tag = frames[name] except KeyError: if is_valid_frame_id(name): yield header + framedata else: try: yield self.__load_framedata(tag, 0, framedata) except NotImplementedError: yield header + framedata except ID3JunkFrameError: pass def __load_framedata(self, tag, flags, framedata): return tag.fromData(self, flags, framedata) f_unsynch = property(lambda s: bool(s.__flags & 0x80)) f_extended = property(lambda s: bool(s.__flags & 0x40)) f_experimental = property(lambda s: bool(s.__flags & 0x20)) f_footer = property(lambda s: bool(s.__flags & 0x10)) #f_crc = property(lambda s: bool(s.__extflags & 0x8000)) def save(self, filename=None, v1=1, v2_version=4, v23_sep='/'): """Save changes to a file. If no filename is given, the one most recently loaded is used. Keyword arguments: v1 -- if 0, ID3v1 tags will be removed if 1, ID3v1 tags will be updated but not added if 2, ID3v1 tags will be created and/or updated v2 -- version of ID3v2 tags (3 or 4). By default Mutagen saves ID3v2.4 tags. If you want to save ID3v2.3 tags, you must call method update_to_v23 before saving the file. v23_sep -- the separator used to join multiple text values if v2_version == 3. Defaults to '/' but if it's None will be the ID3v2v2.4 null separator. The lack of a way to update only an ID3v1 tag is intentional. """ if v2_version == 3: version = self._V23 elif v2_version == 4: version = self._V24 else: raise ValueError("Only 3 or 4 allowed for v2_version") # Sort frames by 'importance' order = ["TIT2", "TPE1", "TRCK", "TALB", "TPOS", "TDRC", "TCON"] order = dict(zip(order, range(len(order)))) last = len(order) frames = self.items() frames.sort(lambda a, b: cmp(order.get(a[0][:4], last), order.get(b[0][:4], last))) framedata = [self.__save_frame(frame, version=version, v23_sep=v23_sep) for (key, frame) in frames] # only write unknown frames if they were loaded from the version # we are saving with or upgraded to it if self.__unknown_version == version: framedata.extend([data for data in self.unknown_frames if len(data) > 10]) if not framedata: try: self.delete(filename) except EnvironmentError, err: from errno import ENOENT if err.errno != ENOENT: raise return framedata = ''.join(framedata) framesize = len(framedata) if filename is None: filename = self.filename try: f = open(filename, 'rb+') except IOError, err: from errno import ENOENT if err.errno != ENOENT: raise f = open(filename, 'ab') # create, then reopen f = open(filename, 'rb+') try: idata = f.read(10) try: id3, vmaj, vrev, flags, insize = unpack('>3sBBB4s', idata) except struct.error: id3, insize = '', 0 insize = BitPaddedInt(insize) if id3 != 'ID3': insize = -10 if insize >= framesize: outsize = insize else: outsize = (framesize + 1023) & ~0x3FF framedata += '\x00' * (outsize - framesize) framesize = BitPaddedInt.to_str(outsize, width=4) flags = 0 header = pack('>3sBBB4s', 'ID3', v2_version, 0, flags, framesize) data = header + framedata if (insize < outsize): insert_bytes(f, outsize-insize, insize+10) f.seek(0) f.write(data) try: f.seek(-128, 2) except IOError, err: # If the file is too small, that's OK - it just means # we're certain it doesn't have a v1 tag. from errno import EINVAL if err.errno != EINVAL: # If we failed to see for some other reason, bail out. raise # Since we're sure this isn't a v1 tag, don't read it. f.seek(0, 2) data = f.read(128) try: idx = data.index("TAG") except ValueError: offset = 0 has_v1 = False else: offset = idx - len(data) has_v1 = True f.seek(offset, 2) if v1 == 1 and has_v1 or v1 == 2: f.write(MakeID3v1(self)) else: f.truncate() finally: f.close() def delete(self, filename=None, delete_v1=True, delete_v2=True): """Remove tags from a file. If no filename is given, the one most recently loaded is used. Keyword arguments: * delete_v1 -- delete any ID3v1 tag * delete_v2 -- delete any ID3v2 tag """ if filename is None: filename = self.filename delete(filename, delete_v1, delete_v2) self.clear() def __save_frame(self, frame, name=None, version=_V24, v23_sep=None): flags = 0 if self.PEDANTIC and isinstance(frame, TextFrame): if len(str(frame)) == 0: return '' if version == self._V23: framev23 = frame._get_v23_frame(sep=v23_sep) framedata = framev23._writeData() else: framedata = frame._writeData() usize = len(framedata) if usize > 2048: # Disabled as this causes iTunes and other programs # to fail to find these frames, which usually includes # e.g. APIC. #framedata = BitPaddedInt.to_str(usize) + framedata.encode('zlib') #flags |= Frame.FLAG24_COMPRESS | Frame.FLAG24_DATALEN pass if version == self._V24: bits = 7 elif version == self._V23: bits = 8 else: raise ValueError datasize = BitPaddedInt.to_str(len(framedata), width=4, bits=bits) header = pack('>4s4sH', name or type(frame).__name__, datasize, flags) return header + framedata def __update_common(self): """Updates done by both v23 and v24 update""" if "TCON" in self: # Get rid of "(xx)Foobr" format. self["TCON"].genres = self["TCON"].genres if self.version < self._V23: # ID3v2.2 PIC frames are slightly different. pics = self.getall("APIC") mimes = {"PNG": "image/png", "JPG": "image/jpeg"} self.delall("APIC") for pic in pics: newpic = APIC( encoding=pic.encoding, mime=mimes.get(pic.mime, pic.mime), type=pic.type, desc=pic.desc, data=pic.data) self.add(newpic) # ID3v2.2 LNK frames are just way too different to upgrade. self.delall("LINK") def update_to_v24(self): """Convert older tags into an ID3v2.4 tag. This updates old ID3v2 frames to ID3v2.4 ones (e.g. TYER to TDRC). If you intend to save tags, you must call this function at some point; it is called by default when loading the tag. """ self.__update_common() if self.__unknown_version == (2, 3, 0): # convert unknown 2.3 frames (flags/size) to 2.4 converted = [] for frame in self.unknown_frames: try: name, size, flags = unpack('>4sLH', frame[:10]) frame = BinaryFrame.fromData(self, flags, frame[10:]) except (struct.error, error): continue converted.append(self.__save_frame(frame, name=name)) self.unknown_frames[:] = converted self.__unknown_version = (2, 4, 0) # TDAT, TYER, and TIME have been turned into TDRC. try: if str(self.get("TYER", "")).strip("\x00"): date = str(self.pop("TYER")) if str(self.get("TDAT", "")).strip("\x00"): dat = str(self.pop("TDAT")) date = "%s-%s-%s" % (date, dat[2:], dat[:2]) if str(self.get("TIME", "")).strip("\x00"): time = str(self.pop("TIME")) date += "T%s:%s:00" % (time[:2], time[2:]) if "TDRC" not in self: self.add(TDRC(encoding=0, text=date)) except UnicodeDecodeError: # Old ID3 tags have *lots* of Unicode problems, so if TYER # is bad, just chuck the frames. pass # TORY can be the first part of a TDOR. if "TORY" in self: f = self.pop("TORY") if "TDOR" not in self: try: self.add(TDOR(encoding=0, text=str(f))) except UnicodeDecodeError: pass # IPLS is now TIPL. if "IPLS" in self: f = self.pop("IPLS") if "TIPL" not in self: self.add(TIPL(encoding=f.encoding, people=f.people)) # These can't be trivially translated to any ID3v2.4 tags, or # should have been removed already. for key in ["RVAD", "EQUA", "TRDA", "TSIZ", "TDAT", "TIME", "CRM"]: if key in self: del(self[key]) def update_to_v23(self): """Convert older (and newer) tags into an ID3v2.3 tag. This updates incompatible ID3v2 frames to ID3v2.3 ones. If you intend to save tags as ID3v2.3, you must call this function at some point. If you want to to go off spec and include some v2.4 frames in v2.3, remove them before calling this and add them back afterwards. """ self.__update_common() # we could downgrade unknown v2.4 frames here, but given that # the main reason to save v2.3 is compatibility and this # might increase the chance of some parser breaking.. better not # TMCL, TIPL -> TIPL if "TIPL" in self or "TMCL" in self: people = [] if "TIPL" in self: f = self.pop("TIPL") people.extend(f.people) if "TMCL" in self: f = self.pop("TMCL") people.extend(f.people) if "IPLS" not in self: self.add(IPLS(encoding=f.encoding, people=people)) # TDOR -> TORY if "TDOR" in self: f = self.pop("TDOR") if f.text: d = f.text[0] if d.year and "TORY" not in self: self.add(TORY(encoding=f.encoding, text="%04d" % d.year)) # TDRC -> TYER, TDAT, TIME if "TDRC" in self: f = self.pop("TDRC") if f.text: d = f.text[0] if d.year and "TYER" not in self: self.add(TYER(encoding=f.encoding, text="%04d" % d.year)) if d.month and d.day and "TDAT" not in self: self.add(TDAT(encoding=f.encoding, text="%02d%02d" % (d.day, d.month))) if d.hour and d.minute and "TIME" not in self: self.add(TIME(encoding=f.encoding, text="%02d%02d" % (d.hour, d.minute))) # New frames added in v2.4 v24_frames = [ 'ASPI', 'EQU2', 'RVA2', 'SEEK', 'SIGN', 'TDEN', 'TDOR', 'TDRC', 'TDRL', 'TDTG', 'TIPL', 'TMCL', 'TMOO', 'TPRO', 'TSOA', 'TSOP', 'TSOT', 'TSST', ] for key in v24_frames: if key in self: del(self[key]) def delete(filename, delete_v1=True, delete_v2=True): """Remove tags from a file. Keyword arguments: * delete_v1 -- delete any ID3v1 tag * delete_v2 -- delete any ID3v2 tag """ f = open(filename, 'rb+') if delete_v1: try: f.seek(-128, 2) except IOError: pass else: if f.read(3) == "TAG": f.seek(-128, 2) f.truncate() # technically an insize=0 tag is invalid, but we delete it anyway # (primarily because we used to write it) if delete_v2: f.seek(0, 0) idata = f.read(10) try: id3, vmaj, vrev, flags, insize = unpack('>3sBBB4s', idata) except struct.error: id3, insize = '', -1 insize = BitPaddedInt(insize) if id3 == 'ID3' and insize >= 0: delete_bytes(f, insize + 10, 0) # support open(filename) as interface Open = ID3 # ID3v1.1 support. def ParseID3v1(string): """Parse an ID3v1 tag, returning a list of ID3v2.4 frames.""" try: string = string[string.index("TAG"):] except ValueError: return None if 128 < len(string) or len(string) < 124: return None # Issue #69 - Previous versions of Mutagen, when encountering # out-of-spec TDRC and TYER frames of less than four characters, # wrote only the characters available - e.g. "1" or "" - into the # year field. To parse those, reduce the size of the year field. # Amazingly, "0s" works as a struct format string. unpack_fmt = "3s30s30s30s%ds29sBB" % (len(string) - 124) try: tag, title, artist, album, year, comment, track, genre = unpack( unpack_fmt, string) except StructError: return None if tag != "TAG": return None def fix(string): return string.split("\x00")[0].strip().decode('latin1') title, artist, album, year, comment = map( fix, [title, artist, album, year, comment]) frames = {} if title: frames["TIT2"] = TIT2(encoding=0, text=title) if artist: frames["TPE1"] = TPE1(encoding=0, text=[artist]) if album: frames["TALB"] = TALB(encoding=0, text=album) if year: frames["TDRC"] = TDRC(encoding=0, text=year) if comment: frames["COMM"] = COMM( encoding=0, lang="eng", desc="ID3v1 Comment", text=comment) # Don't read a track number if it looks like the comment was # padded with spaces instead of nulls (thanks, WinAmp). if track and (track != 32 or string[-3] == '\x00'): frames["TRCK"] = TRCK(encoding=0, text=str(track)) if genre != 255: frames["TCON"] = TCON(encoding=0, text=str(genre)) return frames def MakeID3v1(id3): """Return an ID3v1.1 tag string from a dict of ID3v2.4 frames.""" v1 = {} for v2id, name in {"TIT2": "title", "TPE1": "artist", "TALB": "album"}.items(): if v2id in id3: text = id3[v2id].text[0].encode('latin1', 'replace')[:30] else: text = "" v1[name] = text + ("\x00" * (30 - len(text))) if "COMM" in id3: cmnt = id3["COMM"].text[0].encode('latin1', 'replace')[:28] else: cmnt = "" v1["comment"] = cmnt + ("\x00" * (29 - len(cmnt))) if "TRCK" in id3: try: v1["track"] = chr(+id3["TRCK"]) except ValueError: v1["track"] = "\x00" else: v1["track"] = "\x00" if "TCON" in id3: try: genre = id3["TCON"].genres[0] except IndexError: pass else: if genre in TCON.GENRES: v1["genre"] = chr(TCON.GENRES.index(genre)) if "genre" not in v1: v1["genre"] = "\xff" if "TDRC" in id3: year = str(id3["TDRC"]) elif "TYER" in id3: year = str(id3["TYER"]) else: year = "" v1["year"] = (year + "\x00\x00\x00\x00")[:4] return ("TAG%(title)s%(artist)s%(album)s%(year)s%(comment)s" "%(track)s%(genre)s") % v1 class ID3FileType(mutagen.FileType): """An unknown type of file with ID3 tags.""" ID3 = ID3 class _Info(object): length = 0 def __init__(self, fileobj, offset): pass @staticmethod def pprint(): return "Unknown format with ID3 tag" @staticmethod def score(filename, fileobj, header): return header.startswith("ID3") def add_tags(self, ID3=None): """Add an empty ID3 tag to the file. A custom tag reader may be used in instead of the default mutagen.id3.ID3 object, e.g. an EasyID3 reader. """ if ID3 is None: ID3 = self.ID3 if self.tags is None: self.ID3 = ID3 self.tags = ID3() else: raise error("an ID3 tag already exists") def load(self, filename, ID3=None, **kwargs): """Load stream and tag information from a file. A custom tag reader may be used in instead of the default mutagen.id3.ID3 object, e.g. an EasyID3 reader. """ if ID3 is None: ID3 = self.ID3 else: # If this was initialized with EasyID3, remember that for # when tags are auto-instantiated in add_tags. self.ID3 = ID3 self.filename = filename try: self.tags = ID3(filename, **kwargs) except error: self.tags = None if self.tags is not None: try: offset = self.tags.size except AttributeError: offset = None else: offset = None try: fileobj = open(filename, "rb") self.info = self._Info(fileobj, offset) finally: fileobj.close() mutagen-1.22/mutagen/oggflac.py0000644000175000017500000001055012177421270016744 0ustar lazkalazka00000000000000# Ogg FLAC support. # # Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. """Read and write Ogg FLAC comments. This module handles FLAC files wrapped in an Ogg bitstream. The first FLAC stream found is used. For 'naked' FLACs, see mutagen.flac. This module is based off the specification at http://flac.sourceforge.net/ogg_mapping.html. """ __all__ = ["OggFLAC", "Open", "delete"] import struct from cStringIO import StringIO from mutagen.flac import StreamInfo, VCFLACDict, StrictFileObject from mutagen.ogg import OggPage, OggFileType, error as OggError class error(OggError): pass class OggFLACHeaderError(error): pass class OggFLACStreamInfo(StreamInfo): """Ogg FLAC general header and stream info. This encompasses the Ogg wrapper for the FLAC STREAMINFO metadata block, as well as the Ogg codec setup that precedes it. Attributes (in addition to StreamInfo's): * packets -- number of metadata packets * serial -- Ogg logical stream serial number """ packets = 0 serial = 0 def load(self, data): # Ogg expects file objects that don't raise on read if isinstance(data, StrictFileObject): data = data._fileobj page = OggPage(data) while not page.packets[0].startswith("\x7FFLAC"): page = OggPage(data) major, minor, self.packets, flac = struct.unpack( ">BBH4s", page.packets[0][5:13]) if flac != "fLaC": raise OggFLACHeaderError("invalid FLAC marker (%r)" % flac) elif (major, minor) != (1, 0): raise OggFLACHeaderError( "unknown mapping version: %d.%d" % (major, minor)) self.serial = page.serial # Skip over the block header. stringobj = StrictFileObject(StringIO(page.packets[0][17:])) super(OggFLACStreamInfo, self).load(stringobj) def _post_tags(self, fileobj): if self.length: return page = OggPage.find_last(fileobj, self.serial) self.length = page.position / float(self.sample_rate) def pprint(self): return "Ogg " + super(OggFLACStreamInfo, self).pprint() class OggFLACVComment(VCFLACDict): def load(self, data, info, errors='replace'): # data should be pointing at the start of an Ogg page, after # the first FLAC page. pages = [] complete = False while not complete: page = OggPage(data) if page.serial == info.serial: pages.append(page) complete = page.complete or (len(page.packets) > 1) comment = StringIO(OggPage.to_packets(pages)[0][4:]) super(OggFLACVComment, self).load(comment, errors=errors) def _inject(self, fileobj): """Write tag data into the FLAC Vorbis comment packet/page.""" # Ogg FLAC has no convenient data marker like Vorbis, but the # second packet - and second page - must be the comment data. fileobj.seek(0) page = OggPage(fileobj) while not page.packets[0].startswith("\x7FFLAC"): page = OggPage(fileobj) first_page = page while not (page.sequence == 1 and page.serial == first_page.serial): page = OggPage(fileobj) old_pages = [page] while not (old_pages[-1].complete or len(old_pages[-1].packets) > 1): page = OggPage(fileobj) if page.serial == first_page.serial: old_pages.append(page) packets = OggPage.to_packets(old_pages, strict=False) # Set the new comment block. data = self.write() data = packets[0][0] + struct.pack(">I", len(data))[-3:] + data packets[0] = data new_pages = OggPage.from_packets(packets, old_pages[0].sequence) OggPage.replace(fileobj, old_pages, new_pages) class OggFLAC(OggFileType): """An Ogg FLAC file.""" _Info = OggFLACStreamInfo _Tags = OggFLACVComment _Error = OggFLACHeaderError _mimes = ["audio/x-oggflac"] @staticmethod def score(filename, fileobj, header): return (header.startswith("OggS") * ( ("FLAC" in header) + ("fLaC" in header))) Open = OggFLAC def delete(filename): """Remove tags from a file.""" OggFLAC(filename).delete() mutagen-1.22/mutagen/oggtheora.py0000644000175000017500000000740712177421270017330 0ustar lazkalazka00000000000000# Ogg Theora support. # # Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. """Read and write Ogg Theora comments. This module handles Theora files wrapped in an Ogg bitstream. The first Theora stream found is used. Based on the specification at http://theora.org/doc/Theora_I_spec.pdf. """ __all__ = ["OggTheora", "Open", "delete"] import struct from mutagen._vorbis import VCommentDict from mutagen._util import cdata from mutagen.ogg import OggPage, OggFileType, error as OggError class error(OggError): pass class OggTheoraHeaderError(error): pass class OggTheoraInfo(object): """Ogg Theora stream information. Attributes: * length - file length in seconds, as a float * fps - video frames per second, as a float """ length = 0 def __init__(self, fileobj): page = OggPage(fileobj) while not page.packets[0].startswith("\x80theora"): page = OggPage(fileobj) if not page.first: raise OggTheoraHeaderError( "page has ID header, but doesn't start a stream") data = page.packets[0] vmaj, vmin = struct.unpack("2B", data[7:9]) if (vmaj, vmin) != (3, 2): raise OggTheoraHeaderError( "found Theora version %d.%d != 3.2" % (vmaj, vmin)) fps_num, fps_den = struct.unpack(">2I", data[22:30]) self.fps = fps_num / float(fps_den) self.bitrate = cdata.uint_be("\x00" + data[37:40]) self.granule_shift = (cdata.ushort_be(data[40:42]) >> 5) & 0x1F self.serial = page.serial def _post_tags(self, fileobj): page = OggPage.find_last(fileobj, self.serial) position = page.position mask = (1 << self.granule_shift) - 1 frames = (position >> self.granule_shift) + (position & mask) self.length = frames / float(self.fps) def pprint(self): return "Ogg Theora, %.2f seconds, %d bps" % (self.length, self.bitrate) class OggTheoraCommentDict(VCommentDict): """Theora comments embedded in an Ogg bitstream.""" def __init__(self, fileobj, info): pages = [] complete = False while not complete: page = OggPage(fileobj) if page.serial == info.serial: pages.append(page) complete = page.complete or (len(page.packets) > 1) data = OggPage.to_packets(pages)[0][7:] super(OggTheoraCommentDict, self).__init__(data + "\x01") def _inject(self, fileobj): """Write tag data into the Theora comment packet/page.""" fileobj.seek(0) page = OggPage(fileobj) while not page.packets[0].startswith("\x81theora"): page = OggPage(fileobj) old_pages = [page] while not (old_pages[-1].complete or len(old_pages[-1].packets) > 1): page = OggPage(fileobj) if page.serial == old_pages[0].serial: old_pages.append(page) packets = OggPage.to_packets(old_pages, strict=False) packets[0] = "\x81theora" + self.write(framing=False) new_pages = OggPage.from_packets(packets, old_pages[0].sequence) OggPage.replace(fileobj, old_pages, new_pages) class OggTheora(OggFileType): """An Ogg Theora file.""" _Info = OggTheoraInfo _Tags = OggTheoraCommentDict _Error = OggTheoraHeaderError _mimes = ["video/x-theora"] @staticmethod def score(filename, fileobj, header): return (header.startswith("OggS") * (("\x80theora" in header) + ("\x81theora" in header))) Open = OggTheora def delete(filename): """Remove tags from a file.""" OggTheora(filename).delete() mutagen-1.22/mutagen/_constants.py0000644000175000017500000000645012211656510017515 0ustar lazkalazka00000000000000"""Constants used by Mutagen.""" GENRES = [ u"Blues", u"Classic Rock", u"Country", u"Dance", u"Disco", u"Funk", u"Grunge", u"Hip-Hop", u"Jazz", u"Metal", u"New Age", u"Oldies", u"Other", u"Pop", u"R&B", u"Rap", u"Reggae", u"Rock", u"Techno", u"Industrial", u"Alternative", u"Ska", u"Death Metal", u"Pranks", u"Soundtrack", u"Euro-Techno", u"Ambient", u"Trip-Hop", u"Vocal", u"Jazz+Funk", u"Fusion", u"Trance", u"Classical", u"Instrumental", u"Acid", u"House", u"Game", u"Sound Clip", u"Gospel", u"Noise", u"Alt. Rock", u"Bass", u"Soul", u"Punk", u"Space", u"Meditative", u"Instrumental Pop", u"Instrumental Rock", u"Ethnic", u"Gothic", u"Darkwave", u"Techno-Industrial", u"Electronic", u"Pop-Folk", u"Eurodance", u"Dream", u"Southern Rock", u"Comedy", u"Cult", u"Gangsta Rap", u"Top 40", u"Christian Rap", u"Pop/Funk", u"Jungle", u"Native American", u"Cabaret", u"New Wave", u"Psychedelic", u"Rave", u"Showtunes", u"Trailer", u"Lo-Fi", u"Tribal", u"Acid Punk", u"Acid Jazz", u"Polka", u"Retro", u"Musical", u"Rock & Roll", u"Hard Rock", u"Folk", u"Folk-Rock", u"National Folk", u"Swing", u"Fast-Fusion", u"Bebop", u"Latin", u"Revival", u"Celtic", u"Bluegrass", u"Avantgarde", u"Gothic Rock", u"Progressive Rock", u"Psychedelic Rock", u"Symphonic Rock", u"Slow Rock", u"Big Band", u"Chorus", u"Easy Listening", u"Acoustic", u"Humour", u"Speech", u"Chanson", u"Opera", u"Chamber Music", u"Sonata", u"Symphony", u"Booty Bass", u"Primus", u"Porn Groove", u"Satire", u"Slow Jam", u"Club", u"Tango", u"Samba", u"Folklore", u"Ballad", u"Power Ballad", u"Rhythmic Soul", u"Freestyle", u"Duet", u"Punk Rock", u"Drum Solo", u"A Cappella", u"Euro-House", u"Dance Hall", u"Goa", u"Drum & Bass", u"Club-House", u"Hardcore", u"Terror", u"Indie", u"BritPop", u"Afro-Punk", u"Polsk Punk", u"Beat", u"Christian Gangsta Rap", u"Heavy Metal", u"Black Metal", u"Crossover", u"Contemporary Christian", u"Christian Rock", u"Merengue", u"Salsa", u"Thrash Metal", u"Anime", u"JPop", u"Synthpop", u"Abstract", u"Art Rock", u"Baroque", u"Bhangra", u"Big Beat", u"Breakbeat", u"Chillout", u"Downtempo", u"Dub", u"EBM", u"Eclectic", u"Electro", u"Electroclash", u"Emo", u"Experimental", u"Garage", u"Global", u"IDM", u"Illbient", u"Industro-Goth", u"Jam Band", u"Krautrock", u"Leftfield", u"Lounge", u"Math Rock", u"New Romantic", u"Nu-Breakz", u"Post-Punk", u"Post-Rock", u"Psytrance", u"Shoegaze", u"Space Rock", u"Trop Rock", u"World Music", u"Neoclassical", u"Audiobook", u"Audio Theatre", u"Neue Deutsche Welle", u"Podcast", u"Indie Rock", u"G-Funk", u"Dubstep", u"Garage Rock", u"Psybient", ] """The ID3v1 genre list.""" mutagen-1.22/mutagen/oggvorbis.py0000644000175000017500000001010312177421270017335 0ustar lazkalazka00000000000000# Ogg Vorbis support. # # Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. """Read and write Ogg Vorbis comments. This module handles Vorbis files wrapped in an Ogg bitstream. The first Vorbis stream found is used. Read more about Ogg Vorbis at http://vorbis.com/. This module is based on the specification at http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html. """ __all__ = ["OggVorbis", "Open", "delete"] import struct from mutagen._vorbis import VCommentDict from mutagen.ogg import OggPage, OggFileType, error as OggError class error(OggError): pass class OggVorbisHeaderError(error): pass class OggVorbisInfo(object): """Ogg Vorbis stream information. Attributes: * length - file length in seconds, as a float * bitrate - nominal ('average') bitrate in bits per second, as an int """ length = 0 def __init__(self, fileobj): page = OggPage(fileobj) while not page.packets[0].startswith("\x01vorbis"): page = OggPage(fileobj) if not page.first: raise OggVorbisHeaderError( "page has ID header, but doesn't start a stream") (self.channels, self.sample_rate, max_bitrate, nominal_bitrate, min_bitrate) = struct.unpack(" nominal_bitrate: self.bitrate = min_bitrate else: self.bitrate = nominal_bitrate def _post_tags(self, fileobj): page = OggPage.find_last(fileobj, self.serial) self.length = page.position / float(self.sample_rate) def pprint(self): return "Ogg Vorbis, %.2f seconds, %d bps" % (self.length, self.bitrate) class OggVCommentDict(VCommentDict): """Vorbis comments embedded in an Ogg bitstream.""" def __init__(self, fileobj, info): pages = [] complete = False while not complete: page = OggPage(fileobj) if page.serial == info.serial: pages.append(page) complete = page.complete or (len(page.packets) > 1) data = OggPage.to_packets(pages)[0][7:] # Strip off "\x03vorbis". super(OggVCommentDict, self).__init__(data) def _inject(self, fileobj): """Write tag data into the Vorbis comment packet/page.""" # Find the old pages in the file; we'll need to remove them, # plus grab any stray setup packet data out of them. fileobj.seek(0) page = OggPage(fileobj) while not page.packets[0].startswith("\x03vorbis"): page = OggPage(fileobj) old_pages = [page] while not (old_pages[-1].complete or len(old_pages[-1].packets) > 1): page = OggPage(fileobj) if page.serial == old_pages[0].serial: old_pages.append(page) packets = OggPage.to_packets(old_pages, strict=False) # Set the new comment packet. packets[0] = "\x03vorbis" + self.write() new_pages = OggPage.from_packets(packets, old_pages[0].sequence) OggPage.replace(fileobj, old_pages, new_pages) class OggVorbis(OggFileType): """An Ogg Vorbis file.""" _Info = OggVorbisInfo _Tags = OggVCommentDict _Error = OggVorbisHeaderError _mimes = ["audio/vorbis", "audio/x-vorbis"] @staticmethod def score(filename, fileobj, header): return (header.startswith("OggS") * ("\x01vorbis" in header)) Open = OggVorbis def delete(filename): """Remove tags from a file.""" OggVorbis(filename).delete() mutagen-1.22/mutagen/oggopus.py0000644000175000017500000000667012177421270017035 0ustar lazkalazka00000000000000# Copyright 2012 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. """Read and write Ogg Opus comments. This module handles Opus files wrapped in an Ogg bitstream. The first Opus stream found is used. Based on http://tools.ietf.org/html/draft-terriberry-oggopus-01 """ __all__ = ["OggOpus", "Open", "delete"] import struct from mutagen._vorbis import VCommentDict from mutagen.ogg import OggPage, OggFileType, error as OggError class error(OggError): pass class OggOpusHeaderError(error): pass class OggOpusInfo(object): """Ogg Opus stream information. Attributes: * length - file length in seconds, as a float * channels - number of channels """ length = 0 def __init__(self, fileobj): page = OggPage(fileobj) while not page.packets[0].startswith("OpusHead"): page = OggPage(fileobj) self.serial = page.serial if not page.first: raise OggOpusHeaderError( "page has ID header, but doesn't start a stream") (version, self.channels, pre_skip, orig_sample_rate, output_gain, channel_map) = struct.unpack("> 4, version & 0xF if major != 0: raise OggOpusHeaderError("version %r unsupported" % major) def _post_tags(self, fileobj): page = OggPage.find_last(fileobj, self.serial) self.length = (page.position - self.__pre_skip) / float(48000) def pprint(self): return "Ogg Opus, %.2f seconds" % (self.length) class OggOpusVComment(VCommentDict): """Opus comments embedded in an Ogg bitstream.""" def __get_comment_pages(self, fileobj, info): # find the first tags page with the right serial page = OggPage(fileobj) while info.serial != page.serial or \ not page.packets[0].startswith("OpusTags"): page = OggPage(fileobj) # get all comment pages pages = [page] while not (pages[-1].complete or len(pages[-1].packets) > 1): page = OggPage(fileobj) if page.serial == pages[0].serial: pages.append(page) return pages def __init__(self, fileobj, info): pages = self.__get_comment_pages(fileobj, info) data = OggPage.to_packets(pages)[0][8:] # Strip OpusTags super(OggOpusVComment, self).__init__(data, framing=False) def _inject(self, fileobj): fileobj.seek(0) info = OggOpusInfo(fileobj) old_pages = self.__get_comment_pages(fileobj, info) packets = OggPage.to_packets(old_pages) packets[0] = "OpusTags" + self.write(framing=False) new_pages = OggPage.from_packets(packets, old_pages[0].sequence) OggPage.replace(fileobj, old_pages, new_pages) class OggOpus(OggFileType): """An Ogg Opus file.""" _Info = OggOpusInfo _Tags = OggOpusVComment _Error = OggOpusHeaderError _mimes = ["audio/ogg", "audio/ogg; codecs=opus"] @staticmethod def score(filename, fileobj, header): return (header.startswith("OggS") * ("OpusHead" in header)) Open = OggOpus def delete(filename): """Remove tags from a file.""" OggOpus(filename).delete() mutagen-1.22/mutagen/mp3.py0000644000175000017500000002313112177421270016040 0ustar lazkalazka00000000000000# MP3 stream header information support for Mutagen. # Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. """MPEG audio stream information and tags.""" import os import struct from mutagen.id3 import ID3FileType, BitPaddedInt, delete __all__ = ["MP3", "Open", "delete", "MP3"] class error(RuntimeError): pass class HeaderNotFoundError(error, IOError): pass class InvalidMPEGHeader(error, IOError): pass # Mode values. STEREO, JOINTSTEREO, DUALCHANNEL, MONO = range(4) class MPEGInfo(object): """MPEG audio stream information Parse information about an MPEG audio file. This also reads the Xing VBR header format. This code was implemented based on the format documentation at http://mpgedit.org/mpgedit/mpeg_format/mpeghdr.htm. Useful attributes: * length -- audio length, in seconds * bitrate -- audio bitrate, in bits per second * sketchy -- if true, the file may not be valid MPEG audio Useless attributes: * version -- MPEG version (1, 2, 2.5) * layer -- 1, 2, or 3 * mode -- One of STEREO, JOINTSTEREO, DUALCHANNEL, or MONO (0-3) * protected -- whether or not the file is "protected" * padding -- whether or not audio frames are padded * sample_rate -- audio sample rate, in Hz """ # Map (version, layer) tuples to bitrates. __BITRATE = { (1, 1): range(0, 480, 32), (1, 2): [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384], (1, 3): [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320], (2, 1): [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256], (2, 2): [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], } __BITRATE[(2, 3)] = __BITRATE[(2, 2)] for i in range(1, 4): __BITRATE[(2.5, i)] = __BITRATE[(2, i)] # Map version to sample rates. __RATES = { 1: [44100, 48000, 32000], 2: [22050, 24000, 16000], 2.5: [11025, 12000, 8000] } sketchy = False def __init__(self, fileobj, offset=None): """Parse MPEG stream information from a file-like object. If an offset argument is given, it is used to start looking for stream information and Xing headers; otherwise, ID3v2 tags will be skipped automatically. A correct offset can make loading files significantly faster. """ try: size = os.path.getsize(fileobj.name) except (IOError, OSError, AttributeError): fileobj.seek(0, 2) size = fileobj.tell() # If we don't get an offset, try to skip an ID3v2 tag. if offset is None: fileobj.seek(0, 0) idata = fileobj.read(10) try: id3, insize = struct.unpack('>3sxxx4s', idata) except struct.error: id3, insize = '', 0 insize = BitPaddedInt(insize) if id3 == 'ID3' and insize > 0: offset = insize + 10 else: offset = 0 # Try to find two valid headers (meaning, very likely MPEG data) # at the given offset, 30% through the file, 60% through the file, # and 90% through the file. for i in [offset, 0.3 * size, 0.6 * size, 0.9 * size]: try: self.__try(fileobj, int(i), size - offset) except error: pass else: break # If we can't find any two consecutive frames, try to find just # one frame back at the original offset given. else: self.__try(fileobj, offset, size - offset, False) self.sketchy = True def __try(self, fileobj, offset, real_size, check_second=True): # This is going to be one really long function; bear with it, # because there's not really a sane point to cut it up. fileobj.seek(offset, 0) # We "know" we have an MPEG file if we find two frames that look like # valid MPEG data. If we can't find them in 32k of reads, something # is horribly wrong (the longest frame can only be about 4k). This # is assuming the offset didn't lie. data = fileobj.read(32768) frame_1 = data.find("\xff") while 0 <= frame_1 <= len(data) - 4: frame_data = struct.unpack(">I", data[frame_1:frame_1 + 4])[0] if (frame_data >> 16) & 0xE0 != 0xE0: frame_1 = data.find("\xff", frame_1 + 2) else: version = (frame_data >> 19) & 0x3 layer = (frame_data >> 17) & 0x3 protection = (frame_data >> 16) & 0x1 bitrate = (frame_data >> 12) & 0xF sample_rate = (frame_data >> 10) & 0x3 padding = (frame_data >> 9) & 0x1 #private = (frame_data >> 8) & 0x1 self.mode = (frame_data >> 6) & 0x3 #mode_extension = (frame_data >> 4) & 0x3 #copyright = (frame_data >> 3) & 0x1 #original = (frame_data >> 2) & 0x1 #emphasis = (frame_data >> 0) & 0x3 if (version == 1 or layer == 0 or sample_rate == 0x3 or bitrate == 0 or bitrate == 0xF): frame_1 = data.find("\xff", frame_1 + 2) else: break else: raise HeaderNotFoundError("can't sync to an MPEG frame") # There is a serious problem here, which is that many flags # in an MPEG header are backwards. self.version = [2.5, None, 2, 1][version] self.layer = 4 - layer self.protected = not protection self.padding = bool(padding) self.bitrate = self.__BITRATE[(self.version, self.layer)][bitrate] self.bitrate *= 1000 self.sample_rate = self.__RATES[self.version][sample_rate] if self.layer == 1: frame_length = (12 * self.bitrate / self.sample_rate + padding) * 4 frame_size = 384 elif self.version >= 2 and self.layer == 3: frame_length = 72 * self.bitrate / self.sample_rate + padding frame_size = 576 else: frame_length = 144 * self.bitrate / self.sample_rate + padding frame_size = 1152 if check_second: possible = frame_1 + frame_length if possible > len(data) + 4: raise HeaderNotFoundError("can't sync to second MPEG frame") try: frame_data = struct.unpack( ">H", data[possible:possible + 2])[0] except struct.error: raise HeaderNotFoundError("can't sync to second MPEG frame") if frame_data & 0xFFE0 != 0xFFE0: raise HeaderNotFoundError("can't sync to second MPEG frame") self.length = 8 * real_size / float(self.bitrate) # Try to find/parse the Xing header, which trumps the above length # and bitrate calculation. fileobj.seek(offset, 0) data = fileobj.read(32768) try: xing = data[:-4].index("Xing") except ValueError: # Try to find/parse the VBRI header, which trumps the above length # calculation. try: vbri = data[:-24].index("VBRI") except ValueError: pass else: # If a VBRI header was found, this is definitely MPEG audio. self.sketchy = False vbri_version = struct.unpack('>H', data[vbri + 4:vbri + 6])[0] if vbri_version == 1: frame_count = struct.unpack( '>I', data[vbri + 14:vbri + 18])[0] samples = float(frame_size * frame_count) self.length = (samples / self.sample_rate) or self.length else: # If a Xing header was found, this is definitely MPEG audio. self.sketchy = False flags = struct.unpack('>I', data[xing + 4:xing + 8])[0] if flags & 0x1: frame_count = struct.unpack('>I', data[xing + 8:xing + 12])[0] samples = float(frame_size * frame_count) self.length = (samples / self.sample_rate) or self.length if flags & 0x2: bytes = struct.unpack('>I', data[xing + 12:xing + 16])[0] self.bitrate = int((bytes * 8) // self.length) def pprint(self): s = "MPEG %s layer %d, %d bps, %s Hz, %.2f seconds" % ( self.version, self.layer, self.bitrate, self.sample_rate, self.length) if self.sketchy: s += " (sketchy)" return s class MP3(ID3FileType): """An MPEG audio (usually MPEG-1 Layer 3) file. :ivar info: :class:`MPEGInfo` :ivar tags: :class:`ID3 ` """ _Info = MPEGInfo _mimes = ["audio/mp3", "audio/x-mp3", "audio/mpeg", "audio/mpg", "audio/x-mpeg"] @staticmethod def score(filename, fileobj, header): filename = filename.lower() return (header.startswith("ID3") * 2 + filename.endswith(".mp3") + filename.endswith(".mp2") + filename.endswith(".mpg") + filename.endswith(".mpeg")) Open = MP3 class EasyMP3(MP3): """Like MP3, but uses EasyID3 for tags. :ivar info: :class:`MPEGInfo` :ivar tags: :class:`EasyID3 ` """ from mutagen.easyid3 import EasyID3 as ID3 ID3 = ID3 mutagen-1.22/mutagen/ogg.py0000644000175000017500000004203412177712376016132 0ustar lazkalazka00000000000000# Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. """Read and write Ogg bitstreams and pages. This module reads and writes a subset of the Ogg bitstream format version 0. It does *not* read or write Ogg Vorbis files! For that, you should use mutagen.oggvorbis. This implementation is based on the RFC 3533 standard found at http://www.xiph.org/ogg/doc/rfc3533.txt. """ import struct import sys import zlib from cStringIO import StringIO from mutagen import FileType from mutagen._util import cdata, insert_bytes, delete_bytes class error(IOError): """Ogg stream parsing errors.""" pass class OggPage(object): """A single Ogg page (not necessarily a single encoded packet). A page is a header of 26 bytes, followed by the length of the data, followed by the data. The constructor is givin a file-like object pointing to the start of an Ogg page. After the constructor is finished it is pointing to the start of the next page. Attributes: * version -- stream structure version (currently always 0) * position -- absolute stream position (default -1) * serial -- logical stream serial number (default 0) * sequence -- page sequence number within logical stream (default 0) * offset -- offset this page was read from (default None) * complete -- if the last packet on this page is complete (default True) * packets -- list of raw packet data (default []) Note that if 'complete' is false, the next page's 'continued' property must be true (so set both when constructing pages). If a file-like object is supplied to the constructor, the above attributes will be filled in based on it. """ version = 0 __type_flags = 0 position = 0L serial = 0 sequence = 0 offset = None complete = True def __init__(self, fileobj=None): self.packets = [] if fileobj is None: return self.offset = fileobj.tell() header = fileobj.read(27) if len(header) == 0: raise EOFError try: (oggs, self.version, self.__type_flags, self.position, self.serial, self.sequence, crc, segments) = struct.unpack( "<4sBBqIIiB", header) except struct.error: raise error("unable to read full header; got %r" % header) if oggs != "OggS": raise error("read %r, expected %r, at 0x%x" % ( oggs, "OggS", fileobj.tell() - 27)) if self.version != 0: raise error("version %r unsupported" % self.version) total = 0 lacings = [] lacing_bytes = fileobj.read(segments) if len(lacing_bytes) != segments: raise error("unable to read %r lacing bytes" % segments) for c in map(ord, lacing_bytes): total += c if c < 255: lacings.append(total) total = 0 if total: lacings.append(total) self.complete = False self.packets = map(fileobj.read, lacings) if map(len, self.packets) != lacings: raise error("unable to read full data") def __eq__(self, other): """Two Ogg pages are the same if they write the same data.""" try: return (self.write() == other.write()) except AttributeError: return False __hash__ = object.__hash__ def __repr__(self): attrs = ['version', 'position', 'serial', 'sequence', 'offset', 'complete', 'continued', 'first', 'last'] values = ["%s=%r" % (attr, getattr(self, attr)) for attr in attrs] return "<%s %s, %d bytes in %d packets>" % ( type(self).__name__, " ".join(values), sum(map(len, self.packets)), len(self.packets)) def write(self): """Return a string encoding of the page header and data. A ValueError is raised if the data is too big to fit in a single page. """ data = [ struct.pack("<4sBBqIIi", "OggS", self.version, self.__type_flags, self.position, self.serial, self.sequence, 0) ] lacing_data = [] for datum in self.packets: quot, rem = divmod(len(datum), 255) lacing_data.append("\xff" * quot + chr(rem)) lacing_data = "".join(lacing_data) if not self.complete and lacing_data.endswith("\x00"): lacing_data = lacing_data[:-1] data.append(chr(len(lacing_data))) data.append(lacing_data) data.extend(self.packets) data = "".join(data) # Python's CRC is swapped relative to Ogg's needs. # crc32 returns uint prior to py2.6 on some platforms, so force uint crc = (~zlib.crc32(data.translate(cdata.bitswap), -1)) & 0xffffffff # Although we're using to_uint_be, this actually makes the CRC # a proper le integer, since Python's CRC is byteswapped. crc = cdata.to_uint_be(crc).translate(cdata.bitswap) data = data[:22] + crc + data[26:] return data @property def size(self): """Total frame size.""" size = 27 # Initial header size for datum in self.packets: quot, rem = divmod(len(datum), 255) size += quot + 1 if not self.complete and rem == 0: # Packet contains a multiple of 255 bytes and is not # terminated, so we don't have a \x00 at the end. size -= 1 size += sum(map(len, self.packets)) return size def __set_flag(self, bit, val): mask = 1 << bit if val: self.__type_flags |= mask else: self.__type_flags &= ~mask continued = property( lambda self: cdata.test_bit(self.__type_flags, 0), lambda self, v: self.__set_flag(0, v), doc="The first packet is continued from the previous page.") first = property( lambda self: cdata.test_bit(self.__type_flags, 1), lambda self, v: self.__set_flag(1, v), doc="This is the first page of a logical bitstream.") last = property( lambda self: cdata.test_bit(self.__type_flags, 2), lambda self, v: self.__set_flag(2, v), doc="This is the last page of a logical bitstream.") @classmethod def renumber(klass, fileobj, serial, start): """Renumber pages belonging to a specified logical stream. fileobj must be opened with mode r+b or w+b. Starting at page number 'start', renumber all pages belonging to logical stream 'serial'. Other pages will be ignored. fileobj must point to the start of a valid Ogg page; any occuring after it and part of the specified logical stream will be numbered. No adjustment will be made to the data in the pages nor the granule position; only the page number, and so also the CRC. If an error occurs (e.g. non-Ogg data is found), fileobj will be left pointing to the place in the stream the error occured, but the invalid data will be left intact (since this function does not change the total file size). """ number = start while True: try: page = OggPage(fileobj) except EOFError: break else: if page.serial != serial: # Wrong stream, skip this page. continue # Changing the number can't change the page size, # so seeking back based on the current size is safe. fileobj.seek(-page.size, 1) page.sequence = number fileobj.write(page.write()) fileobj.seek(page.offset + page.size, 0) number += 1 @classmethod def to_packets(klass, pages, strict=False): """Construct a list of packet data from a list of Ogg pages. If strict is true, the first page must start a new packet, and the last page must end the last packet. """ serial = pages[0].serial sequence = pages[0].sequence packets = [] if strict: if pages[0].continued: raise ValueError("first packet is continued") if not pages[-1].complete: raise ValueError("last packet does not complete") elif pages and pages[0].continued: packets.append([""]) for page in pages: if serial != page.serial: raise ValueError("invalid serial number in %r" % page) elif sequence != page.sequence: raise ValueError("bad sequence number in %r" % page) else: sequence += 1 if page.continued: packets[-1].append(page.packets[0]) else: packets.append([page.packets[0]]) packets.extend([[p] for p in page.packets[1:]]) return ["".join(p) for p in packets] @classmethod def from_packets(klass, packets, sequence=0, default_size=4096, wiggle_room=2048): """Construct a list of Ogg pages from a list of packet data. The algorithm will generate pages of approximately default_size in size (rounded down to the nearest multiple of 255). However, it will also allow pages to increase to approximately default_size + wiggle_room if allowing the wiggle room would finish a packet (only one packet will be finished in this way per page; if the next packet would fit into the wiggle room, it still starts on a new page). This method reduces packet fragmentation when packet sizes are slightly larger than the default page size, while still ensuring most pages are of the average size. Pages are numbered started at 'sequence'; other information is uninitialized. """ chunk_size = (default_size // 255) * 255 pages = [] page = OggPage() page.sequence = sequence for packet in packets: page.packets.append("") while packet: data, packet = packet[:chunk_size], packet[chunk_size:] if page.size < default_size and len(page.packets) < 255: page.packets[-1] += data else: # If we've put any packet data into this page yet, # we need to mark it incomplete. However, we can # also have just started this packet on an already # full page, in which case, just start the new # page with this packet. if page.packets[-1]: page.complete = False if len(page.packets) == 1: page.position = -1L else: page.packets.pop(-1) pages.append(page) page = OggPage() page.continued = not pages[-1].complete page.sequence = pages[-1].sequence + 1 page.packets.append(data) if len(packet) < wiggle_room: page.packets[-1] += packet packet = "" if page.packets: pages.append(page) return pages @classmethod def replace(klass, fileobj, old_pages, new_pages): """Replace old_pages with new_pages within fileobj. old_pages must have come from reading fileobj originally. new_pages are assumed to have the 'same' data as old_pages, and so the serial and sequence numbers will be copied, as will the flags for the first and last pages. fileobj will be resized and pages renumbered as necessary. As such, it must be opened r+b or w+b. """ # Number the new pages starting from the first old page. first = old_pages[0].sequence for page, seq in zip(new_pages, range(first, first + len(new_pages))): page.sequence = seq page.serial = old_pages[0].serial new_pages[0].first = old_pages[0].first new_pages[0].last = old_pages[0].last new_pages[0].continued = old_pages[0].continued new_pages[-1].first = old_pages[-1].first new_pages[-1].last = old_pages[-1].last new_pages[-1].complete = old_pages[-1].complete if not new_pages[-1].complete and len(new_pages[-1].packets) == 1: new_pages[-1].position = -1L new_data = "".join(map(klass.write, new_pages)) # Make room in the file for the new data. delta = len(new_data) fileobj.seek(old_pages[0].offset, 0) insert_bytes(fileobj, delta, old_pages[0].offset) fileobj.seek(old_pages[0].offset, 0) fileobj.write(new_data) new_data_end = old_pages[0].offset + delta # Go through the old pages and delete them. Since we shifted # the data down the file, we need to adjust their offsets. We # also need to go backwards, so we don't adjust the deltas of # the other pages. old_pages.reverse() for old_page in old_pages: adj_offset = old_page.offset + delta delete_bytes(fileobj, old_page.size, adj_offset) # Finally, if there's any discrepency in length, we need to # renumber the pages for the logical stream. if len(old_pages) != len(new_pages): fileobj.seek(new_data_end, 0) serial = new_pages[-1].serial sequence = new_pages[-1].sequence + 1 klass.renumber(fileobj, serial, sequence) @classmethod def find_last(klass, fileobj, serial): """Find the last page of the stream 'serial'. If the file is not multiplexed this function is fast. If it is, it must read the whole the stream. This finds the last page in the actual file object, or the last page in the stream (with eos set), whichever comes first. """ # For non-muxed streams, look at the last page. try: fileobj.seek(-256*256, 2) except IOError: # The file is less than 64k in length. fileobj.seek(0) data = fileobj.read() try: index = data.rindex("OggS") except ValueError: raise error("unable to find final Ogg header") stringobj = StringIO(data[index:]) best_page = None try: page = OggPage(stringobj) except error: pass else: if page.serial == serial: if page.last: return page else: best_page = page else: best_page = None # The stream is muxed, so use the slow way. fileobj.seek(0) try: page = OggPage(fileobj) while not page.last: page = OggPage(fileobj) while page.serial != serial: page = OggPage(fileobj) best_page = page return page except error: return best_page except EOFError: return best_page class OggFileType(FileType): """An generic Ogg file.""" _Info = None _Tags = None _Error = None _mimes = ["application/ogg", "application/x-ogg"] def load(self, filename): """Load file information from a filename.""" self.filename = filename fileobj = open(filename, "rb") try: try: self.info = self._Info(fileobj) self.tags = self._Tags(fileobj, self.info) self.info._post_tags(fileobj) except error, e: raise self._Error, e, sys.exc_info()[2] except EOFError: raise self._Error, "no appropriate stream found" finally: fileobj.close() def delete(self, filename=None): """Remove tags from a file. If no filename is given, the one most recently loaded is used. """ if filename is None: filename = self.filename self.tags.clear() fileobj = open(filename, "rb+") try: try: self.tags._inject(fileobj) except error, e: raise self._Error, e, sys.exc_info()[2] except EOFError: raise self._Error, "no appropriate stream found" finally: fileobj.close() def save(self, filename=None): """Save a tag to a file. If no filename is given, the one most recently loaded is used. """ if filename is None: filename = self.filename fileobj = open(filename, "rb+") try: try: self.tags._inject(fileobj) except error, e: raise self._Error, e, sys.exc_info()[2] except EOFError: raise self._Error, "no appropriate stream found" finally: fileobj.close() mutagen-1.22/mutagen/easymp4.py0000644000175000017500000001775412210703053016727 0ustar lazkalazka00000000000000# Copyright 2009 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. from mutagen import Metadata from mutagen._util import DictMixin, dict_match, utf8 from mutagen.mp4 import MP4, MP4Tags, error, delete __all__ = ["EasyMP4Tags", "EasyMP4", "delete", "error"] class EasyMP4KeyError(error, KeyError, ValueError): pass class EasyMP4Tags(DictMixin, Metadata): """A file with MPEG-4 iTunes metadata. Like Vorbis comments, EasyMP4Tags keys are case-insensitive ASCII strings, and values are a list of Unicode strings (and these lists are always of length 0 or 1). If you need access to the full MP4 metadata feature set, you should use MP4, not EasyMP4. """ Set = {} Get = {} Delete = {} List = {} def __init__(self, *args, **kwargs): self.__mp4 = MP4Tags(*args, **kwargs) self.load = self.__mp4.load self.save = self.__mp4.save self.delete = self.__mp4.delete filename = property(lambda s: s.__mp4.filename, lambda s, fn: setattr(s.__mp4, 'filename', fn)) @classmethod def RegisterKey(cls, key, getter=None, setter=None, deleter=None, lister=None): """Register a new key mapping. A key mapping is four functions, a getter, setter, deleter, and lister. The key may be either a string or a glob pattern. The getter, deleted, and lister receive an MP4Tags instance and the requested key name. The setter also receives the desired value, which will be a list of strings. The getter, setter, and deleter are used to implement __getitem__, __setitem__, and __delitem__. The lister is used to implement keys(). It should return a list of keys that are actually in the MP4 instance, provided by its associated getter. """ key = key.lower() if getter is not None: cls.Get[key] = getter if setter is not None: cls.Set[key] = setter if deleter is not None: cls.Delete[key] = deleter if lister is not None: cls.List[key] = lister @classmethod def RegisterTextKey(cls, key, atomid): """Register a text key. If the key you need to register is a simple one-to-one mapping of MP4 atom name to EasyMP4Tags key, then you can use this function:: EasyMP4Tags.RegisterTextKey("artist", "\xa9ART") """ def getter(tags, key): return tags[atomid] def setter(tags, key, value): tags[atomid] = value def deleter(tags, key): del(tags[atomid]) cls.RegisterKey(key, getter, setter, deleter) @classmethod def RegisterIntKey(cls, key, atomid, min_value=0, max_value=2**16-1): """Register a scalar integer key. """ def getter(tags, key): return map(unicode, tags[atomid]) def setter(tags, key, value): clamp = lambda x: int(min(max(min_value, x), max_value)) tags[atomid] = map(clamp, map(int, value)) def deleter(tags, key): del(tags[atomid]) cls.RegisterKey(key, getter, setter, deleter) @classmethod def RegisterIntPairKey(cls, key, atomid, min_value=0, max_value=2**16-1): def getter(tags, key): ret = [] for (track, total) in tags[atomid]: if total: ret.append(u"%d/%d" % (track, total)) else: ret.append(unicode(track)) return ret def setter(tags, key, value): clamp = lambda x: int(min(max(min_value, x), max_value)) data = [] for v in value: try: tracks, total = v.split("/") tracks = clamp(int(tracks)) total = clamp(int(total)) except (ValueError, TypeError): tracks = clamp(int(v)) total = min_value data.append((tracks, total)) tags[atomid] = data def deleter(tags, key): del(tags[atomid]) cls.RegisterKey(key, getter, setter, deleter) @classmethod def RegisterFreeformKey(cls, key, name, mean="com.apple.iTunes"): """Register a text key. If the key you need to register is a simple one-to-one mapping of MP4 freeform atom (----) and name to EasyMP4Tags key, then you can use this function:: EasyMP4Tags.RegisterFreeformKey( "musicbrainz_artistid", "MusicBrainz Artist Id") """ atomid = "----:%s:%s" % (mean, name) def getter(tags, key): return [s.decode("utf-8", "replace") for s in tags[atomid]] def setter(tags, key, value): tags[atomid] = map(utf8, value) def deleter(tags, key): del(tags[atomid]) cls.RegisterKey(key, getter, setter, deleter) def __getitem__(self, key): key = key.lower() func = dict_match(self.Get, key) if func is not None: return func(self.__mp4, key) else: raise EasyMP4KeyError("%r is not a valid key" % key) def __setitem__(self, key, value): key = key.lower() if isinstance(value, basestring): value = [value] func = dict_match(self.Set, key) if func is not None: return func(self.__mp4, key, value) else: raise EasyMP4KeyError("%r is not a valid key" % key) def __delitem__(self, key): key = key.lower() func = dict_match(self.Delete, key) if func is not None: return func(self.__mp4, key) else: raise EasyMP4KeyError("%r is not a valid key" % key) def keys(self): keys = [] for key in self.Get.keys(): if key in self.List: keys.extend(self.List[key](self.__mp4, key)) elif key in self: keys.append(key) return keys def pprint(self): """Print tag key=value pairs.""" strings = [] for key in sorted(self.keys()): values = self[key] for value in values: strings.append("%s=%s" % (key, value)) return "\n".join(strings) for atomid, key in { '\xa9nam': 'title', '\xa9alb': 'album', '\xa9ART': 'artist', 'aART': 'albumartist', '\xa9day': 'date', '\xa9cmt': 'comment', 'desc': 'description', '\xa9grp': 'grouping', '\xa9gen': 'genre', 'cprt': 'copyright', 'soal': 'albumsort', 'soaa': 'albumartistsort', 'soar': 'artistsort', 'sonm': 'titlesort', 'soco': 'composersort', }.items(): EasyMP4Tags.RegisterTextKey(key, atomid) for name, key in { 'MusicBrainz Artist Id': 'musicbrainz_artistid', 'MusicBrainz Track Id': 'musicbrainz_trackid', 'MusicBrainz Album Id': 'musicbrainz_albumid', 'MusicBrainz Album Artist Id': 'musicbrainz_albumartistid', 'MusicIP PUID': 'musicip_puid', 'MusicBrainz Album Status': 'musicbrainz_albumstatus', 'MusicBrainz Album Type': 'musicbrainz_albumtype', 'MusicBrainz Release Country': 'releasecountry', }.items(): EasyMP4Tags.RegisterFreeformKey(key, name) for name, key in { "tmpo": "bpm", }.items(): EasyMP4Tags.RegisterIntKey(key, name) for name, key in { "trkn": "tracknumber", "disk": "discnumber", }.items(): EasyMP4Tags.RegisterIntPairKey(key, name) class EasyMP4(MP4): """Like :class:`MP4 `, but uses :class:`EasyMP4Tags` for tags. :ivar info: :class:`MP4Info ` :ivar tags: :class:`EasyMP4Tags` """ MP4Tags = EasyMP4Tags Get = EasyMP4Tags.Get Set = EasyMP4Tags.Set Delete = EasyMP4Tags.Delete List = EasyMP4Tags.List RegisterTextKey = EasyMP4Tags.RegisterTextKey RegisterKey = EasyMP4Tags.RegisterKey mutagen-1.22/mutagen/easyid3.py0000644000175000017500000003364412177421270016714 0ustar lazkalazka00000000000000# Simpler (but far more limited) API for ID3 editing # Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. """Easier access to ID3 tags. EasyID3 is a wrapper around mutagen.id3.ID3 to make ID3 tags appear more like Vorbis or APEv2 tags. """ import mutagen.id3 from mutagen import Metadata from mutagen._util import DictMixin, dict_match from mutagen.id3 import ID3, error, delete, ID3FileType __all__ = ['EasyID3', 'Open', 'delete'] class EasyID3KeyError(KeyError, ValueError, error): """Raised when trying to get/set an invalid key. Subclasses both KeyError and ValueError for API compatibility, catching KeyError is preferred. """ class EasyID3(DictMixin, Metadata): """A file with an ID3 tag. Like Vorbis comments, EasyID3 keys are case-insensitive ASCII strings. Only a subset of ID3 frames are supported by default. Use EasyID3.RegisterKey and its wrappers to support more. You can also set the GetFallback, SetFallback, and DeleteFallback to generic key getter/setter/deleter functions, which are called if no specific handler is registered for a key. Additionally, ListFallback can be used to supply an arbitrary list of extra keys. These can be set on EasyID3 or on individual instances after creation. To use an EasyID3 class with mutagen.mp3.MP3:: from mutagen.mp3 import EasyMP3 as MP3 MP3(filename) Because many of the attributes are constructed on the fly, things like the following will not work:: ezid3["performer"].append("Joe") Instead, you must do:: values = ezid3["performer"] values.append("Joe") ezid3["performer"] = values """ Set = {} Get = {} Delete = {} List = {} # For compatibility. valid_keys = Get GetFallback = None SetFallback = None DeleteFallback = None ListFallback = None @classmethod def RegisterKey(cls, key, getter=None, setter=None, deleter=None, lister=None): """Register a new key mapping. A key mapping is four functions, a getter, setter, deleter, and lister. The key may be either a string or a glob pattern. The getter, deleted, and lister receive an ID3 instance and the requested key name. The setter also receives the desired value, which will be a list of strings. The getter, setter, and deleter are used to implement __getitem__, __setitem__, and __delitem__. The lister is used to implement keys(). It should return a list of keys that are actually in the ID3 instance, provided by its associated getter. """ key = key.lower() if getter is not None: cls.Get[key] = getter if setter is not None: cls.Set[key] = setter if deleter is not None: cls.Delete[key] = deleter if lister is not None: cls.List[key] = lister @classmethod def RegisterTextKey(cls, key, frameid): """Register a text key. If the key you need to register is a simple one-to-one mapping of ID3 frame name to EasyID3 key, then you can use this function:: EasyID3.RegisterTextKey("title", "TIT2") """ def getter(id3, key): return list(id3[frameid]) def setter(id3, key, value): try: frame = id3[frameid] except KeyError: id3.add(mutagen.id3.Frames[frameid](encoding=3, text=value)) else: frame.encoding = 3 frame.text = value def deleter(id3, key): del(id3[frameid]) cls.RegisterKey(key, getter, setter, deleter) @classmethod def RegisterTXXXKey(cls, key, desc): """Register a user-defined text frame key. Some ID3 tags are stored in TXXX frames, which allow a freeform 'description' which acts as a subkey, e.g. TXXX:BARCODE.:: EasyID3.RegisterTXXXKey('barcode', 'BARCODE'). """ frameid = "TXXX:" + desc def getter(id3, key): return list(id3[frameid]) def setter(id3, key, value): try: frame = id3[frameid] except KeyError: enc = 0 # Store 8859-1 if we can, per MusicBrainz spec. for v in value: if v and max(v) > u'\x7f': enc = 3 id3.add(mutagen.id3.TXXX(encoding=enc, text=value, desc=desc)) else: frame.text = value def deleter(id3, key): del(id3[frameid]) cls.RegisterKey(key, getter, setter, deleter) def __init__(self, filename=None): self.__id3 = ID3() if filename is not None: self.load(filename) load = property(lambda s: s.__id3.load, lambda s, v: setattr(s.__id3, 'load', v)) save = property(lambda s: s.__id3.save, lambda s, v: setattr(s.__id3, 'save', v)) delete = property(lambda s: s.__id3.delete, lambda s, v: setattr(s.__id3, 'delete', v)) filename = property(lambda s: s.__id3.filename, lambda s, fn: setattr(s.__id3, 'filename', fn)) size = property(lambda s: s.__id3.size, lambda s, fn: setattr(s.__id3, 'size', s)) def __getitem__(self, key): key = key.lower() func = dict_match(self.Get, key, self.GetFallback) if func is not None: return func(self.__id3, key) else: raise EasyID3KeyError("%r is not a valid key" % key) def __setitem__(self, key, value): key = key.lower() if isinstance(value, basestring): value = [value] func = dict_match(self.Set, key, self.SetFallback) if func is not None: return func(self.__id3, key, value) else: raise EasyID3KeyError("%r is not a valid key" % key) def __delitem__(self, key): key = key.lower() func = dict_match(self.Delete, key, self.DeleteFallback) if func is not None: return func(self.__id3, key) else: raise EasyID3KeyError("%r is not a valid key" % key) def keys(self): keys = [] for key in self.Get.keys(): if key in self.List: keys.extend(self.List[key](self.__id3, key)) elif key in self: keys.append(key) if self.ListFallback is not None: keys.extend(self.ListFallback(self.__id3, "")) return keys def pprint(self): """Print tag key=value pairs.""" strings = [] for key in sorted(self.keys()): values = self[key] for value in values: strings.append("%s=%s" % (key, value)) return "\n".join(strings) Open = EasyID3 def genre_get(id3, key): return id3["TCON"].genres def genre_set(id3, key, value): try: frame = id3["TCON"] except KeyError: id3.add(mutagen.id3.TCON(encoding=3, text=value)) else: frame.encoding = 3 frame.genres = value def genre_delete(id3, key): del(id3["TCON"]) def date_get(id3, key): return [stamp.text for stamp in id3["TDRC"].text] def date_set(id3, key, value): id3.add(mutagen.id3.TDRC(encoding=3, text=value)) def date_delete(id3, key): del(id3["TDRC"]) def performer_get(id3, key): people = [] wanted_role = key.split(":", 1)[1] try: mcl = id3["TMCL"] except KeyError: raise KeyError(key) for role, person in mcl.people: if role == wanted_role: people.append(person) if people: return people else: raise KeyError(key) def performer_set(id3, key, value): wanted_role = key.split(":", 1)[1] try: mcl = id3["TMCL"] except KeyError: mcl = mutagen.id3.TMCL(encoding=3, people=[]) id3.add(mcl) mcl.encoding = 3 people = [p for p in mcl.people if p[0] != wanted_role] for v in value: people.append((wanted_role, v)) mcl.people = people def performer_delete(id3, key): wanted_role = key.split(":", 1)[1] try: mcl = id3["TMCL"] except KeyError: raise KeyError(key) people = [p for p in mcl.people if p[0] != wanted_role] if people == mcl.people: raise KeyError(key) elif people: mcl.people = people else: del(id3["TMCL"]) def performer_list(id3, key): try: mcl = id3["TMCL"] except KeyError: return [] else: return list(set("performer:" + p[0] for p in mcl.people)) def musicbrainz_trackid_get(id3, key): return [id3["UFID:http://musicbrainz.org"].data.decode('ascii')] def musicbrainz_trackid_set(id3, key, value): if len(value) != 1: raise ValueError("only one track ID may be set per song") value = value[0].encode('ascii') try: frame = id3["UFID:http://musicbrainz.org"] except KeyError: frame = mutagen.id3.UFID(owner="http://musicbrainz.org", data=value) id3.add(frame) else: frame.data = value def musicbrainz_trackid_delete(id3, key): del(id3["UFID:http://musicbrainz.org"]) def website_get(id3, key): urls = [frame.url for frame in id3.getall("WOAR")] if urls: return urls else: raise EasyID3KeyError(key) def website_set(id3, key, value): id3.delall("WOAR") for v in value: id3.add(mutagen.id3.WOAR(url=v)) def website_delete(id3, key): id3.delall("WOAR") def gain_get(id3, key): try: frame = id3["RVA2:" + key[11:-5]] except KeyError: raise EasyID3KeyError(key) else: return [u"%+f dB" % frame.gain] def gain_set(id3, key, value): if len(value) != 1: raise ValueError( "there must be exactly one gain value, not %r.", value) gain = float(value[0].split()[0]) try: frame = id3["RVA2:" + key[11:-5]] except KeyError: frame = mutagen.id3.RVA2(desc=key[11:-5], gain=0, peak=0, channel=1) id3.add(frame) frame.gain = gain def gain_delete(id3, key): try: frame = id3["RVA2:" + key[11:-5]] except KeyError: pass else: if frame.peak: frame.gain = 0.0 else: del(id3["RVA2:" + key[11:-5]]) def peak_get(id3, key): try: frame = id3["RVA2:" + key[11:-5]] except KeyError: raise EasyID3KeyError(key) else: return [u"%f" % frame.peak] def peak_set(id3, key, value): if len(value) != 1: raise ValueError( "there must be exactly one peak value, not %r.", value) peak = float(value[0]) if peak >= 2 or peak < 0: raise ValueError("peak must be => 0 and < 2.") try: frame = id3["RVA2:" + key[11:-5]] except KeyError: frame = mutagen.id3.RVA2(desc=key[11:-5], gain=0, peak=0, channel=1) id3.add(frame) frame.peak = peak def peak_delete(id3, key): try: frame = id3["RVA2:" + key[11:-5]] except KeyError: pass else: if frame.gain: frame.peak = 0.0 else: del(id3["RVA2:" + key[11:-5]]) def peakgain_list(id3, key): keys = [] for frame in id3.getall("RVA2"): keys.append("replaygain_%s_gain" % frame.desc) keys.append("replaygain_%s_peak" % frame.desc) return keys for frameid, key in { "TALB": "album", "TBPM": "bpm", "TCMP": "compilation", # iTunes extension "TCOM": "composer", "TCOP": "copyright", "TENC": "encodedby", "TEXT": "lyricist", "TLEN": "length", "TMED": "media", "TMOO": "mood", "TIT2": "title", "TIT3": "version", "TPE1": "artist", "TPE2": "performer", "TPE3": "conductor", "TPE4": "arranger", "TPOS": "discnumber", "TPUB": "organization", "TRCK": "tracknumber", "TOLY": "author", "TSO2": "albumartistsort", # iTunes extension "TSOA": "albumsort", "TSOC": "composersort", # iTunes extension "TSOP": "artistsort", "TSOT": "titlesort", "TSRC": "isrc", "TSST": "discsubtitle", }.iteritems(): EasyID3.RegisterTextKey(key, frameid) EasyID3.RegisterKey("genre", genre_get, genre_set, genre_delete) EasyID3.RegisterKey("date", date_get, date_set, date_delete) EasyID3.RegisterKey( "performer:*", performer_get, performer_set, performer_delete, performer_list) EasyID3.RegisterKey("musicbrainz_trackid", musicbrainz_trackid_get, musicbrainz_trackid_set, musicbrainz_trackid_delete) EasyID3.RegisterKey("website", website_get, website_set, website_delete) EasyID3.RegisterKey("website", website_get, website_set, website_delete) EasyID3.RegisterKey( "replaygain_*_gain", gain_get, gain_set, gain_delete, peakgain_list) EasyID3.RegisterKey("replaygain_*_peak", peak_get, peak_set, peak_delete) # At various times, information for this came from # http://musicbrainz.org/docs/specs/metadata_tags.html # http://bugs.musicbrainz.org/ticket/1383 # http://musicbrainz.org/doc/MusicBrainzTag for desc, key in { u"MusicBrainz Artist Id": "musicbrainz_artistid", u"MusicBrainz Album Id": "musicbrainz_albumid", u"MusicBrainz Album Artist Id": "musicbrainz_albumartistid", u"MusicBrainz TRM Id": "musicbrainz_trmid", u"MusicIP PUID": "musicip_puid", u"MusicMagic Fingerprint": "musicip_fingerprint", u"MusicBrainz Album Status": "musicbrainz_albumstatus", u"MusicBrainz Album Type": "musicbrainz_albumtype", u"MusicBrainz Album Release Country": "releasecountry", u"MusicBrainz Disc Id": "musicbrainz_discid", u"ASIN": "asin", u"ALBUMARTISTSORT": "albumartistsort", u"BARCODE": "barcode", }.iteritems(): EasyID3.RegisterTXXXKey(key, desc) class EasyID3FileType(ID3FileType): """Like ID3FileType, but uses EasyID3 for tags.""" ID3 = EasyID3 mutagen-1.22/mutagen/wavpack.py0000644000175000017500000000333012177421270016774 0ustar lazkalazka00000000000000# A WavPack reader/tagger # # Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. """WavPack reading and writing. WavPack is a lossless format that uses APEv2 tags. Read http://www.wavpack.com/ for more information. """ __all__ = ["WavPack", "Open", "delete"] from mutagen.apev2 import APEv2File, error, delete from mutagen._util import cdata class WavPackHeaderError(error): pass RATES = [6000, 8000, 9600, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000, 192000] class WavPackInfo(object): """WavPack stream information. Attributes: * channels - number of audio channels (1 or 2) * length - file length in seconds, as a float * sample_rate - audio sampling rate in Hz * version - WavPack stream version """ def __init__(self, fileobj): header = fileobj.read(28) if len(header) != 28 or not header.startswith("wvpk"): raise WavPackHeaderError("not a WavPack file") samples = cdata.uint_le(header[12:16]) flags = cdata.uint_le(header[24:28]) self.version = cdata.short_le(header[8:10]) self.channels = bool(flags & 4) or 2 self.sample_rate = RATES[(flags >> 23) & 0xF] self.length = float(samples) / self.sample_rate def pprint(self): return "WavPack, %.2f seconds, %d Hz" % (self.length, self.sample_rate) class WavPack(APEv2File): _Info = WavPackInfo _mimes = ["audio/x-wavpack"] @staticmethod def score(filename, fileobj, header): return header.startswith("wvpk") * 2 Open = WavPack mutagen-1.22/mutagen/_id3util.py0000644000175000017500000001050212211670434017050 0ustar lazkalazka00000000000000# Copyright (C) 2005 Michael Urman # 2013 Christoph Reiter # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. class error(Exception): pass class ID3NoHeaderError(error, ValueError): pass class ID3BadUnsynchData(error, ValueError): pass class ID3BadCompressedData(error, ValueError): pass class ID3TagError(error, ValueError): pass class ID3UnsupportedVersionError(error, NotImplementedError): pass class ID3EncryptionUnsupportedError(error, NotImplementedError): pass class ID3JunkFrameError(error, ValueError): pass class ID3Warning(error, UserWarning): pass class unsynch(object): @staticmethod def decode(value): output = [] safe = True append = output.append for val in value: if safe: append(val) safe = val != '\xFF' else: if val >= '\xE0': raise ValueError('invalid sync-safe string') elif val != '\x00': append(val) safe = True if not safe: raise ValueError('string ended unsafe') return ''.join(output) @staticmethod def encode(value): output = [] safe = True append = output.append for val in value: if safe: append(val) if val == '\xFF': safe = False elif val == '\x00' or val >= '\xE0': append('\x00') append(val) safe = val != '\xFF' else: append(val) safe = True if not safe: append('\x00') return ''.join(output) class _BitPaddedMixin(object): def as_str(self, width=4, minwidth=4): return self.to_str(self, self.bits, self.bigendian, width, minwidth) @staticmethod def to_str(value, bits=7, bigendian=True, width=4, minwidth=4): mask = (1 << bits) - 1 if width != -1: index = 0 bytes_ = bytearray(width) try: while value: bytes_[index] = value & mask value >>= bits index += 1 except IndexError: raise ValueError('Value too wide (>%d bytes)' % width) else: # PCNT and POPM use growing integers # of at least 4 bytes (=minwidth) as counters. bytes_ = bytearray() append = bytes_.append while value: append(value & mask) value >>= bits bytes_ = bytes_.ljust(minwidth, "\x00") if bigendian: bytes_.reverse() return str(bytes_) @staticmethod def has_valid_padding(value, bits=7): """Whether the padding bits are all zero""" assert bits <= 8 mask = (((1 << (8 - bits)) - 1) << bits) if isinstance(value, (int, long)): while value: if value & mask: return False value >>= 8 elif isinstance(value, str): for byte in value: if ord(byte) & mask: return False else: raise TypeError return True class BitPaddedInt(int, _BitPaddedMixin): def __new__(cls, value, bits=7, bigendian=True): mask = (1 << (bits)) - 1 numeric_value = 0 shift = 0 if isinstance(value, (int, long)): while value: numeric_value += (value & mask) << shift value >>= 8 shift += bits elif isinstance(value, str): if bigendian: value = reversed(value) for byte in value: numeric_value += (ord(byte) & mask) << shift shift += bits else: raise TypeError if isinstance(numeric_value, long): self = long.__new__(BitPaddedLong, numeric_value) else: self = int.__new__(BitPaddedInt, numeric_value) self.bits = bits self.bigendian = bigendian return self class BitPaddedLong(long, _BitPaddedMixin): pass mutagen-1.22/mutagen/_id3frames.py0000644000175000017500000011743212212326305017356 0ustar lazkalazka00000000000000# Copyright (C) 2005 Michael Urman # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. from zlib import error as zlibError from warnings import warn from struct import unpack from mutagen._id3util import ( ID3Warning, ID3JunkFrameError, ID3BadCompressedData, ID3EncryptionUnsupportedError, ID3BadUnsynchData, unsynch) from mutagen._id3specs import ( BinaryDataSpec, StringSpec, Latin1TextSpec, EncodedTextSpec, ByteSpec, EncodingSpec, ASPIIndexSpec, SizedIntegerSpec, IntegerSpec, VolumeAdjustmentsSpec, VolumePeakSpec, VolumeAdjustmentSpec, ChannelSpec, MultiSpec, SynchronizedTextSpec, KeyEventSpec, TimeStampSpec, EncodedNumericPartTextSpec, EncodedNumericTextSpec) def is_valid_frame_id(frame_id): return frame_id.isalnum() and frame_id.isupper() class Frame(object): """Fundamental unit of ID3 data. ID3 tags are split into frames. Each frame has a potentially different structure, and so this base class is not very featureful. """ FLAG23_ALTERTAG = 0x8000 FLAG23_ALTERFILE = 0x4000 FLAG23_READONLY = 0x2000 FLAG23_COMPRESS = 0x0080 FLAG23_ENCRYPT = 0x0040 FLAG23_GROUP = 0x0020 FLAG24_ALTERTAG = 0x4000 FLAG24_ALTERFILE = 0x2000 FLAG24_READONLY = 0x1000 FLAG24_GROUPID = 0x0040 FLAG24_COMPRESS = 0x0008 FLAG24_ENCRYPT = 0x0004 FLAG24_UNSYNCH = 0x0002 FLAG24_DATALEN = 0x0001 _framespec = [] def __init__(self, *args, **kwargs): if len(args) == 1 and len(kwargs) == 0 and \ isinstance(args[0], type(self)): other = args[0] for checker in self._framespec: try: val = checker.validate(self, getattr(other, checker.name)) except ValueError as e: e.message = "%s: %s" % (checker.name, e.message) raise setattr(self, checker.name, val) else: for checker, val in zip(self._framespec, args): setattr(self, checker.name, checker.validate(self, val)) for checker in self._framespec[len(args):]: try: validated = checker.validate( self, kwargs.get(checker.name, None)) except ValueError as e: e.message = "%s: %s" % (checker.name, e.message) raise setattr(self, checker.name, validated) def _get_v23_frame(self, **kwargs): """Returns a frame copy which is suitable for writing into a v2.3 tag. kwargs get passed to the specs. """ new_kwargs = {} for checker in self._framespec: name = checker.name value = getattr(self, name) new_kwargs[name] = checker._validate23(self, value, **kwargs) return type(self)(**new_kwargs) @property def HashKey(self): """An internal key used to ensure frame uniqueness in a tag""" return self.FrameID @property def FrameID(self): """ID3v2 three or four character frame ID""" return type(self).__name__ def __repr__(self): """Python representation of a frame. The string returned is a valid Python expression to construct a copy of this frame. """ kw = [] for attr in self._framespec: kw.append('%s=%r' % (attr.name, getattr(self, attr.name))) return '%s(%s)' % (type(self).__name__, ', '.join(kw)) def _readData(self, data): odata = data for reader in self._framespec: if len(data): try: value, data = reader.read(self, data) except UnicodeDecodeError: raise ID3JunkFrameError else: raise ID3JunkFrameError setattr(self, reader.name, value) if data.strip('\x00'): warn('Leftover data: %s: %r (from %r)' % ( type(self).__name__, data, odata), ID3Warning) def _writeData(self): data = [] for writer in self._framespec: data.append(writer.write(self, getattr(self, writer.name))) return ''.join(data) def pprint(self): """Return a human-readable representation of the frame.""" return "%s=%s" % (type(self).__name__, self._pprint()) def _pprint(self): return "[unrepresentable data]" @classmethod def fromData(cls, id3, tflags, data): """Construct this ID3 frame from raw string data.""" if id3._V24 <= id3.version: if tflags & (Frame.FLAG24_COMPRESS | Frame.FLAG24_DATALEN): # The data length int is syncsafe in 2.4 (but not 2.3). # However, we don't actually need the data length int, # except to work around a QL 0.12 bug, and in that case # all we need are the raw bytes. datalen_bytes = data[:4] data = data[4:] if tflags & Frame.FLAG24_UNSYNCH or id3.f_unsynch: try: data = unsynch.decode(data) except ValueError, err: if id3.PEDANTIC: raise ID3BadUnsynchData('%s: %r' % (err, data)) if tflags & Frame.FLAG24_ENCRYPT: raise ID3EncryptionUnsupportedError if tflags & Frame.FLAG24_COMPRESS: try: data = data.decode('zlib') except zlibError, err: # the initial mutagen that went out with QL 0.12 did not # write the 4 bytes of uncompressed size. Compensate. data = datalen_bytes + data try: data = data.decode('zlib') except zlibError, err: if id3.PEDANTIC: raise ID3BadCompressedData('%s: %r' % (err, data)) elif id3._V23 <= id3.version: if tflags & Frame.FLAG23_COMPRESS: usize, = unpack('>L', data[:4]) data = data[4:] if tflags & Frame.FLAG23_ENCRYPT: raise ID3EncryptionUnsupportedError if tflags & Frame.FLAG23_COMPRESS: try: data = data.decode('zlib') except zlibError, err: if id3.PEDANTIC: raise ID3BadCompressedData('%s: %r' % (err, data)) frame = cls() frame._rawdata = data frame._flags = tflags frame._readData(data) return frame def __hash__(self): raise TypeError("Frame objects are unhashable") class FrameOpt(Frame): """A frame with optional parts. Some ID3 frames have optional data; this class extends Frame to provide support for those parts. """ _optionalspec = [] def __init__(self, *args, **kwargs): super(FrameOpt, self).__init__(*args, **kwargs) for spec in self._optionalspec: if spec.name in kwargs: validated = spec.validate(self, kwargs[spec.name]) setattr(self, spec.name, validated) else: break def _readData(self, data): odata = data for reader in self._framespec: if len(data): value, data = reader.read(self, data) else: raise ID3JunkFrameError setattr(self, reader.name, value) if data: for reader in self._optionalspec: if len(data): value, data = reader.read(self, data) else: break setattr(self, reader.name, value) if data.strip('\x00'): warn('Leftover data: %s: %r (from %r)' % ( type(self).__name__, data, odata), ID3Warning) def _writeData(self): data = [] for writer in self._framespec: data.append(writer.write(self, getattr(self, writer.name))) for writer in self._optionalspec: try: data.append(writer.write(self, getattr(self, writer.name))) except AttributeError: break return ''.join(data) def __repr__(self): kw = [] for attr in self._framespec: kw.append('%s=%r' % (attr.name, getattr(self, attr.name))) for attr in self._optionalspec: if hasattr(self, attr.name): kw.append('%s=%r' % (attr.name, getattr(self, attr.name))) return '%s(%s)' % (type(self).__name__, ', '.join(kw)) class TextFrame(Frame): """Text strings. Text frames support casts to unicode or str objects, as well as list-like indexing, extend, and append. Iterating over a TextFrame iterates over its strings, not its characters. Text frames have a 'text' attribute which is the list of strings, and an 'encoding' attribute; 0 for ISO-8859 1, 1 UTF-16, 2 for UTF-16BE, and 3 for UTF-8. If you don't want to worry about encodings, just set it to 3. """ _framespec = [ EncodingSpec('encoding'), MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000'), ] def __str__(self): return self.__unicode__().encode('utf-8') def __unicode__(self): return u'\u0000'.join(self.text) def __eq__(self, other): if isinstance(other, str): return str(self) == other elif isinstance(other, unicode): return unicode(self) == other return self.text == other __hash__ = Frame.__hash__ def __getitem__(self, item): return self.text[item] def __iter__(self): return iter(self.text) def append(self, value): """Append a string.""" return self.text.append(value) def extend(self, value): """Extend the list by appending all strings from the given list.""" return self.text.extend(value) def _pprint(self): return " / ".join(self.text) class NumericTextFrame(TextFrame): """Numerical text strings. The numeric value of these frames can be gotten with unary plus, e.g.:: frame = TLEN('12345') length = +frame """ _framespec = [ EncodingSpec('encoding'), MultiSpec('text', EncodedNumericTextSpec('text'), sep=u'\u0000'), ] def __pos__(self): """Return the numerical value of the string.""" return int(self.text[0]) class NumericPartTextFrame(TextFrame): """Multivalue numerical text strings. These strings indicate 'part (e.g. track) X of Y', and unary plus returns the first value:: frame = TRCK('4/15') track = +frame # track == 4 """ _framespec = [ EncodingSpec('encoding'), MultiSpec('text', EncodedNumericPartTextSpec('text'), sep=u'\u0000'), ] def __pos__(self): return int(self.text[0].split("/")[0]) class TimeStampTextFrame(TextFrame): """A list of time stamps. The 'text' attribute in this frame is a list of ID3TimeStamp objects, not a list of strings. """ _framespec = [ EncodingSpec('encoding'), MultiSpec('text', TimeStampSpec('stamp'), sep=u','), ] def __str__(self): return self.__unicode__().encode('utf-8') def __unicode__(self): return ','.join([stamp.text for stamp in self.text]) def _pprint(self): return " / ".join([stamp.text for stamp in self.text]) class UrlFrame(Frame): """A frame containing a URL string. The ID3 specification is silent about IRIs and normalized URL forms. Mutagen assumes all URLs in files are encoded as Latin 1, but string conversion of this frame returns a UTF-8 representation for compatibility with other string conversions. The only sane way to handle URLs in MP3s is to restrict them to ASCII. """ _framespec = [Latin1TextSpec('url')] def __str__(self): return self.url.encode('utf-8') def __unicode__(self): return self.url def __eq__(self, other): return self.url == other __hash__ = Frame.__hash__ def _pprint(self): return self.url class UrlFrameU(UrlFrame): @property def HashKey(self): return '%s:%s' % (self.FrameID, self.url) class TALB(TextFrame): "Album" class TBPM(NumericTextFrame): "Beats per minute" class TCOM(TextFrame): "Composer" class TCON(TextFrame): """Content type (Genre) ID3 has several ways genres can be represented; for convenience, use the 'genres' property rather than the 'text' attribute. """ from mutagen._constants import GENRES GENRES = GENRES def __get_genres(self): genres = [] import re genre_re = re.compile(r"((?:\((?P[0-9]+|RX|CR)\))*)(?P.+)?") for value in self.text: # 255 possible entries in id3v1 if value.isdigit() and int(value) < 256: try: genres.append(self.GENRES[int(value)]) except IndexError: genres.append(u"Unknown") elif value == "CR": genres.append(u"Cover") elif value == "RX": genres.append(u"Remix") elif value: newgenres = [] genreid, dummy, genrename = genre_re.match(value).groups() if genreid: for gid in genreid[1:-1].split(")("): if gid.isdigit() and int(gid) < len(self.GENRES): gid = unicode(self.GENRES[int(gid)]) newgenres.append(gid) elif gid == "CR": newgenres.append(u"Cover") elif gid == "RX": newgenres.append(u"Remix") else: newgenres.append(u"Unknown") if genrename: # "Unescaping" the first parenthesis if genrename.startswith("(("): genrename = genrename[1:] if genrename not in newgenres: newgenres.append(genrename) genres.extend(newgenres) return genres def __set_genres(self, genres): if isinstance(genres, basestring): genres = [genres] self.text = map(self.__decode, genres) def __decode(self, value): if isinstance(value, str): enc = EncodedTextSpec._encodings[self.encoding][0] return value.decode(enc) else: return value genres = property(__get_genres, __set_genres, None, "A list of genres parsed from the raw text data.") def _pprint(self): return " / ".join(self.genres) class TCOP(TextFrame): "Copyright (c)" class TCMP(NumericTextFrame): "iTunes Compilation Flag" class TDAT(TextFrame): "Date of recording (DDMM)" class TDEN(TimeStampTextFrame): "Encoding Time" class TDES(TextFrame): "iTunes Podcast Description" class TDOR(TimeStampTextFrame): "Original Release Time" class TDLY(NumericTextFrame): "Audio Delay (ms)" class TDRC(TimeStampTextFrame): "Recording Time" class TDRL(TimeStampTextFrame): "Release Time" class TDTG(TimeStampTextFrame): "Tagging Time" class TENC(TextFrame): "Encoder" class TEXT(TextFrame): "Lyricist" class TFLT(TextFrame): "File type" class TGID(TextFrame): "iTunes Podcast Identifier" class TIME(TextFrame): "Time of recording (HHMM)" class TIT1(TextFrame): "Content group description" class TIT2(TextFrame): "Title" class TIT3(TextFrame): "Subtitle/Description refinement" class TKEY(TextFrame): "Starting Key" class TLAN(TextFrame): "Audio Languages" class TLEN(NumericTextFrame): "Audio Length (ms)" class TMED(TextFrame): "Source Media Type" class TMOO(TextFrame): "Mood" class TOAL(TextFrame): "Original Album" class TOFN(TextFrame): "Original Filename" class TOLY(TextFrame): "Original Lyricist" class TOPE(TextFrame): "Original Artist/Performer" class TORY(NumericTextFrame): "Original Release Year" class TOWN(TextFrame): "Owner/Licensee" class TPE1(TextFrame): "Lead Artist/Performer/Soloist/Group" class TPE2(TextFrame): "Band/Orchestra/Accompaniment" class TPE3(TextFrame): "Conductor" class TPE4(TextFrame): "Interpreter/Remixer/Modifier" class TPOS(NumericPartTextFrame): "Part of set" class TPRO(TextFrame): "Produced (P)" class TPUB(TextFrame): "Publisher" class TRCK(NumericPartTextFrame): "Track Number" class TRDA(TextFrame): "Recording Dates" class TRSN(TextFrame): "Internet Radio Station Name" class TRSO(TextFrame): "Internet Radio Station Owner" class TSIZ(NumericTextFrame): "Size of audio data (bytes)" class TSO2(TextFrame): "iTunes Album Artist Sort" class TSOA(TextFrame): "Album Sort Order key" class TSOC(TextFrame): "iTunes Composer Sort" class TSOP(TextFrame): "Perfomer Sort Order key" class TSOT(TextFrame): "Title Sort Order key" class TSRC(TextFrame): "International Standard Recording Code (ISRC)" class TSSE(TextFrame): "Encoder settings" class TSST(TextFrame): "Set Subtitle" class TYER(NumericTextFrame): "Year of recording" class TXXX(TextFrame): """User-defined text data. TXXX frames have a 'desc' attribute which is set to any Unicode value (though the encoding of the text and the description must be the same). Many taggers use this frame to store freeform keys. """ _framespec = [ EncodingSpec('encoding'), EncodedTextSpec('desc'), MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000'), ] @property def HashKey(self): return '%s:%s' % (self.FrameID, self.desc) def _pprint(self): return "%s=%s" % (self.desc, " / ".join(self.text)) class WCOM(UrlFrameU): "Commercial Information" class WCOP(UrlFrame): "Copyright Information" class WFED(UrlFrame): "iTunes Podcast Feed" class WOAF(UrlFrame): "Official File Information" class WOAR(UrlFrameU): "Official Artist/Performer Information" class WOAS(UrlFrame): "Official Source Information" class WORS(UrlFrame): "Official Internet Radio Information" class WPAY(UrlFrame): "Payment Information" class WPUB(UrlFrame): "Official Publisher Information" class WXXX(UrlFrame): """User-defined URL data. Like TXXX, this has a freeform description associated with it. """ _framespec = [ EncodingSpec('encoding'), EncodedTextSpec('desc'), Latin1TextSpec('url'), ] @property def HashKey(self): return '%s:%s' % (self.FrameID, self.desc) class PairedTextFrame(Frame): """Paired text strings. Some ID3 frames pair text strings, to associate names with a more specific involvement in the song. The 'people' attribute of these frames contains a list of pairs:: [['trumpet', 'Miles Davis'], ['bass', 'Paul Chambers']] Like text frames, these frames also have an encoding attribute. """ _framespec = [ EncodingSpec('encoding'), MultiSpec('people', EncodedTextSpec('involvement'), EncodedTextSpec('person')) ] def __eq__(self, other): return self.people == other __hash__ = Frame.__hash__ class TIPL(PairedTextFrame): "Involved People List" class TMCL(PairedTextFrame): "Musicians Credits List" class IPLS(TIPL): "Involved People List" class BinaryFrame(Frame): """Binary data The 'data' attribute contains the raw byte string. """ _framespec = [BinaryDataSpec('data')] def __eq__(self, other): return self.data == other __hash__ = Frame.__hash__ class MCDI(BinaryFrame): "Binary dump of CD's TOC" class ETCO(Frame): """Event timing codes.""" _framespec = [ ByteSpec("format"), KeyEventSpec("events"), ] def __eq__(self, other): return self.events == other __hash__ = Frame.__hash__ class MLLT(Frame): """MPEG location lookup table. This frame's attributes may be changed in the future based on feedback from real-world use. """ _framespec = [ SizedIntegerSpec('frames', 2), SizedIntegerSpec('bytes', 3), SizedIntegerSpec('milliseconds', 3), ByteSpec('bits_for_bytes'), ByteSpec('bits_for_milliseconds'), BinaryDataSpec('data'), ] def __eq__(self, other): return self.data == other __hash__ = Frame.__hash__ class SYTC(Frame): """Synchronised tempo codes. This frame's attributes may be changed in the future based on feedback from real-world use. """ _framespec = [ ByteSpec("format"), BinaryDataSpec("data"), ] def __eq__(self, other): return self.data == other __hash__ = Frame.__hash__ class USLT(Frame): """Unsynchronised lyrics/text transcription. Lyrics have a three letter ISO language code ('lang'), a description ('desc'), and a block of plain text ('text'). """ _framespec = [ EncodingSpec('encoding'), StringSpec('lang', 3), EncodedTextSpec('desc'), EncodedTextSpec('text'), ] @property def HashKey(self): return '%s:%s:%r' % (self.FrameID, self.desc, self.lang) def __str__(self): return self.text.encode('utf-8') def __unicode__(self): return self.text def __eq__(self, other): return self.text == other __hash__ = Frame.__hash__ class SYLT(Frame): """Synchronised lyrics/text.""" _framespec = [ EncodingSpec('encoding'), StringSpec('lang', 3), ByteSpec('format'), ByteSpec('type'), EncodedTextSpec('desc'), SynchronizedTextSpec('text'), ] @property def HashKey(self): return '%s:%s:%r' % (self.FrameID, self.desc, self.lang) def __eq__(self, other): return str(self) == other __hash__ = Frame.__hash__ def __str__(self): return "".join([text for (text, time) in self.text]).encode('utf-8') class COMM(TextFrame): """User comment. User comment frames have a descrption, like TXXX, and also a three letter ISO language code in the 'lang' attribute. """ _framespec = [ EncodingSpec('encoding'), StringSpec('lang', 3), EncodedTextSpec('desc'), MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000'), ] @property def HashKey(self): return '%s:%s:%r' % (self.FrameID, self.desc, self.lang) def _pprint(self): return "%s=%r=%s" % (self.desc, self.lang, " / ".join(self.text)) class RVA2(Frame): """Relative volume adjustment (2). This frame is used to implemented volume scaling, and in particular, normalization using ReplayGain. Attributes: * desc -- description or context of this adjustment * channel -- audio channel to adjust (master is 1) * gain -- a + or - dB gain relative to some reference level * peak -- peak of the audio as a floating point number, [0, 1] When storing ReplayGain tags, use descriptions of 'album' and 'track' on channel 1. """ _framespec = [ Latin1TextSpec('desc'), ChannelSpec('channel'), VolumeAdjustmentSpec('gain'), VolumePeakSpec('peak'), ] _channels = ["Other", "Master volume", "Front right", "Front left", "Back right", "Back left", "Front centre", "Back centre", "Subwoofer"] @property def HashKey(self): return '%s:%s' % (self.FrameID, self.desc) def __eq__(self, other): try: return ((str(self) == other) or (self.desc == other.desc and self.channel == other.channel and self.gain == other.gain and self.peak == other.peak)) except AttributeError: return False __hash__ = Frame.__hash__ def __str__(self): return "%s: %+0.4f dB/%0.4f" % ( self._channels[self.channel], self.gain, self.peak) class EQU2(Frame): """Equalisation (2). Attributes: method -- interpolation method (0 = band, 1 = linear) desc -- identifying description adjustments -- list of (frequency, vol_adjustment) pairs """ _framespec = [ ByteSpec("method"), Latin1TextSpec("desc"), VolumeAdjustmentsSpec("adjustments"), ] def __eq__(self, other): return self.adjustments == other __hash__ = Frame.__hash__ @property def HashKey(self): return '%s:%s' % (self.FrameID, self.desc) # class RVAD: unsupported # class EQUA: unsupported class RVRB(Frame): """Reverb.""" _framespec = [ SizedIntegerSpec('left', 2), SizedIntegerSpec('right', 2), ByteSpec('bounce_left'), ByteSpec('bounce_right'), ByteSpec('feedback_ltl'), ByteSpec('feedback_ltr'), ByteSpec('feedback_rtr'), ByteSpec('feedback_rtl'), ByteSpec('premix_ltr'), ByteSpec('premix_rtl'), ] def __eq__(self, other): return (self.left, self.right) == other __hash__ = Frame.__hash__ class APIC(Frame): """Attached (or linked) Picture. Attributes: * encoding -- text encoding for the description * mime -- a MIME type (e.g. image/jpeg) or '-->' if the data is a URI * type -- the source of the image (3 is the album front cover) * desc -- a text description of the image * data -- raw image data, as a byte string Mutagen will automatically compress large images when saving tags. """ _framespec = [ EncodingSpec('encoding'), Latin1TextSpec('mime'), ByteSpec('type'), EncodedTextSpec('desc'), BinaryDataSpec('data'), ] def __eq__(self, other): return self.data == other __hash__ = Frame.__hash__ @property def HashKey(self): return '%s:%s' % (self.FrameID, self.desc) def _pprint(self): return "%s (%s, %d bytes)" % ( self.desc, self.mime, len(self.data)) class PCNT(Frame): """Play counter. The 'count' attribute contains the (recorded) number of times this file has been played. This frame is basically obsoleted by POPM. """ _framespec = [IntegerSpec('count')] def __eq__(self, other): return self.count == other __hash__ = Frame.__hash__ def __pos__(self): return self.count def _pprint(self): return unicode(self.count) class POPM(FrameOpt): """Popularimeter. This frame keys a rating (out of 255) and a play count to an email address. Attributes: * email -- email this POPM frame is for * rating -- rating from 0 to 255 * count -- number of times the files has been played (optional) """ _framespec = [ Latin1TextSpec('email'), ByteSpec('rating'), ] _optionalspec = [IntegerSpec('count')] @property def HashKey(self): return '%s:%s' % (self.FrameID, self.email) def __eq__(self, other): return self.rating == other __hash__ = FrameOpt.__hash__ def __pos__(self): return self.rating def _pprint(self): return "%s=%r %r/255" % ( self.email, getattr(self, 'count', None), self.rating) class GEOB(Frame): """General Encapsulated Object. A blob of binary data, that is not a picture (those go in APIC). Attributes: * encoding -- encoding of the description * mime -- MIME type of the data or '-->' if the data is a URI * filename -- suggested filename if extracted * desc -- text description of the data * data -- raw data, as a byte string """ _framespec = [ EncodingSpec('encoding'), Latin1TextSpec('mime'), EncodedTextSpec('filename'), EncodedTextSpec('desc'), BinaryDataSpec('data'), ] @property def HashKey(self): return '%s:%s' % (self.FrameID, self.desc) def __eq__(self, other): return self.data == other __hash__ = Frame.__hash__ class RBUF(FrameOpt): """Recommended buffer size. Attributes: * size -- recommended buffer size in bytes * info -- if ID3 tags may be elsewhere in the file (optional) * offset -- the location of the next ID3 tag, if any Mutagen will not find the next tag itself. """ _framespec = [SizedIntegerSpec('size', 3)] _optionalspec = [ ByteSpec('info'), SizedIntegerSpec('offset', 4), ] def __eq__(self, other): return self.size == other __hash__ = FrameOpt.__hash__ def __pos__(self): return self.size class AENC(FrameOpt): """Audio encryption. Attributes: * owner -- key identifying this encryption type * preview_start -- unencrypted data block offset * preview_length -- number of unencrypted blocks * data -- data required for decryption (optional) Mutagen cannot decrypt files. """ _framespec = [ Latin1TextSpec('owner'), SizedIntegerSpec('preview_start', 2), SizedIntegerSpec('preview_length', 2), ] _optionalspec = [BinaryDataSpec('data')] @property def HashKey(self): return '%s:%s' % (self.FrameID, self.owner) def __str__(self): return self.owner.encode('utf-8') def __unicode__(self): return self.owner def __eq__(self, other): return self.owner == other __hash__ = FrameOpt.__hash__ class LINK(FrameOpt): """Linked information. Attributes: * frameid -- the ID of the linked frame * url -- the location of the linked frame * data -- further ID information for the frame """ _framespec = [ StringSpec('frameid', 4), Latin1TextSpec('url'), ] _optionalspec = [BinaryDataSpec('data')] @property def HashKey(self): try: return "%s:%s:%s:%r" % ( self.FrameID, self.frameid, self.url, self.data) except AttributeError: return "%s:%s:%s" % (self.FrameID, self.frameid, self.url) def __eq__(self, other): try: return (self.frameid, self.url, self.data) == other except AttributeError: return (self.frameid, self.url) == other __hash__ = FrameOpt.__hash__ class POSS(Frame): """Position synchronisation frame Attribute: * format -- format of the position attribute (frames or milliseconds) * position -- current position of the file """ _framespec = [ ByteSpec('format'), IntegerSpec('position'), ] def __pos__(self): return self.position def __eq__(self, other): return self.position == other __hash__ = Frame.__hash__ class UFID(Frame): """Unique file identifier. Attributes: * owner -- format/type of identifier * data -- identifier """ _framespec = [ Latin1TextSpec('owner'), BinaryDataSpec('data'), ] @property def HashKey(self): return '%s:%s' % (self.FrameID, self.owner) def __eq__(s, o): if isinstance(o, UFI): return s.owner == o.owner and s.data == o.data else: return s.data == o __hash__ = Frame.__hash__ def _pprint(self): isascii = ord(max(self.data)) < 128 if isascii: return "%s=%s" % (self.owner, self.data) else: return "%s (%d bytes)" % (self.owner, len(self.data)) class USER(Frame): """Terms of use. Attributes: * encoding -- text encoding * lang -- ISO three letter language code * text -- licensing terms for the audio """ _framespec = [ EncodingSpec('encoding'), StringSpec('lang', 3), EncodedTextSpec('text'), ] @property def HashKey(self): return '%s:%r' % (self.FrameID, self.lang) def __str__(self): return self.text.encode('utf-8') def __unicode__(self): return self.text def __eq__(self, other): return self.text == other __hash__ = Frame.__hash__ def _pprint(self): return "%r=%s" % (self.lang, self.text) class OWNE(Frame): """Ownership frame.""" _framespec = [ EncodingSpec('encoding'), Latin1TextSpec('price'), StringSpec('date', 8), EncodedTextSpec('seller'), ] def __str__(self): return self.seller.encode('utf-8') def __unicode__(self): return self.seller def __eq__(self, other): return self.seller == other __hash__ = Frame.__hash__ class COMR(FrameOpt): """Commercial frame.""" _framespec = [ EncodingSpec('encoding'), Latin1TextSpec('price'), StringSpec('valid_until', 8), Latin1TextSpec('contact'), ByteSpec('format'), EncodedTextSpec('seller'), EncodedTextSpec('desc'), ] _optionalspec = [ Latin1TextSpec('mime'), BinaryDataSpec('logo'), ] @property def HashKey(self): return '%s:%s' % (self.FrameID, self._writeData()) def __eq__(self, other): return self._writeData() == other._writeData() __hash__ = FrameOpt.__hash__ class ENCR(Frame): """Encryption method registration. The standard does not allow multiple ENCR frames with the same owner or the same method. Mutagen only verifies that the owner is unique. """ _framespec = [ Latin1TextSpec('owner'), ByteSpec('method'), BinaryDataSpec('data'), ] @property def HashKey(self): return "%s:%s" % (self.FrameID, self.owner) def __str__(self): return self.data def __eq__(self, other): return self.data == other __hash__ = Frame.__hash__ class GRID(FrameOpt): """Group identification registration.""" _framespec = [ Latin1TextSpec('owner'), ByteSpec('group'), ] _optionalspec = [BinaryDataSpec('data')] @property def HashKey(self): return '%s:%s' % (self.FrameID, self.group) def __pos__(self): return self.group def __str__(self): return self.owner.encode('utf-8') def __unicode__(self): return self.owner def __eq__(self, other): return self.owner == other or self.group == other __hash__ = FrameOpt.__hash__ class PRIV(Frame): """Private frame.""" _framespec = [ Latin1TextSpec('owner'), BinaryDataSpec('data'), ] @property def HashKey(self): return '%s:%s:%s' % ( self.FrameID, self.owner, self.data.decode('latin1')) def __str__(self): return self.data def __eq__(self, other): return self.data == other def _pprint(self): isascii = ord(max(self.data)) < 128 if isascii: return "%s=%s" % (self.owner, self.data) else: return "%s (%d bytes)" % (self.owner, len(self.data)) __hash__ = Frame.__hash__ class SIGN(Frame): """Signature frame.""" _framespec = [ ByteSpec('group'), BinaryDataSpec('sig'), ] @property def HashKey(self): return '%s:%c:%s' % (self.FrameID, self.group, self.sig) def __str__(self): return self.sig def __eq__(self, other): return self.sig == other __hash__ = Frame.__hash__ class SEEK(Frame): """Seek frame. Mutagen does not find tags at seek offsets. """ _framespec = [IntegerSpec('offset')] def __pos__(self): return self.offset def __eq__(self, other): return self.offset == other __hash__ = Frame.__hash__ class ASPI(Frame): """Audio seek point index. Attributes: S, L, N, b, and Fi. For the meaning of these, see the ID3v2.4 specification. Fi is a list of integers. """ _framespec = [ SizedIntegerSpec("S", 4), SizedIntegerSpec("L", 4), SizedIntegerSpec("N", 2), ByteSpec("b"), ASPIIndexSpec("Fi"), ] def __eq__(self, other): return self.Fi == other __hash__ = Frame.__hash__ Frames = dict([(k, v) for (k, v) in globals().items() if len(k) == 4 and isinstance(v, type) and issubclass(v, Frame)]) """All supported ID3v2 frames, keyed by frame name.""" del(k) del(v) # ID3v2.2 frames class UFI(UFID): "Unique File Identifier" class TT1(TIT1): "Content group description" class TT2(TIT2): "Title" class TT3(TIT3): "Subtitle/Description refinement" class TP1(TPE1): "Lead Artist/Performer/Soloist/Group" class TP2(TPE2): "Band/Orchestra/Accompaniment" class TP3(TPE3): "Conductor" class TP4(TPE4): "Interpreter/Remixer/Modifier" class TCM(TCOM): "Composer" class TXT(TEXT): "Lyricist" class TLA(TLAN): "Audio Language(s)" class TCO(TCON): "Content Type (Genre)" class TAL(TALB): "Album" class TPA(TPOS): "Part of set" class TRK(TRCK): "Track Number" class TRC(TSRC): "International Standard Recording Code (ISRC)" class TYE(TYER): "Year of recording" class TDA(TDAT): "Date of recording (DDMM)" class TIM(TIME): "Time of recording (HHMM)" class TRD(TRDA): "Recording Dates" class TMT(TMED): "Source Media Type" class TFT(TFLT): "File Type" class TBP(TBPM): "Beats per minute" class TCP(TCMP): "iTunes Compilation Flag" class TCR(TCOP): "Copyright (C)" class TPB(TPUB): "Publisher" class TEN(TENC): "Encoder" class TSS(TSSE): "Encoder settings" class TOF(TOFN): "Original Filename" class TLE(TLEN): "Audio Length (ms)" class TSI(TSIZ): "Audio Data size (bytes)" class TDY(TDLY): "Audio Delay (ms)" class TKE(TKEY): "Starting Key" class TOT(TOAL): "Original Album" class TOA(TOPE): "Original Artist/Perfomer" class TOL(TOLY): "Original Lyricist" class TOR(TORY): "Original Release Year" class TXX(TXXX): "User-defined Text" class WAF(WOAF): "Official File Information" class WAR(WOAR): "Official Artist/Performer Information" class WAS(WOAS): "Official Source Information" class WCM(WCOM): "Commercial Information" class WCP(WCOP): "Copyright Information" class WPB(WPUB): "Official Publisher Information" class WXX(WXXX): "User-defined URL" class IPL(IPLS): "Involved people list" class MCI(MCDI): "Binary dump of CD's TOC" class ETC(ETCO): "Event timing codes" class MLL(MLLT): "MPEG location lookup table" class STC(SYTC): "Synced tempo codes" class ULT(USLT): "Unsychronised lyrics/text transcription" class SLT(SYLT): "Synchronised lyrics/text" class COM(COMM): "Comment" #class RVA(RVAD) #class EQU(EQUA) class REV(RVRB): "Reverb" class PIC(APIC): """Attached Picture. The 'mime' attribute of an ID3v2.2 attached picture must be either 'PNG' or 'JPG'. """ _framespec = [EncodingSpec('encoding'), StringSpec('mime', 3), ByteSpec('type'), EncodedTextSpec('desc'), BinaryDataSpec('data')] class GEO(GEOB): "General Encapsulated Object" class CNT(PCNT): "Play counter" class POP(POPM): "Popularimeter" class BUF(RBUF): "Recommended buffer size" class CRM(Frame): """Encrypted meta frame""" _framespec = [Latin1TextSpec('owner'), Latin1TextSpec('desc'), BinaryDataSpec('data')] def __eq__(self, other): return self.data == other __hash__ = Frame.__hash__ class CRA(AENC): "Audio encryption" class LNK(LINK): """Linked information""" _framespec = [StringSpec('frameid', 3), Latin1TextSpec('url')] _optionalspec = [BinaryDataSpec('data')] Frames_2_2 = dict([(k, v) for (k, v) in globals().items() if len(k) == 3 and isinstance(v, type) and issubclass(v, Frame)]) del k del v mutagen-1.22/mutagen/m4a.py0000644000175000017500000004364412177421270016035 0ustar lazkalazka00000000000000# Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. """Read and write MPEG-4 audio files with iTunes metadata. This module will read MPEG-4 audio information and metadata, as found in Apple's M4A (aka MP4, M4B, M4P) files. There is no official specification for this format. The source code for TagLib, FAAD, and various MPEG specifications at http://developer.apple.com/documentation/QuickTime/QTFF/, http://www.geocities.com/xhelmboyx/quicktime/formats/mp4-layout.txt, and http://wiki.multimedia.cx/index.php?title=Apple_QuickTime were all consulted. This module does not support 64 bit atom sizes, and so will not work on metadata over 4GB. """ import struct import sys from cStringIO import StringIO from mutagen import FileType, Metadata from mutagen._constants import GENRES from mutagen._util import cdata, insert_bytes, delete_bytes, DictProxy class error(IOError): pass class M4AMetadataError(error): pass class M4AStreamInfoError(error): pass class M4AMetadataValueError(ValueError, M4AMetadataError): pass import warnings warnings.warn( "mutagen.m4a is deprecated; use mutagen.mp4 instead.", DeprecationWarning) # This is not an exhaustive list of container atoms, but just the # ones this module needs to peek inside. _CONTAINERS = ["moov", "udta", "trak", "mdia", "meta", "ilst", "stbl", "minf", "stsd"] _SKIP_SIZE = {"meta": 4} __all__ = ['M4A', 'Open', 'delete', 'M4ACover'] class M4ACover(str): """A cover artwork. Attributes: imageformat -- format of the image (either FORMAT_JPEG or FORMAT_PNG) """ FORMAT_JPEG = 0x0D FORMAT_PNG = 0x0E def __new__(cls, data, imageformat=None): self = str.__new__(cls, data) if imageformat is None: imageformat = M4ACover.FORMAT_JPEG self.imageformat = imageformat try: self.format except AttributeError: self.format = imageformat return self class Atom(object): """An individual atom. Attributes: children -- list child atoms (or None for non-container atoms) length -- length of this atom, including length and name name -- four byte name of the atom, as a str offset -- location in the constructor-given fileobj of this atom This structure should only be used internally by Mutagen. """ children = None def __init__(self, fileobj): self.offset = fileobj.tell() self.length, self.name = struct.unpack(">I4s", fileobj.read(8)) if self.length == 1: raise error("64 bit atom sizes are not supported") elif self.length < 8: return if self.name in _CONTAINERS: self.children = [] fileobj.seek(_SKIP_SIZE.get(self.name, 0), 1) while fileobj.tell() < self.offset + self.length: self.children.append(Atom(fileobj)) else: fileobj.seek(self.offset + self.length, 0) @staticmethod def render(name, data): """Render raw atom data.""" # this raises OverflowError if Py_ssize_t can't handle the atom data size = len(data) + 8 if size <= 0xFFFFFFFF: return struct.pack(">I4s", size, name) + data else: return struct.pack(">I4sQ", 1, name, size + 8) + data def __getitem__(self, remaining): """Look up a child atom, potentially recursively. e.g. atom['udta', 'meta'] => """ if not remaining: return self elif self.children is None: raise KeyError("%r is not a container" % self.name) for child in self.children: if child.name == remaining[0]: return child[remaining[1:]] else: raise KeyError("%r not found" % remaining[0]) def __repr__(self): klass = self.__class__.__name__ if self.children is None: return "<%s name=%r length=%r offset=%r>" % ( klass, self.name, self.length, self.offset) else: children = "\n".join([" " + line for child in self.children for line in repr(child).splitlines()]) return "<%s name=%r length=%r offset=%r\n%s>" % ( klass, self.name, self.length, self.offset, children) class Atoms(object): """Root atoms in a given file. Attributes: atoms -- a list of top-level atoms as Atom objects This structure should only be used internally by Mutagen. """ def __init__(self, fileobj): self.atoms = [] fileobj.seek(0, 2) end = fileobj.tell() fileobj.seek(0) while fileobj.tell() < end: self.atoms.append(Atom(fileobj)) def path(self, *names): """Look up and return the complete path of an atom. For example, atoms.path('moov', 'udta', 'meta') will return a list of three atoms, corresponding to the moov, udta, and meta atoms. """ path = [self] for name in names: path.append(path[-1][name, ]) return path[1:] def __getitem__(self, names): """Look up a child atom. 'names' may be a list of atoms (['moov', 'udta']) or a string specifying the complete path ('moov.udta'). """ if isinstance(names, basestring): names = names.split(".") for child in self.atoms: if child.name == names[0]: return child[names[1:]] else: raise KeyError("%s not found" % names[0]) def __repr__(self): return "\n".join([repr(child) for child in self.atoms]) class M4ATags(DictProxy, Metadata): """Dictionary containing Apple iTunes metadata list key/values. Keys are four byte identifiers, except for freeform ('----') keys. Values are usually unicode strings, but some atoms have a special structure: cpil -- boolean trkn, disk -- tuple of 16 bit ints (current, total) tmpo -- 16 bit int covr -- list of M4ACover objects (which are tagged strs) gnre -- not supported. Use '\\xa9gen' instead. The freeform '----' frames use a key in the format '----:mean:name' where 'mean' is usually 'com.apple.iTunes' and 'name' is a unique identifier for this frame. The value is a str, but is probably text that can be decoded as UTF-8. M4A tag data cannot exist outside of the structure of an M4A file, so this class should not be manually instantiated. Unknown non-text tags are removed. """ def load(self, atoms, fileobj): try: ilst = atoms["moov.udta.meta.ilst"] except KeyError, key: raise M4AMetadataError(key) for atom in ilst.children: fileobj.seek(atom.offset + 8) data = fileobj.read(atom.length - 8) parse = self.__atoms.get(atom.name, (M4ATags.__parse_text,))[0] parse(self, atom, data) @staticmethod def __key_sort(item1, item2): (key1, v1) = item1 (key2, v2) = item2 # iTunes always writes the tags in order of "relevance", try # to copy it as closely as possible. order = ["\xa9nam", "\xa9ART", "\xa9wrt", "\xa9alb", "\xa9gen", "gnre", "trkn", "disk", "\xa9day", "cpil", "tmpo", "\xa9too", "----", "covr", "\xa9lyr"] order = dict(zip(order, range(len(order)))) last = len(order) # If there's no key-based way to distinguish, order by length. # If there's still no way, go by string comparison on the # values, so we at least have something determinstic. return (cmp(order.get(key1[:4], last), order.get(key2[:4], last)) or cmp(len(v1), len(v2)) or cmp(v1, v2)) def save(self, filename): """Save the metadata to the given filename.""" values = [] items = self.items() items.sort(self.__key_sort) for key, value in items: render = self.__atoms.get( key[:4], (None, M4ATags.__render_text))[1] values.append(render(self, key, value)) data = Atom.render("ilst", "".join(values)) # Find the old atoms. fileobj = open(filename, "rb+") try: atoms = Atoms(fileobj) moov = atoms["moov"] if moov != atoms.atoms[-1]: # "Free" the old moov block. Something in the mdat # block is not happy when its offset changes and it # won't play back. So, rather than try to figure that # out, just move the moov atom to the end of the file. offset = self.__move_moov(fileobj, moov) else: offset = 0 try: path = atoms.path("moov", "udta", "meta", "ilst") except KeyError: self.__save_new(fileobj, atoms, data, offset) else: self.__save_existing(fileobj, atoms, path, data, offset) finally: fileobj.close() def __move_moov(self, fileobj, moov): fileobj.seek(moov.offset) data = fileobj.read(moov.length) fileobj.seek(moov.offset) free = Atom.render("free", "\x00" * (moov.length - 8)) fileobj.write(free) fileobj.seek(0, 2) # Figure out how far we have to shift all our successive # seek calls, relative to what the atoms say. old_end = fileobj.tell() fileobj.write(data) return old_end - moov.offset def __save_new(self, fileobj, atoms, ilst, offset): hdlr = Atom.render("hdlr", "\x00" * 8 + "mdirappl" + "\x00" * 9) meta = Atom.render("meta", "\x00\x00\x00\x00" + hdlr + ilst) moov, udta = atoms.path("moov", "udta") insert_bytes(fileobj, len(meta), udta.offset + offset + 8) fileobj.seek(udta.offset + offset + 8) fileobj.write(meta) self.__update_parents(fileobj, [moov, udta], len(meta), offset) def __save_existing(self, fileobj, atoms, path, data, offset): # Replace the old ilst atom. ilst = path.pop() delta = len(data) - ilst.length fileobj.seek(ilst.offset + offset) if delta > 0: insert_bytes(fileobj, delta, ilst.offset + offset) elif delta < 0: delete_bytes(fileobj, -delta, ilst.offset + offset) fileobj.seek(ilst.offset + offset) fileobj.write(data) self.__update_parents(fileobj, path, delta, offset) def __update_parents(self, fileobj, path, delta, offset): # Update all parent atoms with the new size. for atom in path: fileobj.seek(atom.offset + offset) size = cdata.uint_be(fileobj.read(4)) + delta fileobj.seek(atom.offset + offset) fileobj.write(cdata.to_uint_be(size)) def __render_data(self, key, flags, data): data = struct.pack(">2I", flags, 0) + data return Atom.render(key, Atom.render("data", data)) def __parse_freeform(self, atom, data): try: fileobj = StringIO(data) mean_length = cdata.uint_be(fileobj.read(4)) # skip over 8 bytes of atom name, flags mean = fileobj.read(mean_length - 4)[8:] name_length = cdata.uint_be(fileobj.read(4)) name = fileobj.read(name_length - 4)[8:] value_length = cdata.uint_be(fileobj.read(4)) # Name, flags, and reserved bytes value = fileobj.read(value_length - 4)[12:] except struct.error: # Some ---- atoms have no data atom, I have no clue why # they actually end up in the file. pass else: self["%s:%s:%s" % (atom.name, mean, name)] = value def __render_freeform(self, key, value): dummy, mean, name = key.split(":", 2) mean = struct.pack(">I4sI", len(mean) + 12, "mean", 0) + mean name = struct.pack(">I4sI", len(name) + 12, "name", 0) + name value = struct.pack(">I4s2I", len(value) + 16, "data", 0x1, 0) + value final = mean + name + value return Atom.render("----", final) def __parse_pair(self, atom, data): self[atom.name] = struct.unpack(">2H", data[18:22]) def __render_pair(self, key, value): track, total = value if 0 <= track < 1 << 16 and 0 <= total < 1 << 16: data = struct.pack(">4H", 0, track, total, 0) return self.__render_data(key, 0, data) else: raise M4AMetadataValueError("invalid numeric pair %r" % (value,)) def __render_pair_no_trailing(self, key, value): track, total = value if 0 <= track < 1 << 16 and 0 <= total < 1 << 16: data = struct.pack(">3H", 0, track, total) return self.__render_data(key, 0, data) else: raise M4AMetadataValueError("invalid numeric pair %r" % (value,)) def __parse_genre(self, atom, data): # Translate to a freeform genre. genre = cdata.short_be(data[16:18]) if "\xa9gen" not in self: try: self["\xa9gen"] = GENRES[genre - 1] except IndexError: pass def __parse_tempo(self, atom, data): self[atom.name] = cdata.short_be(data[16:18]) def __render_tempo(self, key, value): if 0 <= value < 1 << 16: return self.__render_data(key, 0x15, cdata.to_ushort_be(value)) else: raise M4AMetadataValueError("invalid short integer %r" % value) def __parse_compilation(self, atom, data): try: self[atom.name] = bool(ord(data[16:17])) except TypeError: self[atom.name] = False def __render_compilation(self, key, value): return self.__render_data(key, 0x15, chr(bool(value))) def __parse_cover(self, atom, data): length, name, imageformat = struct.unpack(">I4sI", data[:12]) if name != "data": raise M4AMetadataError( "unexpected atom %r inside 'covr'" % name) if imageformat not in (M4ACover.FORMAT_JPEG, M4ACover.FORMAT_PNG): imageformat = M4ACover.FORMAT_JPEG self[atom.name] = M4ACover(data[16:length], imageformat) def __render_cover(self, key, value): try: imageformat = value.imageformat except AttributeError: imageformat = M4ACover.FORMAT_JPEG data = Atom.render("data", struct.pack(">2I", imageformat, 0) + value) return Atom.render(key, data) def __parse_text(self, atom, data): flags = cdata.uint_be(data[8:12]) if flags == 1: self[atom.name] = data[16:].decode('utf-8', 'replace') def __render_text(self, key, value): return self.__render_data(key, 0x1, value.encode('utf-8')) def delete(self, filename): self.clear() self.save(filename) __atoms = { "----": (__parse_freeform, __render_freeform), "trkn": (__parse_pair, __render_pair), "disk": (__parse_pair, __render_pair_no_trailing), "gnre": (__parse_genre, None), "tmpo": (__parse_tempo, __render_tempo), "cpil": (__parse_compilation, __render_compilation), "covr": (__parse_cover, __render_cover), } def pprint(self): values = [] for key, value in self.iteritems(): key = key.decode('latin1') try: values.append("%s=%s" % (key, value)) except UnicodeDecodeError: values.append("%s=[%d bytes of data]" % (key, len(value))) return "\n".join(values) class M4AInfo(object): """MPEG-4 stream information. Attributes: bitrate -- bitrate in bits per second, as an int length -- file length in seconds, as a float """ bitrate = 0 def __init__(self, atoms, fileobj): hdlr = atoms["moov.trak.mdia.hdlr"] fileobj.seek(hdlr.offset) if "soun" not in fileobj.read(hdlr.length): raise M4AStreamInfoError("track has no audio data") mdhd = atoms["moov.trak.mdia.mdhd"] fileobj.seek(mdhd.offset) data = fileobj.read(mdhd.length) if ord(data[8]) == 0: offset = 20 fmt = ">2I" else: offset = 28 fmt = ">IQ" end = offset + struct.calcsize(fmt) unit, length = struct.unpack(fmt, data[offset:end]) self.length = float(length) / unit try: atom = atoms["moov.trak.mdia.minf.stbl.stsd"] fileobj.seek(atom.offset) data = fileobj.read(atom.length) self.bitrate = cdata.uint_be(data[-17:-13]) except (ValueError, KeyError): # Bitrate values are optional. pass def pprint(self): return "MPEG-4 audio, %.2f seconds, %d bps" % ( self.length, self.bitrate) class M4A(FileType): """An MPEG-4 audio file, probably containing AAC. If more than one track is present in the file, the first is used. Only audio ('soun') tracks will be read. """ _mimes = ["audio/mp4", "audio/x-m4a", "audio/mpeg4", "audio/aac"] def load(self, filename): self.filename = filename fileobj = open(filename, "rb") try: atoms = Atoms(fileobj) try: self.info = M4AInfo(atoms, fileobj) except StandardError, err: raise M4AStreamInfoError, err, sys.exc_info()[2] try: self.tags = M4ATags(atoms, fileobj) except M4AMetadataError: self.tags = None except StandardError, err: raise M4AMetadataError, err, sys.exc_info()[2] finally: fileobj.close() def add_tags(self): self.tags = M4ATags() @staticmethod def score(filename, fileobj, header): return ("ftyp" in header) + ("mp4" in header) Open = M4A def delete(filename): """Remove tags from a file.""" M4A(filename).delete() mutagen-1.22/tests/0000755000175000017500000000000012213137321014461 5ustar lazkalazka00000000000000mutagen-1.22/tests/test_tools_mutagen_pony.py0000644000175000017500000000052412211140540022013 0ustar lazkalazka00000000000000import os from tests import add from tests.test_tools import _TTools class TMutagenPony(_TTools): TOOL_NAME = "mutagen-pony" def test_basic(self): base = os.path.join('tests', 'data') res, out = self.call(base) self.failIf(res) self.failUnless("Report for tests/data" in out) add(TMutagenPony) mutagen-1.22/tests/test__util.py0000644000175000017500000003024612211074140017210 0ustar lazkalazka00000000000000from mutagen._util import DictMixin, cdata, utf8, insert_bytes, delete_bytes from tests import TestCase, add import random class FDict(DictMixin): def __init__(self): self.__d = {} self.keys = self.__d.keys def __getitem__(self, *args): return self.__d.__getitem__(*args) def __setitem__(self, *args): return self.__d.__setitem__(*args) def __delitem__(self, *args): return self.__d.__delitem__(*args) class Tutf8(TestCase): def test_str(self): value = utf8("1234") self.failUnlessEqual(value, "1234") self.failUnless(isinstance(value, str)) def test_bad_str(self): value = utf8("\xab\xde") # Two '?' symbols. self.failUnlessEqual(value, "\xef\xbf\xbd\xef\xbf\xbd") self.failUnless(isinstance(value, str)) def test_low_unicode(self): value = utf8(u"1234") self.failUnlessEqual(value, "1234") self.failUnless(isinstance(value, str)) def test_high_unicode(self): value = utf8(u"\u1234") self.failUnlessEqual(value, '\xe1\x88\xb4') self.failUnless(isinstance(value, str)) def test_invalid(self): self.failUnlessRaises(TypeError, utf8, 1234) add(Tutf8) class TDictMixin(TestCase): def setUp(self): self.fdict = FDict() self.rdict = {} self.fdict["foo"] = self.rdict["foo"] = "bar" def test_getsetitem(self): self.failUnlessEqual(self.fdict["foo"], "bar") self.failUnlessRaises(KeyError, self.fdict.__getitem__, "bar") def test_has_key_contains(self): self.failUnless("foo" in self.fdict) self.failIf("bar" in self.fdict) self.failUnless(self.fdict.has_key("foo")) self.failIf(self.fdict.has_key("bar")) def test_iter(self): self.failUnlessEqual(list(iter(self.fdict)), ["foo"]) def test_clear(self): self.fdict.clear() self.rdict.clear() self.failIf(self.fdict) def test_keys(self): self.failUnlessEqual(list(self.fdict.keys()), list(self.rdict.keys())) self.failUnlessEqual( list(self.fdict.iterkeys()), list(self.rdict.iterkeys())) def test_values(self): self.failUnlessEqual( list(self.fdict.values()), list(self.rdict.values())) self.failUnlessEqual( list(self.fdict.itervalues()), list(self.rdict.itervalues())) def test_items(self): self.failUnlessEqual( list(self.fdict.items()), list(self.rdict.items())) self.failUnlessEqual( list(self.fdict.iteritems()), list(self.rdict.iteritems())) def test_pop(self): self.failUnlessEqual(self.fdict.pop("foo"), self.rdict.pop("foo")) self.failUnlessRaises(KeyError, self.fdict.pop, "woo") def test_pop_bad(self): self.failUnlessRaises(TypeError, self.fdict.pop, "foo", 1, 2) def test_popitem(self): self.failUnlessEqual(self.fdict.popitem(), self.rdict.popitem()) self.failUnlessRaises(KeyError, self.fdict.popitem) def test_update_other(self): other = {"a": 1, "b": 2} self.fdict.update(other) self.rdict.update(other) def test_update_other_is_list(self): other = [("a", 1), ("b", 2)] self.fdict.update(other) self.rdict.update(dict(other)) def test_update_kwargs(self): self.fdict.update(a=1, b=2) # Ironically, the *real* dict doesn't support this on Python 2.3 other = {"a": 1, "b": 2} self.rdict.update(other) def test_setdefault(self): self.fdict.setdefault("foo", "baz") self.rdict.setdefault("foo", "baz") self.fdict.setdefault("bar", "baz") self.rdict.setdefault("bar", "baz") def test_get(self): self.failUnlessEqual(self.rdict.get("a"), self.fdict.get("a")) self.failUnlessEqual( self.rdict.get("a", "b"), self.fdict.get("a", "b")) self.failUnlessEqual(self.rdict.get("foo"), self.fdict.get("foo")) def test_repr(self): self.failUnlessEqual(repr(self.rdict), repr(self.fdict)) def test_len(self): self.failUnlessEqual(len(self.rdict), len(self.fdict)) def tearDown(self): self.failUnlessEqual(self.fdict, self.rdict) self.failUnlessEqual(self.rdict, self.fdict) add(TDictMixin) class Tcdata(TestCase): ZERO = "\x00\x00\x00\x00" LEONE = "\x01\x00\x00\x00" BEONE = "\x00\x00\x00\x01" NEGONE = "\xff\xff\xff\xff" def test_int_le(self): self.failUnlessEqual(cdata.int_le(self.ZERO), 0) self.failUnlessEqual(cdata.int_le(self.LEONE), 1) self.failUnlessEqual(cdata.int_le(self.BEONE), 16777216) self.failUnlessEqual(cdata.int_le(self.NEGONE), -1) def test_uint_le(self): self.failUnlessEqual(cdata.uint_le(self.ZERO), 0) self.failUnlessEqual(cdata.uint_le(self.LEONE), 1) self.failUnlessEqual(cdata.uint_le(self.BEONE), 16777216) self.failUnlessEqual(cdata.uint_le(self.NEGONE), 2**32-1) def test_longlong_le(self): self.failUnlessEqual(cdata.longlong_le(self.ZERO * 2), 0) self.failUnlessEqual(cdata.longlong_le(self.LEONE + self.ZERO), 1) self.failUnlessEqual(cdata.longlong_le(self.NEGONE * 2), -1) def test_ulonglong_le(self): self.failUnlessEqual(cdata.ulonglong_le(self.ZERO * 2), 0) self.failUnlessEqual(cdata.ulonglong_le(self.LEONE + self.ZERO), 1) self.failUnlessEqual(cdata.ulonglong_le(self.NEGONE * 2), 2**64-1) def test_invalid_lengths(self): self.failUnlessRaises(cdata.error, cdata.int_le, "") self.failUnlessRaises(cdata.error, cdata.longlong_le, "") self.failUnlessRaises(cdata.error, cdata.uint_le, "") self.failUnlessRaises(cdata.error, cdata.ulonglong_le, "") def test_test(self): self.failUnless(cdata.test_bit((1), 0)) self.failIf(cdata.test_bit(1, 1)) self.failUnless(cdata.test_bit(2, 1)) self.failIf(cdata.test_bit(2, 0)) v = (1 << 12) + (1 << 5) + 1 self.failUnless(cdata.test_bit(v, 0)) self.failUnless(cdata.test_bit(v, 5)) self.failUnless(cdata.test_bit(v, 12)) self.failIf(cdata.test_bit(v, 3)) self.failIf(cdata.test_bit(v, 8)) self.failIf(cdata.test_bit(v, 13)) add(Tcdata) class FileHandling(TestCase): def file(self, contents): import tempfile temp = tempfile.TemporaryFile() temp.write(contents) temp.flush() temp.seek(0) return temp def read(self, fobj): fobj.seek(0, 0) return fobj.read() def test_insert_into_empty(self): o = self.file('') insert_bytes(o, 8, 0) self.assertEquals('\x00' * 8, self.read(o)) def test_insert_before_one(self): o = self.file('a') insert_bytes(o, 8, 0) self.assertEquals('a' + '\x00' * 7 + 'a', self.read(o)) def test_insert_after_one(self): o = self.file('a') insert_bytes(o, 8, 1) self.assertEquals('a' + '\x00' * 8, self.read(o)) def test_smaller_than_file_middle(self): o = self.file('abcdefghij') insert_bytes(o, 4, 4) self.assertEquals('abcdefghefghij', self.read(o)) def test_smaller_than_file_to_end(self): o = self.file('abcdefghij') insert_bytes(o, 4, 6) self.assertEquals('abcdefghijghij', self.read(o)) def test_smaller_than_file_across_end(self): o = self.file('abcdefghij') insert_bytes(o, 4, 8) self.assertEquals('abcdefghij\x00\x00ij', self.read(o)) def test_smaller_than_file_at_end(self): o = self.file('abcdefghij') insert_bytes(o, 3, 10) self.assertEquals('abcdefghij\x00\x00\x00', self.read(o)) def test_smaller_than_file_at_beginning(self): o = self.file('abcdefghij') insert_bytes(o, 3, 0) self.assertEquals('abcabcdefghij', self.read(o)) def test_zero(self): o = self.file('abcdefghij') self.assertRaises((AssertionError, ValueError), insert_bytes, o, 0, 1) def test_negative(self): o = self.file('abcdefghij') self.assertRaises((AssertionError, ValueError), insert_bytes, o, 8, -1) def test_delete_one(self): o = self.file('a') delete_bytes(o, 1, 0) self.assertEquals('', self.read(o)) def test_delete_first_of_two(self): o = self.file('ab') delete_bytes(o, 1, 0) self.assertEquals('b', self.read(o)) def test_delete_second_of_two(self): o = self.file('ab') delete_bytes(o, 1, 1) self.assertEquals('a', self.read(o)) def test_delete_third_of_two(self): o = self.file('ab') self.assertRaises(AssertionError, delete_bytes, o, 1, 2) def test_delete_middle(self): o = self.file('abcdefg') delete_bytes(o, 3, 2) self.assertEquals('abfg', self.read(o)) def test_delete_across_end(self): o = self.file('abcdefg') self.assertRaises(AssertionError, delete_bytes, o, 4, 8) def test_delete_zero(self): o = self.file('abcdefg') self.assertRaises(AssertionError, delete_bytes, o, 0, 3) def test_delete_negative(self): o = self.file('abcdefg') self.assertRaises(AssertionError, delete_bytes, o, 4, -8) def test_insert_6106_79_51760(self): # This appears to be due to ANSI C limitations in read/write on rb+ # files. The problematic behavior only showed up in our mmap fallback # code for transfers of this or similar sizes. data = ''.join(map(str, range(12574))) # 51760 bytes o = self.file(data) insert_bytes(o, 6106, 79) self.failUnless(data[:6106+79] + data[79:] == self.read(o)) def test_delete_6106_79_51760(self): # This appears to be due to ANSI C limitations in read/write on rb+ # files. The problematic behavior only showed up in our mmap fallback # code for transfers of this or similar sizes. data = ''.join(map(str, range(12574))) # 51760 bytes o = self.file(data[:6106+79] + data[79:]) delete_bytes(o, 6106, 79) self.failUnless(data == self.read(o)) # Generate a bunch of random insertions, apply them, delete them, # and make sure everything is still correct. # # The num_runs and num_changes values are tuned to take about 10s # on my laptop, or about 30 seconds since we we have 3 variations # on insert/delete_bytes brokenness. If I ever get a faster # laptop, it's probably a good idea to increase them. :) def test_many_changes(self, num_runs=5, num_changes=300, min_change_size=500, max_change_size=1000, min_buffer_size=1, max_buffer_size=2000): self.failUnless(min_buffer_size < min_change_size and max_buffer_size > max_change_size and min_change_size < max_change_size and min_buffer_size < max_buffer_size, "Given testing parameters make this test useless") for j in range(num_runs): data = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" * 1024 fobj = self.file(data) filesize = len(data) # Generate the list of changes to apply changes = [] for i in range(num_changes): change_size = random.randrange(min_change_size, max_change_size) change_offset = random.randrange(0, filesize) filesize += change_size changes.append((change_offset, change_size)) # Apply the changes, and make sure they all took. for offset, size in changes: buffer_size = random.randrange(min_buffer_size, max_buffer_size) insert_bytes(fobj, size, offset, BUFFER_SIZE=buffer_size) fobj.seek(0) self.failIfEqual(fobj.read(len(data)), data) fobj.seek(0, 2) self.failUnlessEqual(fobj.tell(), filesize) # Then, undo them. changes.reverse() for offset, size in changes: buffer_size = random.randrange(min_buffer_size, max_buffer_size) delete_bytes(fobj, size, offset, BUFFER_SIZE=buffer_size) fobj.seek(0) self.failUnless(fobj.read() == data) add(FileHandling) mutagen-1.22/tests/test_oggflac.py0000644000175000017500000000715412177421270017513 0ustar lazkalazka00000000000000import os import shutil from tempfile import mkstemp from cStringIO import StringIO from mutagen.oggflac import OggFLAC, OggFLACStreamInfo, delete from mutagen.ogg import OggPage, error as OggError from tests import add from tests.test_ogg import TOggFileType try: from os.path import devnull except ImportError: devnull = "/dev/null" class TOggFLAC(TOggFileType): Kind = OggFLAC def setUp(self): original = os.path.join("tests", "data", "empty.oggflac") fd, self.filename = mkstemp(suffix='.ogg') os.close(fd) shutil.copy(original, self.filename) self.audio = OggFLAC(self.filename) def test_vendor(self): self.failUnless( self.audio.tags.vendor.startswith("reference libFLAC")) self.failUnlessRaises(KeyError, self.audio.tags.__getitem__, "vendor") def test_streaminfo_bad_marker(self): page = OggPage(open(self.filename, "rb")).write() page = page.replace("fLaC", "!fLa", 1) self.failUnlessRaises(IOError, OggFLACStreamInfo, StringIO(page)) def test_streaminfo_too_short(self): page = OggPage(open(self.filename, "rb")).write() self.failUnlessRaises(OggError, OggFLACStreamInfo, StringIO(page[:10])) def test_streaminfo_bad_version(self): page = OggPage(open(self.filename, "rb")).write() page = page.replace("\x01\x00", "\x02\x00", 1) self.failUnlessRaises(IOError, OggFLACStreamInfo, StringIO(page)) def test_flac_reference_simple_save(self): if not have_flac: return self.audio.save() self.scan_file() value = os.system("flac --ogg -t %s 2> %s" % (self.filename, devnull)) self.failIf(value and value != NOTFOUND) def test_flac_reference_really_big(self): if not have_flac: return self.test_really_big() self.audio.save() self.scan_file() value = os.system("flac --ogg -t %s 2> %s" % (self.filename, devnull)) self.failIf(value and value != NOTFOUND) def test_module_delete(self): delete(self.filename) self.scan_file() self.failIf(OggFLAC(self.filename).tags) def test_flac_reference_delete(self): if not have_flac: return self.audio.delete() self.scan_file() value = os.system("flac --ogg -t %s 2> %s" % (self.filename, devnull)) self.failIf(value and value != NOTFOUND) def test_flac_reference_medium_sized(self): if not have_flac: return self.audio["foobar"] = "foobar" * 1000 self.audio.save() self.scan_file() value = os.system("flac --ogg -t %s 2> %s" % (self.filename, devnull)) self.failIf(value and value != NOTFOUND) def test_flac_reference_delete_readd(self): if not have_flac: return self.audio.delete() self.audio.tags.clear() self.audio["foobar"] = "foobar" * 1000 self.audio.save() self.scan_file() value = os.system("flac --ogg -t %s 2> %s" % (self.filename, devnull)) self.failIf(value and value != NOTFOUND) def test_not_my_ogg(self): fn = os.path.join('tests', 'data', 'empty.ogg') self.failUnlessRaises(IOError, type(self.audio), fn) self.failUnlessRaises(IOError, self.audio.save, fn) self.failUnlessRaises(IOError, self.audio.delete, fn) def test_mime(self): self.failUnless("audio/x-oggflac" in self.audio.mime) add(TOggFLAC) NOTFOUND = os.system("tools/notarealprogram 2> %s" % devnull) have_flac = True if os.system("flac 2> %s > %s" % (devnull, devnull)) == NOTFOUND: have_flac = False print "WARNING: Skipping Ogg FLAC reference tests." mutagen-1.22/tests/test_tools_moggsplit.py0000644000175000017500000000233412211136236021323 0ustar lazkalazka00000000000000import os from tempfile import mkstemp import shutil from tests import add from tests.test_tools import _TTools class TMOggSPlit(_TTools): TOOL_NAME = "moggsplit" def setUp(self): super(TMOggSPlit, self).setUp() original = os.path.join('tests', 'data', 'multipagecomment.ogg') fd, self.filename = mkstemp(suffix='.ogg') os.close(fd) shutil.copy(original, self.filename) # append the second file first = open(self.filename, "ab") to_append = os.path.join('tests', 'data', 'multipage-setup.ogg') second = open(to_append, "rb") first.write(second.read()) second.close() first.close() def tearDown(self): super(TMOggSPlit, self).tearDown() os.unlink(self.filename) def test_basic(self): d = os.path.dirname(self.filename) p = os.path.join(d, "%(stream)d.%(ext)s") res, out = self.call("--pattern", p, self.filename) self.failIf(res) self.failIf(out) for stream in [1002429366, 1806412655]: stream_path = os.path.join(d, str(stream) + ".ogg") self.failUnless(os.path.exists(stream_path)) os.unlink(stream_path) add(TMOggSPlit) mutagen-1.22/tests/test_oggvorbis.py0000644000175000017500000001417312177421270020111 0ustar lazkalazka00000000000000import os import shutil from cStringIO import StringIO from mutagen.ogg import OggPage from mutagen.oggvorbis import OggVorbis, OggVorbisInfo, delete from tests import add from tests.test_ogg import TOggFileType from tempfile import mkstemp class TOggVorbis(TOggFileType): Kind = OggVorbis def setUp(self): original = os.path.join("tests", "data", "empty.ogg") fd, self.filename = mkstemp(suffix='.ogg') os.close(fd) shutil.copy(original, self.filename) self.audio = self.Kind(self.filename) def test_module_delete(self): delete(self.filename) self.scan_file() self.failIf(OggVorbis(self.filename).tags) def test_bitrate(self): self.failUnlessEqual(112000, self.audio.info.bitrate) def test_channels(self): self.failUnlessEqual(2, self.audio.info.channels) def test_sample_rate(self): self.failUnlessEqual(44100, self.audio.info.sample_rate) def test_invalid_not_first(self): page = OggPage(open(self.filename, "rb")) page.first = False self.failUnlessRaises(IOError, OggVorbisInfo, StringIO(page.write())) def test_avg_bitrate(self): page = OggPage(open(self.filename, "rb")) packet = page.packets[0] packet = (packet[:16] + "\x00\x00\x01\x00" + "\x00\x00\x00\x00" + "\x00\x00\x00\x00" + packet[28:]) page.packets[0] = packet info = OggVorbisInfo(StringIO(page.write())) self.failUnlessEqual(info.bitrate, 32768) def test_overestimated_bitrate(self): page = OggPage(open(self.filename, "rb")) packet = page.packets[0] packet = (packet[:16] + "\x00\x00\x01\x00" + "\x00\x00\x00\x01" + "\x00\x00\x00\x00" + packet[28:]) page.packets[0] = packet info = OggVorbisInfo(StringIO(page.write())) self.failUnlessEqual(info.bitrate, 65536) def test_underestimated_bitrate(self): page = OggPage(open(self.filename, "rb")) packet = page.packets[0] packet = (packet[:16] + "\x00\x00\x01\x00" + "\x01\x00\x00\x00" + "\x00\x00\x01\x00" + packet[28:]) page.packets[0] = packet info = OggVorbisInfo(StringIO(page.write())) self.failUnlessEqual(info.bitrate, 65536) def test_negative_bitrate(self): page = OggPage(open(self.filename, "rb")) packet = page.packets[0] packet = (packet[:16] + "\xff\xff\xff\xff" + "\xff\xff\xff\xff" + "\xff\xff\xff\xff" + packet[28:]) page.packets[0] = packet info = OggVorbisInfo(StringIO(page.write())) self.failUnlessEqual(info.bitrate, 0) def test_vendor(self): self.failUnless( self.audio.tags.vendor.startswith("Xiph.Org libVorbis")) self.failUnlessRaises(KeyError, self.audio.tags.__getitem__, "vendor") def test_vorbiscomment(self): self.audio.save() self.scan_file() if ogg is None: return self.failUnless(ogg.vorbis.VorbisFile(self.filename)) def test_vorbiscomment_big(self): self.test_really_big() self.audio.save() self.scan_file() if ogg is None: return vfc = ogg.vorbis.VorbisFile(self.filename).comment() self.failUnlessEqual(self.audio["foo"], vfc["foo"]) def test_vorbiscomment_delete(self): self.audio.delete() self.scan_file() if ogg is None: return vfc = ogg.vorbis.VorbisFile(self.filename).comment() self.failUnlessEqual(vfc.keys(), ["VENDOR"]) def test_vorbiscomment_delete_readd(self): self.audio.delete() self.audio.tags.clear() self.audio["foobar"] = "foobar" * 1000 self.audio.save() self.scan_file() if ogg is None: return vfc = ogg.vorbis.VorbisFile(self.filename).comment() self.failUnlessEqual(self.audio["foobar"], vfc["foobar"]) self.failUnless("FOOBAR" in vfc.keys()) self.failUnless("VENDOR" in vfc.keys()) def test_huge_tag(self): vorbis = self.Kind( os.path.join("tests", "data", "multipagecomment.ogg")) self.failUnless("big" in vorbis.tags) self.failUnless("bigger" in vorbis.tags) self.failUnlessEqual(vorbis.tags["big"], ["foobar" * 10000]) self.failUnlessEqual(vorbis.tags["bigger"], ["quuxbaz" * 10000]) self.scan_file() def test_not_my_ogg(self): fn = os.path.join('tests', 'data', 'empty.oggflac') self.failUnlessRaises(IOError, type(self.audio), fn) self.failUnlessRaises(IOError, self.audio.save, fn) self.failUnlessRaises(IOError, self.audio.delete, fn) def test_save_split_setup_packet(self): fn = os.path.join("tests", "data", "multipage-setup.ogg") shutil.copy(fn, self.filename) audio = OggVorbis(self.filename) tags = audio.tags self.failUnless(tags) audio.save() self.audio = OggVorbis(self.filename) self.failUnlessEqual(self.audio.tags, tags) def test_save_split_setup_packet_reference(self): if ogg is None: return self.test_save_split_setup_packet() vfc = ogg.vorbis.VorbisFile(self.filename).comment() for key in self.audio: self.failUnlessEqual(vfc[key], self.audio[key]) self.ogg_reference(self.filename) def test_save_grown_split_setup_packet_reference(self): if ogg is None: return fn = os.path.join("tests", "data", "multipage-setup.ogg") shutil.copy(fn, self.filename) audio = OggVorbis(self.filename) audio["foobar"] = ["quux" * 50000] tags = audio.tags self.failUnless(tags) audio.save() self.audio = OggVorbis(self.filename) self.failUnlessEqual(self.audio.tags, tags) vfc = ogg.vorbis.VorbisFile(self.filename).comment() for key in self.audio: self.failUnlessEqual(vfc[key], self.audio[key]) self.ogg_reference(self.filename) def test_mime(self): self.failUnless("audio/vorbis" in self.audio.mime) try: import ogg.vorbis except ImportError: print "WARNING: Skipping Ogg Vorbis reference tests." ogg = None add(TOggVorbis) mutagen-1.22/tests/test_monkeysaudio.py0000644000175000017500000000367212211074405020612 0ustar lazkalazka00000000000000import os from mutagen.monkeysaudio import MonkeysAudio, MonkeysAudioHeaderError from tests import TestCase, add class TMonkeysAudio(TestCase): def setUp(self): self.mac399 = MonkeysAudio(os.path.join("tests", "data", "mac-399.ape")) self.mac396 = MonkeysAudio(os.path.join("tests", "data", "mac-396.ape")) self.mac390 = MonkeysAudio(os.path.join("tests", "data", "mac-390-hdr.ape")) def test_channels(self): self.failUnlessEqual(self.mac399.info.channels, 2) self.failUnlessEqual(self.mac396.info.channels, 2) self.failUnlessEqual(self.mac390.info.channels, 2) def test_sample_rate(self): self.failUnlessEqual(self.mac399.info.sample_rate, 44100) self.failUnlessEqual(self.mac396.info.sample_rate, 44100) self.failUnlessEqual(self.mac390.info.sample_rate, 44100) def test_length(self): self.failUnlessAlmostEqual(self.mac399.info.length, 3.68, 2) self.failUnlessAlmostEqual(self.mac396.info.length, 3.68, 2) self.failUnlessAlmostEqual(self.mac390.info.length, 15.63, 2) def test_version(self): self.failUnlessEqual(self.mac399.info.version, 3.99) self.failUnlessEqual(self.mac396.info.version, 3.96) self.failUnlessEqual(self.mac390.info.version, 3.90) def test_not_my_file(self): self.failUnlessRaises( MonkeysAudioHeaderError, MonkeysAudio, os.path.join("tests", "data", "empty.ogg")) self.failUnlessRaises( MonkeysAudioHeaderError, MonkeysAudio, os.path.join("tests", "data", "click.mpc")) def test_mime(self): self.failUnless("audio/x-ape" in self.mac399.mime) def test_pprint(self): self.failUnless(self.mac399.pprint()) self.failUnless(self.mac396.pprint()) add(TMonkeysAudio) mutagen-1.22/tests/test__vorbis.py0000644000175000017500000001633712211424713017551 0ustar lazkalazka00000000000000from tests import add, TestCase from mutagen._vorbis import VComment, VCommentDict, istag class Tistag(TestCase): def test_empty(self): self.failIf(istag("")) def test_tilde(self): self.failIf(istag("ti~tle")) def test_equals(self): self.failIf(istag("ti=tle")) def test_less(self): self.failIf(istag("ti\x19tle")) def test_greater(self): self.failIf(istag("ti\xa0tle")) def test_simple(self): self.failUnless(istag("title")) def test_space(self): self.failUnless(istag("ti tle")) def test_ugly(self): self.failUnless(istag("!{}[]-_()*&")) add(Tistag) class TVComment(TestCase): def setUp(self): self.c = VComment() self.c.append(("artist", u"piman")) self.c.append(("artist", u"mu")) self.c.append(("title", u"more fakes")) def test_invalid_init(self): self.failUnlessRaises(TypeError, VComment, []) def test_equal(self): self.failUnlessEqual(self.c, self.c) def test_not_header(self): self.failUnlessRaises(IOError, VComment, "foo") def test_unset_framing_bit(self): self.failUnlessRaises( IOError, VComment, "\x00\x00\x00\x00" * 2 + "\x00") def test_empty_valid(self): self.failIf(VComment("\x00\x00\x00\x00" * 2 + "\x01")) def test_validate(self): self.failUnless(self.c.validate()) def test_validate_broken_key(self): self.c.append((1, u"valid")) self.failUnlessRaises(ValueError, self.c.validate) self.failUnlessRaises(ValueError, self.c.write) def test_validate_broken_value(self): self.c.append(("valid", 1)) self.failUnlessRaises(ValueError, self.c.validate) self.failUnlessRaises(ValueError, self.c.write) def test_validate_nonunicode_value(self): self.c.append(("valid", "wt\xff")) self.failUnlessRaises(ValueError, self.c.validate) self.failUnlessRaises(ValueError, self.c.write) def test_vendor_default(self): self.failUnless(self.c.vendor.startswith("Mutagen")) def test_vendor_set(self): self.c.vendor = "Not Mutagen" self.failUnless(self.c.write()[4:].startswith("Not Mutagen")) def test_vendor_invalid(self): self.c.vendor = "\xffNot Mutagen" self.failUnlessRaises(ValueError, self.c.validate) self.failUnlessRaises(ValueError, self.c.write) def test_invalid_format_strict(self): data = ('\x07\x00\x00\x00Mutagen\x01\x00\x00\x00\x03\x00\x00' '\x00abc\x01') self.failUnlessRaises(IOError, VComment, data, errors='strict') def test_invalid_format_replace(self): data = ('\x07\x00\x00\x00Mutagen\x01\x00\x00\x00\x03\x00\x00' '\x00abc\x01') comment = VComment(data) self.failUnlessEqual("abc", comment[0][1]) def test_invalid_format_ignore(self): data = ('\x07\x00\x00\x00Mutagen\x01\x00\x00\x00\x03\x00\x00' '\x00abc\x01') comment = VComment(data, errors='ignore') self.failIf(len(comment)) # Slightly different test data than above, we want the tag name # to be valid UTF-8 but not valid ASCII. def test_invalid_tag_strict(self): data = ('\x07\x00\x00\x00Mutagen\x01\x00\x00\x00\x04\x00\x00' '\x00\xc2\xaa=c\x01') self.failUnlessRaises(IOError, VComment, data, errors='strict') def test_invalid_tag_replace(self): data = ('\x07\x00\x00\x00Mutagen\x01\x00\x00\x00\x04\x00\x00' '\x00\xc2\xaa=c\x01') comment = VComment(data) self.failUnlessEqual("?=c", comment.pprint()) def test_invalid_tag_ignore(self): data = ('\x07\x00\x00\x00Mutagen\x01\x00\x00\x00\x04\x00\x00' '\x00\xc2\xaa=c\x01') comment = VComment(data, errors='ignore') self.failIf(len(comment)) def test_roundtrip(self): self.failUnlessEqual(self.c, VComment(self.c.write())) add(TVComment) class TVCommentDict(TestCase): Kind = VCommentDict def setUp(self): self.c = self.Kind() self.c["artist"] = ["mu", "piman"] self.c["title"] = u"more fakes" def test_correct_len(self): self.failUnlessEqual(len(self.c), 3) def test_keys(self): self.failUnless("artist" in self.c.keys()) self.failUnless("title" in self.c.keys()) def test_values(self): self.failUnless(["mu", "piman"] in self.c.values()) self.failUnless(["more fakes"] in self.c.values()) def test_items(self): self.failUnless(("artist", ["mu", "piman"]) in self.c.items()) self.failUnless(("title", ["more fakes"]) in self.c.items()) def test_equal(self): self.failUnlessEqual(self.c, self.c) def test_get(self): self.failUnlessEqual(self.c["artist"], ["mu", "piman"]) self.failUnlessEqual(self.c["title"], ["more fakes"]) def test_set(self): self.c["woo"] = "bar" self.failUnlessEqual(self.c["woo"], ["bar"]) def test_del(self): del(self.c["title"]) self.failUnlessRaises(KeyError, self.c.__getitem__, "title") def test_contains(self): self.failIf("foo" in self.c) self.failUnless("title" in self.c) def test_get_case(self): self.failUnlessEqual(self.c["ARTIST"], ["mu", "piman"]) def test_set_case(self): self.c["TITLE"] = "another fake" self.failUnlessEqual(self.c["title"], ["another fake"]) def test_set_preserve_case(self): del(self.c["title"]) self.c["TiTlE"] = "blah" self.failUnless(("TiTlE", "blah") in list(self.c)) self.failUnless("title" in self.c) def test_contains_case(self): self.failUnless("TITLE" in self.c) def test_del_case(self): del(self.c["TITLE"]) self.failUnlessRaises(KeyError, self.c.__getitem__, "title") def test_get_failure(self): self.failUnlessRaises(KeyError, self.c.__getitem__, "woo") def test_del_failure(self): self.failUnlessRaises(KeyError, self.c.__delitem__, "woo") def test_roundtrip(self): self.failUnlessEqual(self.c, self.Kind(self.c.write())) def test_roundtrip_vc(self): self.failUnlessEqual(self.c, VComment(self.c.write())) def test_case_items_426(self): self.c.append(("WOO", "bar")) self.failUnless(("woo", ["bar"]) in self.c.items()) def test_empty(self): self.c = VCommentDict() self.failIf(self.c.keys()) self.failIf(self.c.values()) self.failIf(self.c.items()) def test_as_dict(self): d = self.c.as_dict() self.failUnless("artist" in d) self.failUnless("title" in d) self.failUnlessEqual(d["artist"], self.c["artist"]) self.failUnlessEqual(d["title"], self.c["title"]) def test_bad_key(self): self.failUnlessRaises(UnicodeError, self.c.get, u"\u1234") self.failUnlessRaises( UnicodeError, self.c.__setitem__, u"\u1234", "foo") self.failUnlessRaises( UnicodeError, self.c.__delitem__, u"\u1234") def test_duplicate_keys(self): self.c = VCommentDict() keys = ("key", "Key", "KEY") for key in keys: self.c.append((key, "value")) self.failUnlessEqual(len(self.c.keys()), 1) self.failUnlessEqual(len(self.c.as_dict()), 1) add(TVCommentDict) mutagen-1.22/tests/test_flac.py0000644000175000017500000004771412212322664017021 0ustar lazkalazka00000000000000 import shutil, os from tests import TestCase, add from mutagen.id3 import ID3, TIT2, ID3NoHeaderError from mutagen.flac import to_int_be, Padding, VCFLACDict, MetadataBlock, error from mutagen.flac import StreamInfo, SeekTable, CueSheet, FLAC, delete, Picture from tests.test__vorbis import TVCommentDict, VComment try: from os.path import devnull except ImportError: devnull = "/dev/null" class Tto_int_be(TestCase): def test_empty(self): self.failUnlessEqual(to_int_be(""), 0) def test_0(self): self.failUnlessEqual(to_int_be("\x00"), 0) def test_1(self): self.failUnlessEqual(to_int_be("\x01"), 1) def test_256(self): self.failUnlessEqual(to_int_be("\x01\x00"), 256) def test_long(self): self.failUnlessEqual(to_int_be("\x01\x00\x00\x00\x00"), 2**32) add(Tto_int_be) class TVCFLACDict(TVCommentDict): Kind = VCFLACDict def test_roundtrip_vc(self): self.failUnlessEqual(self.c, VComment(self.c.write() + "\x01")) add(TVCFLACDict) class TMetadataBlock(TestCase): def test_empty(self): self.failUnlessEqual(MetadataBlock("").write(), "") def test_not_empty(self): self.failUnlessEqual(MetadataBlock("foobar").write(), "foobar") def test_change(self): b = MetadataBlock("foobar") b.data = "quux" self.failUnlessEqual(b.write(), "quux") def test_writeblocks(self): blocks = [Padding("\x00" * 20), Padding("\x00" * 30)] self.failUnlessEqual(len(MetadataBlock.writeblocks(blocks)), 58) def test_ctr_garbage(self): self.failUnlessRaises(TypeError, StreamInfo, 12) def test_group_padding(self): blocks = [Padding("\x00" * 20), Padding("\x00" * 30), MetadataBlock("foobar")] blocks[-1].code = 0 length1 = len(MetadataBlock.writeblocks(blocks)) MetadataBlock.group_padding(blocks) length2 = len(MetadataBlock.writeblocks(blocks)) self.failUnlessEqual(length1, length2) self.failUnlessEqual(len(blocks), 2) add(TMetadataBlock) class TStreamInfo(TestCase): data = ('\x12\x00\x12\x00\x00\x00\x0e\x005\xea\n\xc4H\xf0\x00\xca0' '\x14(\x90\xf9\xe1)2\x13\x01\xd4\xa7\xa9\x11!8\xab\x91') data_invalid = len(data) * '\x00' def setUp(self): self.i = StreamInfo(self.data) def test_invalid(self): # http://code.google.com/p/mutagen/issues/detail?id=117 self.failUnlessRaises(error, StreamInfo, self.data_invalid) def test_blocksize(self): self.failUnlessEqual(self.i.max_blocksize, 4608) self.failUnlessEqual(self.i.min_blocksize, 4608) self.failUnless(self.i.min_blocksize <= self.i.max_blocksize) def test_framesize(self): self.failUnlessEqual(self.i.min_framesize, 14) self.failUnlessEqual(self.i.max_framesize, 13802) self.failUnless(self.i.min_framesize <= self.i.max_framesize) def test_sample_rate(self): self.failUnlessEqual(self.i.sample_rate, 44100) def test_channels(self): self.failUnlessEqual(self.i.channels, 5) def test_bps(self): self.failUnlessEqual(self.i.bits_per_sample, 16) def test_length(self): self.failUnlessAlmostEqual(self.i.length, 300.5, 1) def test_total_samples(self): self.failUnlessEqual(self.i.total_samples, 13250580) def test_md5_signature(self): self.failUnlessEqual(self.i.md5_signature, int("2890f9e129321301d4a7a9112138ab91", 16)) def test_eq(self): self.failUnlessEqual(self.i, self.i) def test_roundtrip(self): self.failUnlessEqual(StreamInfo(self.i.write()), self.i) add(TStreamInfo) class TSeekTable(TestCase): SAMPLE = os.path.join("tests", "data", "silence-44-s.flac") def setUp(self): self.flac = FLAC(self.SAMPLE) self.st = self.flac.seektable def test_seektable(self): self.failUnlessEqual(self.st.seekpoints, [(0, 0, 4608), (41472, 11852, 4608), (50688, 14484, 4608), (87552, 25022, 4608), (105984, 30284, 4608), (0xFFFFFFFFFFFFFFFF, 0, 0)]) def test_eq(self): self.failUnlessEqual(self.st, self.st) def test_neq(self): self.failIfEqual(self.st, 12) def test_repr(self): repr(self.st) def test_roundtrip(self): self.failUnlessEqual(SeekTable(self.st.write()), self.st) add(TSeekTable) class TCueSheet(TestCase): SAMPLE = os.path.join("tests", "data", "silence-44-s.flac") def setUp(self): self.flac = FLAC(self.SAMPLE) self.cs = self.flac.cuesheet def test_cuesheet(self): self.failUnlessEqual(self.cs.media_catalog_number, "1234567890123") self.failUnlessEqual(self.cs.lead_in_samples, 88200) self.failUnlessEqual(self.cs.compact_disc, True) self.failUnlessEqual(len(self.cs.tracks), 4) def test_first_track(self): self.failUnlessEqual(self.cs.tracks[0].track_number, 1) self.failUnlessEqual(self.cs.tracks[0].start_offset, 0) self.failUnlessEqual(self.cs.tracks[0].isrc, '123456789012') self.failUnlessEqual(self.cs.tracks[0].type, 0) self.failUnlessEqual(self.cs.tracks[0].pre_emphasis, False) self.failUnlessEqual(self.cs.tracks[0].indexes, [(1, 0)]) def test_second_track(self): self.failUnlessEqual(self.cs.tracks[1].track_number, 2) self.failUnlessEqual(self.cs.tracks[1].start_offset, 44100L) self.failUnlessEqual(self.cs.tracks[1].isrc, '') self.failUnlessEqual(self.cs.tracks[1].type, 1) self.failUnlessEqual(self.cs.tracks[1].pre_emphasis, True) self.failUnlessEqual(self.cs.tracks[1].indexes, [(1, 0), (2, 588)]) def test_lead_out(self): self.failUnlessEqual(self.cs.tracks[-1].track_number, 170) self.failUnlessEqual(self.cs.tracks[-1].start_offset, 162496) self.failUnlessEqual(self.cs.tracks[-1].isrc, '') self.failUnlessEqual(self.cs.tracks[-1].type, 0) self.failUnlessEqual(self.cs.tracks[-1].pre_emphasis, False) self.failUnlessEqual(self.cs.tracks[-1].indexes, []) def test_track_eq(self): track = self.cs.tracks[-1] self.assertReallyEqual(track, track) self.assertReallyNotEqual(track, 42) def test_eq(self): self.assertReallyEqual(self.cs, self.cs) def test_neq(self): self.assertReallyNotEqual(self.cs, 12) def test_repr(self): repr(self.cs) def test_roundtrip(self): self.failUnlessEqual(CueSheet(self.cs.write()), self.cs) add(TCueSheet) class TPicture(TestCase): SAMPLE = os.path.join("tests", "data", "silence-44-s.flac") def setUp(self): self.flac = FLAC(self.SAMPLE) self.p = self.flac.pictures[0] def test_count(self): self.failUnlessEqual(len(self.flac.pictures), 1) def test_picture(self): self.failUnlessEqual(self.p.width, 1) self.failUnlessEqual(self.p.height, 1) self.failUnlessEqual(self.p.depth, 24) self.failUnlessEqual(self.p.colors, 0) self.failUnlessEqual(self.p.mime, u'image/png') self.failUnlessEqual(self.p.desc, u'A pixel.') self.failUnlessEqual(self.p.type, 3) self.failUnlessEqual(len(self.p.data), 150) def test_eq(self): self.failUnlessEqual(self.p, self.p) def test_neq(self): self.failIfEqual(self.p, 12) def test_repr(self): repr(self.p) def test_roundtrip(self): self.failUnlessEqual(Picture(self.p.write()), self.p) add(TPicture) class TPadding(TestCase): def setUp(self): self.b = Padding("\x00" * 100) def test_padding(self): self.failUnlessEqual(self.b.write(), "\x00" * 100) def test_blank(self): self.failIf(Padding().write()) def test_empty(self): self.failIf(Padding("").write()) def test_repr(self): repr(Padding()) def test_change(self): self.b.length = 20 self.failUnlessEqual(self.b.write(), "\x00" * 20) add(TPadding) class TFLAC(TestCase): SAMPLE = os.path.join("tests", "data", "silence-44-s.flac") NEW = SAMPLE + ".new" def setUp(self): shutil.copy(self.SAMPLE, self.NEW) self.failUnlessEqual(open(self.SAMPLE).read(), open(self.NEW).read()) self.flac = FLAC(self.NEW) def test_delete(self): self.failUnless(self.flac.tags) self.flac.delete() self.failIf(self.flac.tags) flac = FLAC(self.NEW) self.failIf(flac.tags) def test_module_delete(self): delete(self.NEW) flac = FLAC(self.NEW) self.failIf(flac.tags) def test_info(self): self.failUnlessAlmostEqual(FLAC(self.NEW).info.length, 3.7, 1) def test_keys(self): self.failUnlessEqual(self.flac.keys(), self.flac.tags.keys()) def test_values(self): self.failUnlessEqual(self.flac.values(), self.flac.tags.values()) def test_items(self): self.failUnlessEqual(self.flac.items(), self.flac.tags.items()) def test_vc(self): self.failUnlessEqual(self.flac['title'][0], 'Silence') def test_write_nochange(self): f = FLAC(self.NEW) f.save() self.failUnlessEqual(open(self.SAMPLE).read(), open(self.NEW).read()) def test_write_changetitle(self): f = FLAC(self.NEW) f["title"] = "A New Title" f.save() f = FLAC(self.NEW) self.failUnlessEqual(f["title"][0], "A New Title") def test_write_changetitle_unicode_value(self): f = FLAC(self.NEW) f["title"] = u"A Unicode Title \u2022" f.save() f = FLAC(self.NEW) self.failUnlessEqual(f["title"][0], u"A Unicode Title \u2022") def test_write_changetitle_unicode_key(self): f = FLAC(self.NEW) f[u"title"] = "A New Title" f.save() f = FLAC(self.NEW) self.failUnlessEqual(f[u"title"][0], "A New Title") def test_write_changetitle_unicode_key_and_value(self): f = FLAC(self.NEW) f[u"title"] = u"A Unicode Title \u2022" f.save() f = FLAC(self.NEW) self.failUnlessEqual(f[u"title"][0], u"A Unicode Title \u2022") def test_force_grow(self): f = FLAC(self.NEW) f["faketag"] = ["a" * 1000] * 1000 f.save() f = FLAC(self.NEW) self.failUnlessEqual(f["faketag"], ["a" * 1000] * 1000) def test_force_shrink(self): self.test_force_grow() f = FLAC(self.NEW) f["faketag"] = "foo" f.save() f = FLAC(self.NEW) self.failUnlessEqual(f["faketag"], ["foo"]) def test_add_vc(self): f = FLAC(os.path.join("tests", "data", "no-tags.flac")) self.failIf(f.tags) f.add_tags() self.failUnless(f.tags == []) self.failUnlessRaises(ValueError, f.add_tags) def test_add_vc_implicit(self): f = FLAC(os.path.join("tests", "data", "no-tags.flac")) self.failIf(f.tags) f["foo"] = "bar" self.failUnless(f.tags == [("foo", "bar")]) self.failUnlessRaises(ValueError, f.add_tags) def test_ooming_vc_header(self): # issue 112: Malformed FLAC Vorbis header causes out of memory error # http://code.google.com/p/mutagen/issues/detail?id=112 self.assertRaises(IOError, FLAC, os.path.join('tests', 'data', 'ooming-header.flac')) def test_with_real_flac(self): if not have_flac: return self.flac["faketag"] = "foobar" * 1000 self.flac.save() badval = os.system("tools/notarealprogram 2> %s" % devnull) value = os.system("flac -t %s 2> %s" % (self.flac.filename, devnull)) self.failIf(value and value != badval) def test_save_unknown_block(self): block = MetadataBlock("test block data") block.code = 99 self.flac.metadata_blocks.append(block) self.flac.save() def test_load_unknown_block(self): self.test_save_unknown_block() flac = FLAC(self.NEW) self.failUnlessEqual(len(flac.metadata_blocks), 7) self.failUnlessEqual(flac.metadata_blocks[5].code, 99) self.failUnlessEqual(flac.metadata_blocks[5].data, "test block data") def test_two_vorbis_blocks(self): self.flac.metadata_blocks.append(self.flac.metadata_blocks[1]) self.flac.save() self.failUnlessRaises(IOError, FLAC, self.NEW) def test_missing_streaminfo(self): self.flac.metadata_blocks.pop(0) self.flac.save() self.failUnlessRaises(IOError, FLAC, self.NEW) def test_load_invalid_flac(self): self.failUnlessRaises( IOError, FLAC, os.path.join("tests", "data", "xing.mp3")) def test_save_invalid_flac(self): self.failUnlessRaises( IOError, self.flac.save, os.path.join("tests", "data", "xing.mp3")) def test_pprint(self): self.failUnless(self.flac.pprint()) def test_double_load(self): blocks = list(self.flac.metadata_blocks) self.flac.load(self.flac.filename) self.failUnlessEqual(blocks, self.flac.metadata_blocks) def test_seektable(self): self.failUnless(self.flac.seektable) def test_cuesheet(self): self.failUnless(self.flac.cuesheet) def test_pictures(self): self.failUnless(self.flac.pictures) def test_add_picture(self): f = FLAC(self.NEW) c = len(f.pictures) f.add_picture(Picture()) f.save() f = FLAC(self.NEW) self.failUnlessEqual(len(f.pictures), c + 1) def test_clear_pictures(self): f = FLAC(self.NEW) c1 = len(f.pictures) c2 = len(f.metadata_blocks) f.clear_pictures() f.save() f = FLAC(self.NEW) self.failUnlessEqual(len(f.metadata_blocks), c2 - c1) def test_ignore_id3(self): id3 = ID3() id3.add(TIT2(encoding=0, text='id3 title')) id3.save(self.NEW) f = FLAC(self.NEW) f['title'] = 'vc title' f.save() id3 = ID3(self.NEW) self.failUnlessEqual(id3['TIT2'].text, ['id3 title']) f = FLAC(self.NEW) self.failUnlessEqual(f['title'], ['vc title']) def test_delete_id3(self): id3 = ID3() id3.add(TIT2(encoding=0, text='id3 title')) id3.save(self.NEW, v1=2) f = FLAC(self.NEW) f['title'] = 'vc title' f.save(deleteid3=True) self.failUnlessRaises(ID3NoHeaderError, ID3, self.NEW) f = FLAC(self.NEW) self.failUnlessEqual(f['title'], ['vc title']) def test_mime(self): self.failUnless("audio/x-flac" in self.flac.mime) def test_variable_block_size(self): FLAC(os.path.join("tests", "data", "variable-block.flac")) def test_load_flac_with_application_block(self): FLAC(os.path.join("tests", "data", "flac_application.flac")) def tearDown(self): os.unlink(self.NEW) add(TFLAC) class TFLACFile(TestCase): def test_open_nonexistant(self): """mutagen 1.2 raises UnboundLocalError, then it tries to open non-existant FLAC files""" filename = os.path.join("tests", "data", "doesntexist.flac") self.assertRaises(IOError, FLAC, filename) add(TFLACFile) class TFLACBadBlockSize(TestCase): TOO_SHORT = os.path.join("tests", "data", "52-too-short-block-size.flac") TOO_SHORT_2 = os.path.join("tests", "data", "106-short-picture-block-size.flac") OVERWRITTEN = os.path.join("tests", "data", "52-overwritten-metadata.flac") INVAL_INFO = os.path.join("tests", "data", "106-invalid-streaminfo.flac") def test_too_short_read(self): flac = FLAC(self.TOO_SHORT) self.failUnlessEqual(flac["artist"], ["Tunng"]) def test_too_short_read_picture(self): flac = FLAC(self.TOO_SHORT_2) self.failUnlessEqual(flac.pictures[0].width, 10) def test_overwritten_read(self): flac = FLAC(self.OVERWRITTEN) self.failUnlessEqual(flac["artist"], ["Giora Feidman"]) def test_inval_streaminfo(self): self.assertRaises(error, FLAC, self.INVAL_INFO) add(TFLACBadBlockSize) class TFLACBadBlockSizeWrite(TestCase): TOO_SHORT = os.path.join("tests", "data", "52-too-short-block-size.flac") NEW = TOO_SHORT + ".new" def setUp(self): shutil.copy(self.TOO_SHORT, self.NEW) def test_write_reread(self): flac = FLAC(self.NEW) del(flac["artist"]) flac.save() flac2 = FLAC(self.NEW) self.failUnlessEqual(flac["title"], flac2["title"]) data = open(self.NEW, "rb").read(1024) self.failIf("Tunng" in data) def tearDown(self): os.unlink(self.NEW) add(TFLACBadBlockSizeWrite) class CVE20074619(TestCase): # Tests to ensure Mutagen is not vulnerable to a number of security # issues found in libFLAC. # http://research.eeye.com/html/advisories/published/AD20071115.html def test_1(self): # "Editing any Metadata Block Size value to a large value such # as 0xFFFFFFFF may result in a heap based overflow in the # decoding software." filename = os.path.join("tests", "data", "CVE-2007-4619-1.flac") self.failUnlessRaises(IOError, FLAC, filename) def test_2(self): # "The second vulnerability lies within the parsing of any # VORBIS Comment String Size fields. Settings this fields to # an overly large size, such as 0xFFFFFFF, could also result # in another heap-based overflow allowing arbitrary code to # execute in the content of the decoding program." filename = os.path.join("tests", "data", "CVE-2007-4619-2.flac") self.failUnlessRaises(IOError, FLAC, filename) # "By inserting an overly long VORBIS Comment data string along # with an large VORBIS Comment data string size value (such as # 0x000061A8 followed by 25,050 A's), applications that do not # properly apply boundary checks will result in a stack-based # buffer overflow." # # This is tested, among other places, in # test_save_grown_split_setup_packet_reference which saves a # comment field of 200K in size. # Vulnerabilities 4-10 are the same thing for the picture block. # Vulnerability 11 does not apply to Mutagen as it does not # download images when given a redirect MIME type. # "An overly large Padding length field value would set the basis # for another heap overflow inside a vulnerable application. By # setting this value to a large value such as 0xFFFFFFFF, a # malformed FLAC file could cause a heap based corruption scenario # when the memory for the Padding length is calculated without # proper bounds checks." # # We should raise an IOError when trying to write such large # blocks, or when reading blocks with an incorrect padding length. # Although, I do wonder about the correctness of this # vulnerability, since a padding length of 0xFFFFFFFF is # impossible to store in a FLAC file. def test_12_read(self): filename = os.path.join("tests", "data", "CVE-2007-4619-12.flac") self.failUnlessRaises(IOError, FLAC, filename) def test_12_write_too_big(self): filename = os.path.join("tests", "data", "silence-44-s.flac") f = FLAC(filename) # This size is too big to be an integer. f.metadata_blocks[-1].length = 0xFFFFFFFFFFFFFFFF self.failUnlessRaises(IOError, f.metadata_blocks[-1].write) def test_12_write_too_big_for_flac(self): from mutagen.flac import MetadataBlock filename = os.path.join("tests", "data", "silence-44-s.flac") f = FLAC(filename) # This size is too big to be in a FLAC block but is overwise fine. f.metadata_blocks[-1].length = 0x1FFFFFF self.failUnlessRaises( IOError, MetadataBlock.writeblocks, [f.metadata_blocks[-1]]) # Vulnerability 13 and 14 are specific to libFLAC and C/C++ memory # management schemes. add(CVE20074619) NOTFOUND = os.system("tools/notarealprogram 2> %s" % devnull) have_flac = True if os.system("flac 2> %s > %s" % (devnull, devnull)) == NOTFOUND: have_flac = False print "WARNING: Skipping FLAC reference tests." mutagen-1.22/tests/test_trueaudio.py0000644000175000017500000000301712211074441020075 0ustar lazkalazka00000000000000import os import shutil from mutagen.trueaudio import TrueAudio, delete from mutagen.id3 import TIT1 from tests import TestCase, add from tempfile import mkstemp class TTrueAudio(TestCase): def setUp(self): self.audio = TrueAudio(os.path.join("tests", "data", "empty.tta")) def test_tags(self): self.failUnless(self.audio.tags is None) def test_length(self): self.failUnlessAlmostEqual(self.audio.info.length, 3.7, 1) def test_sample_rate(self): self.failUnlessEqual(44100, self.audio.info.sample_rate) def test_not_my_file(self): filename = os.path.join("tests", "data", "empty.ogg") self.failUnlessRaises(IOError, TrueAudio, filename) def test_module_delete(self): delete(os.path.join("tests", "data", "empty.tta")) def test_delete(self): self.audio.delete() self.failIf(self.audio.tags) def test_pprint(self): self.failUnless(self.audio.pprint()) def test_save_reload(self): try: fd, filename = mkstemp(suffix='.tta') os.close(fd) shutil.copy(self.audio.filename, filename) audio = TrueAudio(filename) audio.add_tags() audio.tags.add(TIT1(encoding=0, text="A Title")) audio.save() audio = TrueAudio(filename) self.failUnlessEqual(audio["TIT1"], "A Title") finally: os.unlink(filename) def test_mime(self): self.failUnless("audio/x-tta" in self.audio.mime) add(TTrueAudio) mutagen-1.22/tests/test___init__.py0000644000175000017500000002034312211074367017643 0ustar lazkalazka00000000000000import os from StringIO import StringIO from tempfile import mkstemp import shutil from tests import TestCase, add from mutagen import File, Metadata, FileType from mutagen.oggvorbis import OggVorbis from mutagen.oggflac import OggFLAC from mutagen.oggspeex import OggSpeex from mutagen.oggtheora import OggTheora from mutagen.oggopus import OggOpus from mutagen.mp3 import MP3, EasyMP3 from mutagen.apev2 import APEv2File from mutagen.flac import FLAC from mutagen.wavpack import WavPack from mutagen.trueaudio import TrueAudio, EasyTrueAudio from mutagen.mp4 import MP4 from mutagen.musepack import Musepack from mutagen.monkeysaudio import MonkeysAudio from mutagen.optimfrog import OptimFROG from mutagen.asf import ASF try: from os.path import devnull except ImportError: devnull = "/dev/null" class TMetadata(TestCase): class FakeMeta(Metadata): def __init__(self): pass def test_virtual_constructor(self): self.failUnlessRaises(NotImplementedError, Metadata, "filename") def test_load(self): m = Metadata() self.failUnlessRaises(NotImplementedError, m.load, "filename") def test_virtual_save(self): self.failUnlessRaises(NotImplementedError, self.FakeMeta().save) self.failUnlessRaises( NotImplementedError, self.FakeMeta().save, "filename") def test_virtual_delete(self): self.failUnlessRaises(NotImplementedError, self.FakeMeta().delete) self.failUnlessRaises( NotImplementedError, self.FakeMeta().delete, "filename") add(TMetadata) class TFileType(TestCase): def setUp(self): self.vorbis = File(os.path.join("tests", "data", "empty.ogg")) def test_delitem_not_there(self): self.failUnlessRaises(KeyError, self.vorbis.__delitem__, "foobar") def test_add_tags(self): self.failUnlessRaises(NotImplementedError, FileType().add_tags) def test_delitem(self): self.vorbis["foobar"] = "quux" del(self.vorbis["foobar"]) self.failIf("quux" in self.vorbis) add(TFileType) class TFile(TestCase): def test_bad(self): try: self.failUnless(File(devnull) is None) except (OSError, IOError): print "WARNING: Unable to open %s." % devnull self.failUnless(File(__file__) is None) def test_empty(self): filename = os.path.join("tests", "data", "empty") open(filename, "wb").close() try: self.failUnless(File(filename) is None) finally: os.unlink(filename) def test_not_file(self): self.failUnlessRaises(EnvironmentError, File, "/dev/doesnotexist") def test_no_options(self): for filename in ["empty.ogg", "empty.oggflac", "silence-44-s.mp3"]: filename = os.path.join("tests", "data", "empty.ogg") self.failIf(File(filename, options=[])) def test_oggvorbis(self): self.failUnless(isinstance( File(os.path.join("tests", "data", "empty.ogg")), OggVorbis)) def test_oggflac(self): self.failUnless(isinstance( File(os.path.join("tests", "data", "empty.oggflac")), OggFLAC)) def test_oggspeex(self): self.failUnless(isinstance( File(os.path.join("tests", "data", "empty.spx")), OggSpeex)) def test_oggtheora(self): self.failUnless(isinstance(File( os.path.join("tests", "data", "sample.oggtheora")), OggTheora)) def test_oggopus(self): self.failUnless(isinstance(File( os.path.join("tests", "data", "example.opus")), OggOpus)) def test_mp3(self): self.failUnless(isinstance( File(os.path.join("tests", "data", "bad-xing.mp3")), MP3)) self.failUnless(isinstance( File(os.path.join("tests", "data", "xing.mp3")), MP3)) self.failUnless(isinstance( File(os.path.join("tests", "data", "silence-44-s.mp3")), MP3)) def test_easy_mp3(self): self.failUnless(isinstance( File(os.path.join("tests", "data", "silence-44-s.mp3"), easy=True), EasyMP3)) def test_flac(self): self.failUnless(isinstance( File(os.path.join("tests", "data", "silence-44-s.flac")), FLAC)) def test_musepack(self): self.failUnless(isinstance( File(os.path.join("tests", "data", "click.mpc")), Musepack)) self.failUnless(isinstance( File(os.path.join("tests", "data", "sv4_header.mpc")), Musepack)) self.failUnless(isinstance( File(os.path.join("tests", "data", "sv5_header.mpc")), Musepack)) self.failUnless(isinstance( File(os.path.join("tests", "data", "sv8_header.mpc")), Musepack)) def test_monkeysaudio(self): self.failUnless(isinstance( File(os.path.join("tests", "data", "mac-399.ape")), MonkeysAudio)) self.failUnless(isinstance( File(os.path.join("tests", "data", "mac-396.ape")), MonkeysAudio)) def test_apev2(self): self.failUnless(isinstance( File(os.path.join("tests", "data", "oldtag.apev2")), APEv2File)) def test_tta(self): self.failUnless(isinstance( File(os.path.join("tests", "data", "empty.tta")), TrueAudio)) def test_easy_tta(self): self.failUnless(isinstance( File(os.path.join("tests", "data", "empty.tta"), easy=True), EasyTrueAudio)) def test_wavpack(self): self.failUnless(isinstance( File(os.path.join("tests", "data", "silence-44-s.wv")), WavPack)) def test_mp4(self): self.failUnless(isinstance( File(os.path.join("tests", "data", "has-tags.m4a")), MP4)) self.failUnless(isinstance( File(os.path.join("tests", "data", "no-tags.m4a")), MP4)) self.failUnless(isinstance( File(os.path.join("tests", "data", "no-tags.3g2")), MP4)) self.failUnless(isinstance( File(os.path.join("tests", "data", "truncated-64bit.mp4")), MP4)) def test_optimfrog(self): self.failUnless(isinstance( File(os.path.join("tests", "data", "empty.ofr")), OptimFROG)) self.failUnless(isinstance( File(os.path.join("tests", "data", "empty.ofs")), OptimFROG)) def test_asf(self): self.failUnless(isinstance( File(os.path.join("tests", "data", "silence-1.wma")), ASF)) self.failUnless(isinstance( File(os.path.join("tests", "data", "silence-2.wma")), ASF)) self.failUnless(isinstance( File(os.path.join("tests", "data", "silence-3.wma")), ASF)) def test_id3_indicates_mp3_not_tta(self): header = "ID3 the rest of this is garbage" fileobj = StringIO(header) filename = "not-identifiable.ext" self.failUnless(TrueAudio.score(filename, fileobj, header) < MP3.score(filename, fileobj, header)) add(TFile) class TFileUpperExt(TestCase): FILES = [(os.path.join("tests", "data", "empty.ofr"), OptimFROG), (os.path.join("tests", "data", "sv5_header.mpc"), Musepack), (os.path.join("tests", "data", "silence-3.wma"), ASF), (os.path.join("tests", "data", "truncated-64bit.mp4"), MP4), (os.path.join("tests", "data", "silence-44-s.flac"), FLAC), ] def setUp(self): checks = [] for (original, instance) in self.FILES: ext = original.rsplit(".", 1)[-1] fd, filename = mkstemp(suffix='.'+ext.upper()) os.close(fd) shutil.copy(original, filename) checks.append((filename, instance)) self.checks = checks def test_case_insensitive_ext(self): for (path, instance) in self.checks: self.failUnless(isinstance(File(path, options=[instance]), instance)) def tearDown(self): for (path, instance) in self.checks: os.unlink(path) add(TFileUpperExt) class TModuleImportAll(TestCase): def test_all(self): import mutagen files = os.listdir(mutagen.__path__[0]) modules = [os.path.splitext(f)[0] for f in files] modules = [f for f in modules if not f.startswith("_")] for module in modules: mod = getattr(__import__("mutagen." + module), module) for attr in getattr(mod, "__all__", []): getattr(mod, attr) add(TModuleImportAll) mutagen-1.22/tests/test_id3.py0000644000175000017500000016232312212327214016561 0ustar lazkalazka00000000000000import os; from os.path import join import shutil from tests import TestCase from tests import add from mutagen import id3 from mutagen.id3 import ID3, COMR, Frames, Frames_2_2, ID3Warning, ID3JunkFrameError from StringIO import StringIO import warnings warnings.simplefilter('error', ID3Warning) _22 = ID3(); _22.version = (2,2,0) _23 = ID3(); _23.version = (2,3,0) _24 = ID3(); _24.version = (2,4,0) class ID3GetSetDel(TestCase): def setUp(self): self.i = ID3() self.i["BLAH"] = 1 self.i["QUUX"] = 2 self.i["FOOB:ar"] = 3 self.i["FOOB:az"] = 4 def test_getnormal(self): self.assertEquals(self.i.getall("BLAH"), [1]) self.assertEquals(self.i.getall("QUUX"), [2]) self.assertEquals(self.i.getall("FOOB:ar"), [3]) self.assertEquals(self.i.getall("FOOB:az"), [4]) def test_getlist(self): self.assert_(self.i.getall("FOOB") in [[3, 4], [4, 3]]) def test_delnormal(self): self.assert_("BLAH" in self.i) self.i.delall("BLAH") self.assert_("BLAH" not in self.i) def test_delone(self): self.i.delall("FOOB:ar") self.assertEquals(self.i.getall("FOOB"), [4]) def test_delall(self): self.assert_("FOOB:ar" in self.i) self.assert_("FOOB:az" in self.i) self.i.delall("FOOB") self.assert_("FOOB:ar" not in self.i) self.assert_("FOOB:az" not in self.i) def test_setone(self): class TEST(object): HashKey = "FOOB:ar" t = TEST() self.i.setall("FOOB", [t]) self.assertEquals(self.i["FOOB:ar"], t) self.assertEquals(self.i.getall("FOOB"), [t]) def test_settwo(self): class TEST(object): HashKey = "FOOB:ar" t = TEST() t2 = TEST(); t2.HashKey = "FOOB:az" self.i.setall("FOOB", [t, t2]) self.assertEquals(self.i["FOOB:ar"], t) self.assertEquals(self.i["FOOB:az"], t2) self.assert_(self.i.getall("FOOB") in [[t, t2], [t2, t]]) class ID3Loading(TestCase): empty = join('tests', 'data', 'emptyfile.mp3') silence = join('tests', 'data', 'silence-44-s.mp3') unsynch = join('tests', 'data', 'id3v23_unsynch.id3') def test_empty_file(self): name = self.empty self.assertRaises(ValueError, ID3, filename=name) #from_name = ID3(name) #obj = open(name, 'rb') #from_obj = ID3(fileobj=obj) #self.assertEquals(from_name, from_explicit_name) #self.assertEquals(from_name, from_obj) def test_nonexistent_file(self): name = join('tests', 'data', 'does', 'not', 'exist') self.assertRaises(EnvironmentError, ID3, name) def test_header_empty(self): id3 = ID3() id3._ID3__fileobj = open(self.empty, 'rb') self.assertRaises(EOFError, id3._ID3__load_header) def test_header_silence(self): id3 = ID3() id3._ID3__fileobj = open(self.silence, 'rb') id3._ID3__load_header() self.assertEquals(id3.version, (2,3,0)) self.assertEquals(id3.size, 1314) def test_header_2_4_invalid_flags(self): id3 = ID3() id3._ID3__fileobj = StringIO('ID3\x04\x00\x1f\x00\x00\x00\x00') self.assertRaises(ValueError, id3._ID3__load_header) def test_header_2_4_unsynch_size(self): id3 = ID3() id3._ID3__fileobj = StringIO('ID3\x04\x00\x10\x00\x00\x00\xFF') self.assertRaises(ValueError, id3._ID3__load_header) def test_header_2_4_allow_footer(self): id3 = ID3() id3._ID3__fileobj = StringIO('ID3\x04\x00\x10\x00\x00\x00\x00') id3._ID3__load_header() def test_header_2_3_invalid_flags(self): id3 = ID3() id3._ID3__fileobj = StringIO('ID3\x03\x00\x1f\x00\x00\x00\x00') self.assertRaises(ValueError, id3._ID3__load_header) id3._ID3__fileobj = StringIO('ID3\x03\x00\x0f\x00\x00\x00\x00') self.assertRaises(ValueError, id3._ID3__load_header) def test_header_2_2(self): id3 = ID3() id3._ID3__fileobj = StringIO('ID3\x02\x00\x00\x00\x00\x00\x00') id3._ID3__load_header() self.assertEquals(id3.version, (2,2,0)) def test_header_2_1(self): id3 = ID3() id3._ID3__fileobj = StringIO('ID3\x01\x00\x00\x00\x00\x00\x00') self.assertRaises(NotImplementedError, id3._ID3__load_header) def test_header_too_small(self): id3 = ID3() id3._ID3__fileobj = StringIO('ID3\x01\x00\x00\x00\x00\x00') self.assertRaises(EOFError, id3._ID3__load_header) def test_header_2_4_extended(self): id3 = ID3() id3._ID3__fileobj = StringIO( 'ID3\x04\x00\x40\x00\x00\x00\x00\x00\x00\x00\x05\x5a') id3._ID3__load_header() self.assertEquals(id3._ID3__extsize, 1) self.assertEquals(id3._ID3__extdata, '\x5a') def test_header_2_4_extended_unsynch_size(self): id3 = ID3() id3._ID3__fileobj = StringIO( 'ID3\x04\x00\x40\x00\x00\x00\x00\x00\x00\x00\xFF\x5a') self.assertRaises(ValueError, id3._ID3__load_header) def test_header_2_4_extended_but_not(self): id3 = ID3() id3._ID3__fileobj = StringIO( 'ID3\x04\x00\x40\x00\x00\x00\x00TIT1\x00\x00\x00\x01a') id3._ID3__load_header() self.assertEquals(id3._ID3__extsize, 0) self.assertEquals(id3._ID3__extdata, '') def test_header_2_4_extended_but_not_but_not_tag(self): id3 = ID3() id3._ID3__fileobj = StringIO( 'ID3\x04\x00\x40\x00\x00\x00\x00TIT9') self.failUnlessRaises(EOFError, id3._ID3__load_header) def test_header_2_3_extended(self): id3 = ID3() id3._ID3__fileobj = StringIO( 'ID3\x03\x00\x40\x00\x00\x00\x00\x00\x00\x00\x06' '\x00\x00\x56\x78\x9a\xbc') id3._ID3__load_header() self.assertEquals(id3._ID3__extsize, 6) self.assertEquals(id3._ID3__extdata, '\x00\x00\x56\x78\x9a\xbc') def test_unsynch(self): id3 = ID3() id3.version = (2,4,0) id3._ID3__flags = 0x80 badsync = '\x00\xff\x00ab\x00' self.assertEquals( id3._ID3__load_framedata(Frames["TPE2"], 0, badsync), [u"\xffab"]) id3._ID3__flags = 0x00 self.assertEquals(id3._ID3__load_framedata( Frames["TPE2"], 0x02, badsync), [u"\xffab"]) tag = id3._ID3__load_framedata(Frames["TPE2"], 0, badsync) self.assertEquals(tag, [u"\xff", u"ab"]) def test_load_v23_unsynch(self): id3 = ID3(self.unsynch) self.assertEquals(id3["TPE1"], ["Nina Simone"]) def test_insane__ID3__fullread(self): id3 = ID3() id3._ID3__filesize = 0 self.assertRaises(ValueError, id3._ID3__fullread, -3) self.assertRaises(EOFError, id3._ID3__fullread, 3) class Issue21(TestCase): # Files with bad extended header flags failed to read tags. # Ensure the extended header is turned off, and the frames are # read. def setUp(self): self.id3 = ID3(join('tests', 'data', 'issue_21.id3')) def test_no_ext(self): self.failIf(self.id3.f_extended) def test_has_tags(self): self.failUnless("TIT2" in self.id3) self.failUnless("TALB" in self.id3) def test_tit2_value(self): self.failUnlessEqual(self.id3["TIT2"].text, [u"Punk To Funk"]) add(Issue21) class ID3Tags(TestCase): def setUp(self): self.silence = join('tests', 'data', 'silence-44-s.mp3') def test_None(self): id3 = ID3(self.silence, known_frames={}) self.assertEquals(0, len(id3.keys())) self.assertEquals(9, len(id3.unknown_frames)) def test_has_docs(self): for Kind in Frames.values() + Frames_2_2.values(): self.failUnless(Kind.__doc__, "%s has no docstring" % Kind) def test_23(self): id3 = ID3(self.silence) self.assertEquals(8, len(id3.keys())) self.assertEquals(0, len(id3.unknown_frames)) self.assertEquals('Quod Libet Test Data', id3['TALB']) self.assertEquals('Silence', str(id3['TCON'])) self.assertEquals('Silence', str(id3['TIT1'])) self.assertEquals('Silence', str(id3['TIT2'])) self.assertEquals(3000, +id3['TLEN']) self.assertNotEquals(['piman','jzig'], id3['TPE1']) self.assertEquals('02/10', id3['TRCK']) self.assertEquals(2, +id3['TRCK']) self.assertEquals('2004', id3['TDRC']) def test_23_multiframe_hack(self): class ID3hack(ID3): "Override 'correct' behavior with desired behavior" def loaded_frame(self, tag): if tag.HashKey in self: self[tag.HashKey].extend(tag[:]) else: self[tag.HashKey] = tag id3 = ID3hack(self.silence) self.assertEquals(8, len(id3.keys())) self.assertEquals(0, len(id3.unknown_frames)) self.assertEquals('Quod Libet Test Data', id3['TALB']) self.assertEquals('Silence', str(id3['TCON'])) self.assertEquals('Silence', str(id3['TIT1'])) self.assertEquals('Silence', str(id3['TIT2'])) self.assertEquals(3000, +id3['TLEN']) self.assertEquals(['piman','jzig'], id3['TPE1']) self.assertEquals('02/10', id3['TRCK']) self.assertEquals(2, +id3['TRCK']) self.assertEquals('2004', id3['TDRC']) def test_badencoding(self): self.assertRaises(IndexError, Frames["TPE1"].fromData, _24, 0, "\x09ab") self.assertRaises(ValueError, Frames["TPE1"], encoding=9, text="ab") def test_badsync(self): self.assertRaises( ValueError, Frames["TPE1"].fromData, _24, 0x02, "\x00\xff\xfe") def test_noencrypt(self): self.assertRaises( NotImplementedError, Frames["TPE1"].fromData, _24, 0x04, "\x00") self.assertRaises( NotImplementedError, Frames["TPE1"].fromData, _23, 0x40, "\x00") def test_badcompress(self): self.assertRaises( ValueError, Frames["TPE1"].fromData, _24, 0x08, "\x00\x00\x00\x00#") self.assertRaises( ValueError, Frames["TPE1"].fromData, _23, 0x80, "\x00\x00\x00\x00#") def test_junkframe(self): self.assertRaises(ValueError, Frames["TPE1"].fromData, _24, 0, "") def test_bad_sylt(self): self.assertRaises( ID3JunkFrameError, Frames["SYLT"].fromData, _24, 0x0, "\x00eng\x01description\x00foobar") self.assertRaises( ID3JunkFrameError, Frames["SYLT"].fromData, _24, 0x0, "\x00eng\x01description\x00foobar\x00\xFF\xFF\xFF") def test_extradata(self): from mutagen.id3 import RVRB, RBUF self.assertRaises(ID3Warning, RVRB()._readData, 'L1R1BBFFFFPP#xyz') self.assertRaises(ID3Warning, RBUF()._readData, '\x00\x01\x00\x01\x00\x00\x00\x00#xyz') class ID3v1Tags(TestCase): def setUp(self): self.silence = join('tests', 'data', 'silence-44-s-v1.mp3') self.id3 = ID3(self.silence) def test_album(self): self.assertEquals('Quod Libet Test Data', self.id3['TALB']) def test_genre(self): self.assertEquals('Darkwave', self.id3['TCON'].genres[0]) def test_title(self): self.assertEquals('Silence', str(self.id3['TIT2'])) def test_artist(self): self.assertEquals(['piman'], self.id3['TPE1']) def test_track(self): self.assertEquals('2', self.id3['TRCK']) self.assertEquals(2, +self.id3['TRCK']) def test_year(self): self.assertEquals('2004', self.id3['TDRC']) def test_v1_not_v11(self): from mutagen.id3 import MakeID3v1, ParseID3v1, TRCK self.id3["TRCK"] = TRCK(encoding=0, text="32") tag = MakeID3v1(self.id3) self.failUnless(32, ParseID3v1(tag)["TRCK"]) del(self.id3["TRCK"]) tag = MakeID3v1(self.id3) tag = tag[:125] + ' ' + tag[-1] self.failIf("TRCK" in ParseID3v1(tag)) def test_nulls(self): from mutagen.id3 import ParseID3v1 s = 'TAG%(title)30s%(artist)30s%(album)30s%(year)4s%(cmt)29s\x03\x01' s = s % dict(artist='abcd\00fg', title='hijklmn\x00p', album='qrst\x00v', cmt='wxyz', year='1224') tags = ParseID3v1(s) self.assertEquals('abcd'.decode('latin1'), tags['TPE1']) self.assertEquals('hijklmn'.decode('latin1'), tags['TIT2']) self.assertEquals('qrst'.decode('latin1'), tags['TALB']) def test_nonascii(self): from mutagen.id3 import ParseID3v1 s = 'TAG%(title)30s%(artist)30s%(album)30s%(year)4s%(cmt)29s\x03\x01' s = s % dict(artist='abcd\xe9fg', title='hijklmn\xf3p', album='qrst\xfcv', cmt='wxyz', year='1234') tags = ParseID3v1(s) self.assertEquals('abcd\xe9fg'.decode('latin1'), tags['TPE1']) self.assertEquals('hijklmn\xf3p'.decode('latin1'), tags['TIT2']) self.assertEquals('qrst\xfcv'.decode('latin1'), tags['TALB']) self.assertEquals('wxyz', tags['COMM']) self.assertEquals("3", tags['TRCK']) self.assertEquals("1234", tags['TDRC']) def test_roundtrip(self): from mutagen.id3 import ParseID3v1, MakeID3v1 frames = {} for key in ["TIT2", "TALB", "TPE1", "TDRC"]: frames[key] = self.id3[key] self.assertEquals(ParseID3v1(MakeID3v1(frames)), frames) def test_make_from_empty(self): from mutagen.id3 import MakeID3v1, TCON, COMM empty = 'TAG' + '\x00' * 124 + '\xff' self.assertEquals(MakeID3v1({}), empty) self.assertEquals(MakeID3v1({'TCON': TCON()}), empty) self.assertEquals( MakeID3v1({'COMM': COMM(encoding=0, text="")}), empty) def test_make_v1_from_tyer(self): from mutagen.id3 import ParseID3v1, MakeID3v1, TYER, TDRC self.assertEquals( MakeID3v1({"TDRC": TDRC(text="2010-10-10")}), MakeID3v1({"TYER": TYER(text="2010")})) self.assertEquals( ParseID3v1(MakeID3v1({"TDRC": TDRC(text="2010-10-10")})), ParseID3v1(MakeID3v1({"TYER": TYER(text="2010")}))) def test_invalid(self): from mutagen.id3 import ParseID3v1 self.failUnless(ParseID3v1("") is None) def test_invalid_track(self): from mutagen.id3 import ParseID3v1, MakeID3v1, TRCK tag = {} tag["TRCK"] = TRCK(encoding=0, text="not a number") v1tag = MakeID3v1(tag) self.failIf("TRCK" in ParseID3v1(v1tag)) def test_v1_genre(self): from mutagen.id3 import ParseID3v1, MakeID3v1, TCON tag = {} tag["TCON"] = TCON(encoding=0, text="Pop") v1tag = MakeID3v1(tag) self.failUnlessEqual(ParseID3v1(v1tag)["TCON"].genres, ["Pop"]) class TestWriteID3v1(TestCase): SILENCE = os.path.join("tests", "data", "silence-44-s.mp3") def setUp(self): from tempfile import mkstemp fd, self.filename = mkstemp(suffix='.mp3') os.close(fd) shutil.copy(self.SILENCE, self.filename) self.audio = ID3(self.filename) def failIfV1(self): fileobj = open(self.filename, "rb") fileobj.seek(-128, 2) self.failIf(fileobj.read(3) == "TAG") def failUnlessV1(self): fileobj = open(self.filename, "rb") fileobj.seek(-128, 2) self.failUnless(fileobj.read(3) == "TAG") def test_save_delete(self): self.audio.save(v1=0) self.failIfV1() def test_save_add(self): self.audio.save(v1=2) self.failUnlessV1() def test_save_defaults(self): self.audio.save(v1=0) self.failIfV1() self.audio.save(v1=1) self.failIfV1() self.audio.save(v1=2) self.failUnlessV1() self.audio.save(v1=1) self.failUnlessV1() def tearDown(self): os.unlink(self.filename) add(TestWriteID3v1) class TestV22Tags(TestCase): def setUp(self): filename = os.path.join("tests", "data", "id3v22-test.mp3") self.tags = ID3(filename) def test_tags(self): self.failUnless(self.tags["TRCK"].text == ["3/11"]) self.failUnless(self.tags["TPE1"].text == ["Anais Mitchell"]) add(TestV22Tags) def TestReadTags(): tests = [ ['TALB', '\x00a/b', 'a/b', '', dict(encoding=0)], ['TBPM', '\x00120', '120', 120, dict(encoding=0)], ['TCMP', '\x001', '1', 1, dict(encoding=0)], ['TCMP', '\x000', '0', 0, dict(encoding=0)], ['TCOM', '\x00a/b', 'a/b', '', dict(encoding=0)], ['TCON', '\x00(21)Disco', '(21)Disco', '', dict(encoding=0)], ['TCOP', '\x001900 c', '1900 c', '', dict(encoding=0)], ['TDAT', '\x00a/b', 'a/b', '', dict(encoding=0)], ['TDEN', '\x001987', '1987', '', dict(encoding=0, year=[1987])], ['TDOR', '\x001987-12', '1987-12', '', dict(encoding=0, year=[1987], month=[12])], ['TDRC', '\x001987\x00', '1987', '', dict(encoding=0, year=[1987])], ['TDRL', '\x001987\x001988', '1987,1988', '', dict(encoding=0, year=[1987,1988])], ['TDTG', '\x001987', '1987', '', dict(encoding=0, year=[1987])], ['TDLY', '\x001205', '1205', 1205, dict(encoding=0)], ['TENC', '\x00a b/c d', 'a b/c d', '', dict(encoding=0)], ['TEXT', '\x00a b\x00c d', ['a b', 'c d'], '', dict(encoding=0)], ['TFLT', '\x00MPG/3', 'MPG/3', '', dict(encoding=0)], ['TIME', '\x001205', '1205', '', dict(encoding=0)], ['TIPL', '\x02\x00a\x00\x00\x00b', [["a", "b"]], '', dict(encoding=2)], ['TIT1', '\x00a/b', 'a/b', '', dict(encoding=0)], # TIT2 checks misaligned terminator '\x00\x00' across crosses utf16 chars ['TIT2', '\x01\xff\xfe\x38\x00\x00\x38', u'8\u3800', '', dict(encoding=1)], ['TIT3', '\x00a/b', 'a/b', '', dict(encoding=0)], ['TKEY', '\x00A#m', 'A#m', '', dict(encoding=0)], ['TLAN', '\x006241', '6241', '', dict(encoding=0)], ['TLEN', '\x006241', '6241', 6241, dict(encoding=0)], ['TMCL', '\x02\x00a\x00\x00\x00b', [["a", "b"]], '', dict(encoding=2)], ['TMED', '\x00med', 'med', '', dict(encoding=0)], ['TMOO', '\x00moo', 'moo', '', dict(encoding=0)], ['TOAL', '\x00alb', 'alb', '', dict(encoding=0)], ['TOFN', '\x0012 : bar', '12 : bar', '', dict(encoding=0)], ['TOLY', '\x00lyr', 'lyr', '', dict(encoding=0)], ['TOPE', '\x00own/lic', 'own/lic', '', dict(encoding=0)], ['TORY', '\x001923', '1923', 1923, dict(encoding=0)], ['TOWN', '\x00own/lic', 'own/lic', '', dict(encoding=0)], ['TPE1', '\x00ab', ['ab'], '', dict(encoding=0)], ['TPE2', '\x00ab\x00cd\x00ef', ['ab','cd','ef'], '', dict(encoding=0)], ['TPE3', '\x00ab\x00cd', ['ab','cd'], '', dict(encoding=0)], ['TPE4', '\x00ab\x00', ['ab'], '', dict(encoding=0)], ['TPOS', '\x0008/32', '08/32', 8, dict(encoding=0)], ['TPRO', '\x00pro', 'pro', '', dict(encoding=0)], ['TPUB', '\x00pub', 'pub', '', dict(encoding=0)], ['TRCK', '\x004/9', '4/9', 4, dict(encoding=0)], ['TRDA', '\x00Sun Jun 12', 'Sun Jun 12', '', dict(encoding=0)], ['TRSN', '\x00ab/cd', 'ab/cd', '', dict(encoding=0)], ['TRSO', '\x00ab', 'ab', '', dict(encoding=0)], ['TSIZ', '\x0012345', '12345', 12345, dict(encoding=0)], ['TSOA', '\x00ab', 'ab', '', dict(encoding=0)], ['TSOP', '\x00ab', 'ab', '', dict(encoding=0)], ['TSOT', '\x00ab', 'ab', '', dict(encoding=0)], ['TSO2', '\x00ab', 'ab', '', dict(encoding=0)], ['TSOC', '\x00ab', 'ab', '', dict(encoding=0)], ['TSRC', '\x0012345', '12345', '', dict(encoding=0)], ['TSSE', '\x0012345', '12345', '', dict(encoding=0)], ['TSST', '\x0012345', '12345', '', dict(encoding=0)], ['TYER', '\x002004', '2004', 2004, dict(encoding=0)], ['TXXX', '\x00usr\x00a/b\x00c', ['a/b','c'], '', dict(encoding=0, desc='usr')], ['WCOM', 'http://foo', 'http://foo', '', {}], ['WCOP', 'http://bar', 'http://bar', '', {}], ['WOAF', 'http://baz', 'http://baz', '', {}], ['WOAR', 'http://bar', 'http://bar', '', {}], ['WOAS', 'http://bar', 'http://bar', '', {}], ['WORS', 'http://bar', 'http://bar', '', {}], ['WPAY', 'http://bar', 'http://bar', '', {}], ['WPUB', 'http://bar', 'http://bar', '', {}], ['WXXX', '\x00usr\x00http', 'http', '', dict(encoding=0, desc='usr')], ['IPLS', '\x00a\x00A\x00b\x00B\x00', [['a','A'],['b','B']], '', dict(encoding=0)], ['MCDI', '\x01\x02\x03\x04', '\x01\x02\x03\x04', '', {}], ['ETCO', '\x01\x12\x00\x00\x7f\xff', [(18, 32767)], '', dict(format=1)], ['COMM', '\x00ENUT\x00Com', 'Com', '', dict(desc='T', lang='ENU', encoding=0)], # found in a real MP3 ['COMM', '\x00\x00\xcc\x01\x00 ', ' ', '', dict(desc=u'', lang='\x00\xcc\x01', encoding=0)], ['APIC', '\x00-->\x00\x03cover\x00cover.jpg', 'cover.jpg', '', dict(mime='-->', type=3, desc='cover', encoding=0)], ['USER', '\x00ENUCom', 'Com', '', dict(lang='ENU', encoding=0)], ['RVA2', 'testdata\x00\x01\xfb\x8c\x10\x12\x23', 'Master volume: -2.2266 dB/0.1417', '', dict(desc='testdata', channel=1, gain=-2.22656, peak=0.14169)], ['RVA2', 'testdata\x00\x01\xfb\x8c\x24\x01\x22\x30\x00\x00', 'Master volume: -2.2266 dB/0.1417', '', dict(desc='testdata', channel=1, gain=-2.22656, peak=0.14169)], ['RVA2', 'testdata2\x00\x01\x04\x01\x00', 'Master volume: +2.0020 dB/0.0000', '', dict(desc='testdata2', channel=1, gain=2.001953125, peak=0)], ['PCNT', '\x00\x00\x00\x11', 17, 17, dict(count=17)], ['POPM', 'foo@bar.org\x00\xde\x00\x00\x00\x11', 222, 222, dict(email="foo@bar.org", rating=222, count=17)], ['POPM', 'foo@bar.org\x00\xde\x00', 222, 222, dict(email="foo@bar.org", rating=222, count=0)], # Issue #33 - POPM may have no playcount at all. ['POPM', 'foo@bar.org\x00\xde', 222, 222, dict(email="foo@bar.org", rating=222)], ['UFID', 'own\x00data', 'data', '', dict(data='data', owner='own')], ['UFID', 'own\x00\xdd', '\xdd', '', dict(data='\xdd', owner='own')], ['GEOB', '\x00mime\x00name\x00desc\x00data', 'data', '', dict(encoding=0, mime='mime', filename='name', desc='desc')], ['USLT', '\x00engsome lyrics\x00woo\nfun', 'woo\nfun', '', dict(encoding=0, lang='eng', desc='some lyrics', text='woo\nfun')], ['SYLT', ('\x00eng\x02\x01some lyrics\x00foo\x00\x00\x00\x00\x01bar' '\x00\x00\x00\x00\x10'), "foobar", '', dict(encoding=0, lang='eng', type=1, format=2, desc='some lyrics')], ['POSS', '\x01\x0f', 15, 15, dict(format=1, position=15)], ['OWNE', '\x00USD10.01\x0020041010CDBaby', 'CDBaby', 'CDBaby', dict(encoding=0, price="USD10.01", date='20041010', seller='CDBaby')], ['PRIV', 'a@b.org\x00random data', 'random data', 'random data', dict(owner='a@b.org', data='random data')], ['PRIV', 'a@b.org\x00\xdd', '\xdd', '\xdd', dict(owner='a@b.org', data='\xdd')], ['SIGN', '\x92huh?', 'huh?', 'huh?', dict(group=0x92, sig='huh?')], ['ENCR', 'a@b.org\x00\x92Data!', 'Data!', 'Data!', dict(owner='a@b.org', method=0x92, data='Data!')], ['SEEK', '\x00\x12\x00\x56', 0x12*256*256+0x56, 0x12*256*256+0x56, dict(offset=0x12*256*256+0x56)], ['SYTC', "\x01\x10obar", '\x10obar', '', dict(format=1, data='\x10obar')], ['RBUF', '\x00\x12\x00', 0x12*256, 0x12*256, dict(size=0x12*256)], ['RBUF', '\x00\x12\x00\x01', 0x12*256, 0x12*256, dict(size=0x12*256, info=1)], ['RBUF', '\x00\x12\x00\x01\x00\x00\x00\x23', 0x12*256, 0x12*256, dict(size=0x12*256, info=1, offset=0x23)], ['RVRB', '\x12\x12\x23\x23\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11', (0x12*256+0x12, 0x23*256+0x23), '', dict(left=0x12*256+0x12, right=0x23*256+0x23) ], ['AENC', 'a@b.org\x00\x00\x12\x00\x23', 'a@b.org', 'a@b.org', dict(owner='a@b.org', preview_start=0x12, preview_length=0x23)], ['AENC', 'a@b.org\x00\x00\x12\x00\x23!', 'a@b.org', 'a@b.org', dict(owner='a@b.org', preview_start=0x12, preview_length=0x23, data='!')], ['GRID', 'a@b.org\x00\x99', 'a@b.org', 0x99, dict(owner='a@b.org', group=0x99)], ['GRID', 'a@b.org\x00\x99data', 'a@b.org', 0x99, dict(owner='a@b.org', group=0x99, data='data')], ['COMR', '\x00USD10.00\x0020051010ql@sc.net\x00\x09Joe\x00A song\x00' 'x-image/fake\x00some data', COMR(encoding=0, price="USD10.00", valid_until="20051010", contact="ql@sc.net", format=9, seller="Joe", desc="A song", mime='x-image/fake', logo='some data'), '', dict( encoding=0, price="USD10.00", valid_until="20051010", contact="ql@sc.net", format=9, seller="Joe", desc="A song", mime='x-image/fake', logo='some data')], ['COMR', '\x00USD10.00\x0020051010ql@sc.net\x00\x09Joe\x00A song\x00', COMR(encoding=0, price="USD10.00", valid_until="20051010", contact="ql@sc.net", format=9, seller="Joe", desc="A song"), '', dict( encoding=0, price="USD10.00", valid_until="20051010", contact="ql@sc.net", format=9, seller="Joe", desc="A song")], ['MLLT', '\x00\x01\x00\x00\x02\x00\x00\x03\x04\x08foobar', 'foobar', '', dict(frames=1, bytes=2, milliseconds=3, bits_for_bytes=4, bits_for_milliseconds=8, data='foobar')], ['EQU2', '\x00Foobar\x00\x01\x01\x04\x00', [(128.5, 2.0)], '', dict(method=0, desc="Foobar")], ['ASPI', '\x00\x00\x00\x00\x00\x00\x00\x10\x00\x03\x08\x01\x02\x03', [1, 2, 3], '', dict(S=0, L=16, N=3, b=8)], ['ASPI', '\x00\x00\x00\x00\x00\x00\x00\x10\x00\x03\x10' '\x00\x01\x00\x02\x00\x03', [1, 2, 3], '', dict(S=0, L=16, N=3, b=16)], ['LINK', 'TIT1http://www.example.org/TIT1.txt\x00', ("TIT1", 'http://www.example.org/TIT1.txt'), '', dict(frameid='TIT1', url='http://www.example.org/TIT1.txt')], ['LINK', 'COMMhttp://www.example.org/COMM.txt\x00engfoo', ("COMM", 'http://www.example.org/COMM.txt', 'engfoo'), '', dict(frameid='COMM', url='http://www.example.org/COMM.txt', data='engfoo')], # iTunes podcast frames ['TGID', '\x00i', u'i', '', dict(encoding=0)], ['TDES', '\x00ii', u'ii', '', dict(encoding=0)], ['WFED', 'http://zzz', 'http://zzz', '', {}], # 2.2 tags ['UFI', 'own\x00data', 'data', '', dict(data='data', owner='own')], ['SLT', ('\x00eng\x02\x01some lyrics\x00foo\x00\x00\x00\x00\x01bar' '\x00\x00\x00\x00\x10'), "foobar", '', dict(encoding=0, lang='eng', type=1, format=2, desc='some lyrics')], ['TT1', '\x00ab\x00', 'ab', '', dict(encoding=0)], ['TT2', '\x00ab', 'ab', '', dict(encoding=0)], ['TT3', '\x00ab', 'ab', '', dict(encoding=0)], ['TP1', '\x00ab\x00', 'ab', '', dict(encoding=0)], ['TP2', '\x00ab', 'ab', '', dict(encoding=0)], ['TP3', '\x00ab', 'ab', '', dict(encoding=0)], ['TP4', '\x00ab', 'ab', '', dict(encoding=0)], ['TCM', '\x00ab/cd', 'ab/cd', '', dict(encoding=0)], ['TXT', '\x00lyr', 'lyr', '', dict(encoding=0)], ['TLA', '\x00ENU', 'ENU', '', dict(encoding=0)], ['TCO', '\x00gen', 'gen', '', dict(encoding=0)], ['TAL', '\x00alb', 'alb', '', dict(encoding=0)], ['TPA', '\x001/9', '1/9', 1, dict(encoding=0)], ['TRK', '\x002/8', '2/8', 2, dict(encoding=0)], ['TRC', '\x00isrc', 'isrc', '', dict(encoding=0)], ['TYE', '\x001900', '1900', 1900, dict(encoding=0)], ['TDA', '\x002512', '2512', '', dict(encoding=0)], ['TIM', '\x001225', '1225', '', dict(encoding=0)], ['TRD', '\x00Jul 17', 'Jul 17', '', dict(encoding=0)], ['TMT', '\x00DIG/A', 'DIG/A', '', dict(encoding=0)], ['TFT', '\x00MPG/3', 'MPG/3', '', dict(encoding=0)], ['TBP', '\x00133', '133', 133, dict(encoding=0)], ['TCP', '\x001', '1', 1, dict(encoding=0)], ['TCP', '\x000', '0', 0, dict(encoding=0)], ['TCR', '\x00Me', 'Me', '', dict(encoding=0)], ['TPB', '\x00Him', 'Him', '', dict(encoding=0)], ['TEN', '\x00Lamer', 'Lamer', '', dict(encoding=0)], ['TSS', '\x00ab', 'ab', '', dict(encoding=0)], ['TOF', '\x00ab:cd', 'ab:cd', '', dict(encoding=0)], ['TLE', '\x0012', '12', 12, dict(encoding=0)], ['TSI', '\x0012', '12', 12, dict(encoding=0)], ['TDY', '\x0012', '12', 12, dict(encoding=0)], ['TKE', '\x00A#m', 'A#m', '', dict(encoding=0)], ['TOT', '\x00org', 'org', '', dict(encoding=0)], ['TOA', '\x00org', 'org', '', dict(encoding=0)], ['TOL', '\x00org', 'org', '', dict(encoding=0)], ['TOR', '\x001877', '1877', 1877, dict(encoding=0)], ['TXX', '\x00desc\x00val', 'val', '', dict(encoding=0, desc='desc')], ['WAF', 'http://zzz', 'http://zzz', '', {}], ['WAR', 'http://zzz', 'http://zzz', '', {}], ['WAS', 'http://zzz', 'http://zzz', '', {}], ['WCM', 'http://zzz', 'http://zzz', '', {}], ['WCP', 'http://zzz', 'http://zzz', '', {}], ['WPB', 'http://zzz', 'http://zzz', '', {}], ['WXX', '\x00desc\x00http', 'http', '', dict(encoding=0, desc='desc')], ['IPL', '\x00a\x00A\x00b\x00B\x00', [['a','A'],['b','B']], '', dict(encoding=0)], ['MCI', '\x01\x02\x03\x04', '\x01\x02\x03\x04', '', {}], ['ETC', '\x01\x12\x00\x00\x7f\xff', [(18, 32767)], '', dict(format=1)], ['COM', '\x00ENUT\x00Com', 'Com', '', dict(desc='T', lang='ENU', encoding=0)], ['PIC', '\x00-->\x03cover\x00cover.jpg', 'cover.jpg', '', dict(mime='-->', type=3, desc='cover', encoding=0)], ['POP', 'foo@bar.org\x00\xde\x00\x00\x00\x11', 222, 222, dict(email="foo@bar.org", rating=222, count=17)], ['CNT', '\x00\x00\x00\x11', 17, 17, dict(count=17)], ['GEO', '\x00mime\x00name\x00desc\x00data', 'data', '', dict(encoding=0, mime='mime', filename='name', desc='desc')], ['ULT', '\x00engsome lyrics\x00woo\nfun', 'woo\nfun', '', dict(encoding=0, lang='eng', desc='some lyrics', text='woo\nfun')], ['BUF', '\x00\x12\x00', 0x12*256, 0x12*256, dict(size=0x12*256)], ['CRA', 'a@b.org\x00\x00\x12\x00\x23', 'a@b.org', 'a@b.org', dict(owner='a@b.org', preview_start=0x12, preview_length=0x23)], ['CRA', 'a@b.org\x00\x00\x12\x00\x23!', 'a@b.org', 'a@b.org', dict(owner='a@b.org', preview_start=0x12, preview_length=0x23, data='!')], ['REV', '\x12\x12\x23\x23\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11', (0x12*256+0x12, 0x23*256+0x23), '', dict(left=0x12*256+0x12, right=0x23*256+0x23) ], ['STC', "\x01\x10obar", '\x10obar', '', dict(format=1, data='\x10obar')], ['MLL', '\x00\x01\x00\x00\x02\x00\x00\x03\x04\x08foobar', 'foobar', '', dict(frames=1, bytes=2, milliseconds=3, bits_for_bytes=4, bits_for_milliseconds=8, data='foobar')], ['LNK', 'TT1http://www.example.org/TIT1.txt\x00', ("TT1", 'http://www.example.org/TIT1.txt'), '', dict(frameid='TT1', url='http://www.example.org/TIT1.txt')], ['CRM', 'foo@example.org\x00test\x00woo', 'woo', '', dict(owner='foo@example.org', desc='test', data='woo')], ] load_tests = {} repr_tests = {} write_tests = {} for i, (tag, data, value, intval, info) in enumerate(tests): info = info.copy() def test_tag(self, tag=tag, data=data, value=value, intval=intval, info=info): from operator import pos id3 = __import__('mutagen.id3', globals(), locals(), [tag]) TAG = getattr(id3, tag) tag = TAG.fromData(_23, 0, data) self.failUnless(tag.HashKey) self.failUnless(tag.pprint()) self.assertEquals(value, tag) if 'encoding' not in info: self.assertRaises(AttributeError, getattr, tag, 'encoding') for attr, value in info.iteritems(): t = tag if not isinstance(value, list): value = [value] t = [t] for value, t in zip(value, iter(t)): if isinstance(value, float): self.failUnlessAlmostEqual(value, getattr(t, attr), 5) else: self.assertEquals(value, getattr(t, attr)) if isinstance(intval, (int,long)): self.assertEquals(intval, pos(t)) else: self.assertRaises(TypeError, pos, t) load_tests['test_%s_%d' % (tag, i)] = test_tag def test_tag_repr(self, tag=tag, data=data): from mutagen.id3 import ID3TimeStamp id3 = __import__('mutagen.id3', globals(), locals(), [tag]) TAG = getattr(id3, tag) tag = TAG.fromData(_23, 0, data) tag2 = eval(repr(tag), {TAG.__name__:TAG, 'ID3TimeStamp':ID3TimeStamp}) self.assertEquals(type(tag), type(tag2)) for spec in TAG._framespec: attr = spec.name self.assertEquals(getattr(tag, attr), getattr(tag2, attr)) # test __str__, __unicode__ self.assertTrue(isinstance(tag.__str__(), str)) if hasattr(tag, "__unicode__"): self.assertTrue(isinstance(tag.__unicode__(), unicode)) repr_tests['test_repr_%s_%d' % (tag, i)] = test_tag_repr def test_tag_write(self, tag=tag, data=data): id3 = __import__('mutagen.id3', globals(), locals(), [tag]) TAG = getattr(id3, tag) tag = TAG.fromData(_24, 0, data) towrite = tag._writeData() tag2 = TAG.fromData(_24, 0, towrite) for spec in TAG._framespec: attr = spec.name self.assertEquals(getattr(tag, attr), getattr(tag2, attr)) write_tests['test_write_%s_%d' % (tag, i)] = test_tag_write testcase = type('TestReadTags', (TestCase,), load_tests) add(testcase) testcase = type('TestReadReprTags', (TestCase,), repr_tests) add(testcase) testcase = type('TestReadWriteTags', (TestCase,), write_tests) add(testcase) from mutagen.id3 import Frames, Frames_2_2 check = dict.fromkeys(Frames.keys() + Frames_2_2.keys()) tested_tags = dict.fromkeys([row[0] for row in tests]) for tag in check: def check(self, tag=tag): self.assert_(tag in tested_tags) tested_tags['test_' + tag + '_tested'] = check testcase = type('TestTestedTags', (TestCase,), tested_tags) add(testcase) TestReadTags() del TestReadTags class UpdateTo24(TestCase): def test_pic(self): from mutagen.id3 import PIC id3 = ID3() id3.version = (2, 2) id3.add(PIC(encoding=0, mime="PNG", desc="cover", type=3, data="")) id3.update_to_v24() self.failUnlessEqual(id3["APIC:cover"].mime, "image/png") def test_tyer(self): from mutagen.id3 import TYER id3 = ID3() id3.version = (2, 3) id3.add(TYER(encoding=0, text="2006")) id3.update_to_v24() self.failUnlessEqual(id3["TDRC"], "2006") def test_tyer_tdat(self): from mutagen.id3 import TYER, TDAT id3 = ID3() id3.version = (2, 3) id3.add(TYER(encoding=0, text="2006")) id3.add(TDAT(encoding=0, text="0603")) id3.update_to_v24() self.failUnlessEqual(id3["TDRC"], "2006-03-06") def test_tyer_tdat_time(self): from mutagen.id3 import TYER, TDAT, TIME id3 = ID3() id3.version = (2, 3) id3.add(TYER(encoding=0, text="2006")) id3.add(TDAT(encoding=0, text="0603")) id3.add(TIME(encoding=0, text="1127")) id3.update_to_v24() self.failUnlessEqual(id3["TDRC"], "2006-03-06 11:27:00") def test_tory(self): from mutagen.id3 import TORY id3 = ID3() id3.version = (2, 3) id3.add(TORY(encoding=0, text="2006")) id3.update_to_v24() self.failUnlessEqual(id3["TDOR"], "2006") def test_ipls(self): from mutagen.id3 import IPLS id3 = ID3() id3.version = (2, 3) id3.add(IPLS(encoding=0, people=[["a", "b"], ["c", "d"]])) id3.update_to_v24() self.failUnlessEqual(id3["TIPL"], [["a", "b"], ["c", "d"]]) def test_dropped(self): from mutagen.id3 import TIME id3 = ID3() id3.version = (2, 3) id3.add(TIME(encoding=0, text=["1155"])) id3.update_to_v24() self.assertFalse(id3.getall("TIME")) add(UpdateTo24) class Issue97_UpgradeUnknown23(TestCase): SILENCE = os.path.join("tests", "data", "97-unknown-23-update.mp3") def setUp(self): from tempfile import mkstemp fd, self.filename = mkstemp(suffix='.mp3') os.close(fd) shutil.copy(self.SILENCE, self.filename) def test_unknown(self): from mutagen.id3 import TPE1 orig = ID3(self.filename) self.failUnlessEqual(orig.version, (2, 3, 0)) # load a 2.3 file and pretend we don't support TIT2 unknown = ID3(self.filename, known_frames={"TPE1": TPE1}, translate=False) # TIT2 ends up in unknown_frames self.failUnlessEqual(unknown.unknown_frames[0][:4], "TIT2") # frame should be different now orig_unknown = unknown.unknown_frames[0] unknown.update_to_v24() self.failIfEqual(unknown.unknown_frames[0], orig_unknown) # save as 2.4 unknown.save() # load again with support for TIT2, all should be there again new = ID3(self.filename) self.failUnlessEqual(new.version, (2, 4, 0)) self.failUnlessEqual(new["TIT2"].text, orig["TIT2"].text) self.failUnlessEqual(new["TPE1"].text, orig["TPE1"].text) def test_double_update(self): from mutagen.id3 import TPE1 unknown = ID3(self.filename, known_frames={"TPE1": TPE1}) # Make sure the data doesn't get updated again unknown.update_to_v24() unknown.unknown_frames = ["foobar"] unknown.update_to_v24() self.failUnless(unknown.unknown_frames) def test_unkown_invalid(self): f = ID3(self.filename, translate=False) f.unknown_frames = ["foobar", "\xff"*50] # throw away invalid frames f.update_to_v24() self.failIf(f.unknown_frames) def tearDown(self): os.unlink(self.filename) add(Issue97_UpgradeUnknown23) class BrokenDiscarded(TestCase): def test_empty(self): from mutagen.id3 import TPE1, ID3JunkFrameError self.assertRaises(ID3JunkFrameError, TPE1.fromData, _24, 0x00, '') def test_wacky_truncated_RVA2(self): from mutagen.id3 import RVA2, ID3JunkFrameError data = '\x01{\xf0\x10\xff\xff\x00' self.assertRaises(ID3JunkFrameError, RVA2.fromData, _24, 0x00, data) def test_bad_number_of_bits_RVA2(self): from mutagen.id3 import RVA2, ID3JunkFrameError data = '\x00\x00\x01\xe6\xfc\x10{\xd7' self.assertRaises(ID3JunkFrameError, RVA2.fromData, _24, 0x00, data) def test_drops_truncated_frames(self): from mutagen.id3 import Frames id3 = ID3() tail = '\x00\x00\x00\x03\x00\x00' '\x01\x02\x03' for head in 'RVA2 TXXX APIC'.split(): data = head + tail self.assertEquals( 0, len(list(id3._ID3__read_frames(data, Frames)))) def test_drops_nonalphanum_frames(self): from mutagen.id3 import Frames id3 = ID3() tail = '\x00\x00\x00\x03\x00\x00' '\x01\x02\x03' for head in ['\x06\xaf\xfe\x20', 'ABC\x00', 'A ']: data = head + tail self.assertEquals( 0, len(list(id3._ID3__read_frames(data, Frames)))) def test_bad_unicodedecode(self): from mutagen.id3 import COMM, ID3JunkFrameError # 7 bytes of "UTF16" data. data = '\x01\x00\x00\x00\xff\xfe\x00\xff\xfeh\x00' self.assertRaises(ID3JunkFrameError, COMM.fromData, _24, 0x00, data) class BrokenButParsed(TestCase): def test_missing_encoding(self): from mutagen.id3 import TIT2 tag = TIT2.fromData(_23, 0x00, 'a test') self.assertEquals(0, tag.encoding) self.assertEquals('a test', tag) self.assertEquals(['a test'], tag) self.assertEquals(['a test'], tag.text) def test_zerolength_framedata(self): from mutagen.id3 import Frames id3 = ID3() tail = '\x00' * 6 for head in 'WOAR TENC TCOP TOPE WXXX'.split(): data = head + tail self.assertEquals( 0, len(list(id3._ID3__read_frames(data, Frames)))) def test_lengthone_utf16(self): from mutagen.id3 import TPE1 tpe1 = TPE1.fromData(_24, 0, '\x01\x00') self.assertEquals(u'', tpe1) tpe1 = TPE1.fromData(_24, 0, '\x01\x00\x00\x00\x00') self.assertEquals([u'', u''], tpe1) def test_fake_zlib_pedantic(self): from mutagen.id3 import TPE1, Frame, ID3BadCompressedData id3 = ID3() id3.PEDANTIC = True self.assertRaises(ID3BadCompressedData, TPE1.fromData, id3, Frame.FLAG24_COMPRESS, '\x03abcdefg') def test_zlib_bpi(self): from mutagen.id3 import TPE1 id3 = ID3() tpe1 = TPE1(encoding=0, text="a" * (0xFFFF - 2)) data = id3._ID3__save_frame(tpe1) datalen_size = data[4 + 4 + 2:4 + 4 + 2 + 4] self.failIf( max(datalen_size) >= '\x80', "data is not syncsafe: %r" % data) def test_fake_zlib_nopedantic(self): from mutagen.id3 import TPE1, Frame id3 = ID3() id3.PEDANTIC = False tpe1 = TPE1.fromData(id3, Frame.FLAG24_COMPRESS, '\x03abcdefg') self.assertEquals(u'abcdefg', tpe1) def test_ql_0_12_missing_uncompressed_size(self): from mutagen.id3 import TPE1 tag = TPE1.fromData(_24, 0x08, 'x\x9cc\xfc\xff\xaf\x84!\x83!\x93' '\xa1\x98A\x01J&2\xe83\x940\xa4\x02\xd9%\x0c\x00\x87\xc6\x07#') self.assertEquals(tag.encoding, 1) self.assertEquals(tag, ['this is a/test']) def test_zlib_latin1_missing_datalen(self): from mutagen.id3 import TPE1 tag = TPE1.fromData(_24, 0x8, '\x00\x00\x00\x0f' 'x\x9cc(\xc9\xc8,V\x00\xa2D\xfd\x92\xd4\xe2\x12\x00&\x7f\x05%') self.assertEquals(tag.encoding, 0) self.assertEquals(tag, ['this is a/test']) def test_detect_23_ints_in_24_frames(self): from mutagen.id3 import Frames head = 'TIT1\x00\x00\x01\x00\x00\x00\x00' tail = 'TPE1\x00\x00\x00\x04\x00\x00Yay!' tagsgood = list(_24._ID3__read_frames(head + 'a'*127 + tail, Frames)) tagsbad = list(_24._ID3__read_frames(head + 'a'*255 + tail, Frames)) self.assertEquals(2, len(tagsgood)) self.assertEquals(2, len(tagsbad)) self.assertEquals('a'*127, tagsgood[0]) self.assertEquals('a'*255, tagsbad[0]) self.assertEquals('Yay!', tagsgood[1]) self.assertEquals('Yay!', tagsbad[1]) tagsgood = list(_24._ID3__read_frames(head + 'a'*127, Frames)) tagsbad = list(_24._ID3__read_frames(head + 'a'*255, Frames)) self.assertEquals(1, len(tagsgood)) self.assertEquals(1, len(tagsbad)) self.assertEquals('a'*127, tagsgood[0]) self.assertEquals('a'*255, tagsbad[0]) class OddWrites(TestCase): silence = join('tests', 'data', 'silence-44-s.mp3') newsilence = join('tests', 'data', 'silence-written.mp3') def setUp(self): shutil.copy(self.silence, self.newsilence) def test_toemptyfile(self): os.unlink(self.newsilence) open(self.newsilence, "wb").close() ID3(self.silence).save(self.newsilence) def test_tononfile(self): os.unlink(self.newsilence) ID3(self.silence).save(self.newsilence) def test_1bfile(self): os.unlink(self.newsilence) f = open(self.newsilence, "wb") f.write("!") f.close() ID3(self.silence).save(self.newsilence) self.assert_(os.path.getsize(self.newsilence) > 1) self.assertEquals(open(self.newsilence, "rb").read()[-1], "!") def tearDown(self): try: os.unlink(self.newsilence) except OSError: pass class WriteRoundtrip(TestCase): silence = join('tests', 'data', 'silence-44-s.mp3') newsilence = join('tests', 'data', 'silence-written.mp3') def setUp(self): shutil.copy(self.silence, self.newsilence) def test_same(self): ID3(self.newsilence).save() id3 = ID3(self.newsilence) self.assertEquals(id3["TALB"], "Quod Libet Test Data") self.assertEquals(id3["TCON"], "Silence") self.assertEquals(id3["TIT2"], "Silence") self.assertEquals(id3["TPE1"], ["jzig"]) def test_same_v23(self): id3 = ID3(self.newsilence, v2_version=3) id3.save(v2_version=3) id3 = ID3(self.newsilence) self.assertEqual(id3.version, (2, 3, 0)) self.assertEquals(id3["TALB"], "Quod Libet Test Data") self.assertEquals(id3["TCON"], "Silence") self.assertEquals(id3["TIT2"], "Silence") self.assertEquals(id3["TPE1"], "jzig") def test_addframe(self): from mutagen.id3 import TIT3 f = ID3(self.newsilence) self.assert_("TIT3" not in f) f["TIT3"] = TIT3(encoding=0, text="A subtitle!") f.save() id3 = ID3(self.newsilence) self.assertEquals(id3["TIT3"], "A subtitle!") def test_changeframe(self): f = ID3(self.newsilence) self.assertEquals(f["TIT2"], "Silence") f["TIT2"].text = [u"The sound of silence."] f.save() id3 = ID3(self.newsilence) self.assertEquals(id3["TIT2"], "The sound of silence.") def test_replaceframe(self): from mutagen.id3 import TPE1 f = ID3(self.newsilence) self.assertEquals(f["TPE1"], "jzig") f["TPE1"] = TPE1(encoding=0, text=u"jzig\x00piman") f.save() id3 = ID3(self.newsilence) self.assertEquals(id3["TPE1"], ["jzig", "piman"]) def test_compressibly_large(self): from mutagen.id3 import TPE2 f = ID3(self.newsilence) self.assert_("TPE2" not in f) f["TPE2"] = TPE2(encoding=0, text="Ab" * 1025) f.save() id3 = ID3(self.newsilence) self.assertEquals(id3["TPE2"], "Ab" * 1025) def test_nofile_emptytag(self): os.unlink(self.newsilence) ID3().save(self.newsilence) self.assertRaises(EnvironmentError, open, self.newsilence) def test_nofile_silencetag(self): id3 = ID3(self.newsilence) os.unlink(self.newsilence) id3.save(self.newsilence) self.assertEquals('ID3', open(self.newsilence).read(3)) self.test_same() def test_emptyfile_silencetag(self): id3 = ID3(self.newsilence) open(self.newsilence, 'wb').truncate() id3.save(self.newsilence) self.assertEquals('ID3', open(self.newsilence).read(3)) self.test_same() def test_empty_plustag_minustag_empty(self): id3 = ID3(self.newsilence) open(self.newsilence, 'wb').truncate() id3.save() id3.delete() self.failIf(id3) self.assertEquals(open(self.newsilence).read(10), '') def test_empty_plustag_emptytag_empty(self): id3 = ID3(self.newsilence) open(self.newsilence, 'wb').truncate() id3.save() id3.clear() id3.save() self.assertEquals(open(self.newsilence).read(10), '') def test_delete_invalid_zero(self): f = open(self.newsilence, 'wb') f.write('ID3\x04\x00\x00\x00\x00\x00\x00abc') f.close() ID3(self.newsilence).delete() self.assertEquals(open(self.newsilence).read(10), 'abc') def test_frame_order(self): from mutagen.id3 import TIT2, APIC, TALB, COMM f = ID3(self.newsilence) f["TIT2"] = TIT2(encoding=0, text="A title!") f["APIC"] = APIC(encoding=0, mime="b", type=3, desc='', data="a") f["TALB"] = TALB(encoding=0, text="c") f["COMM"] = COMM(encoding=0, desc="x", text="y") f.save() data = open(self.newsilence, 'rb').read() self.assert_(data.find("TIT2") < data.find("APIC")) self.assert_(data.find("TIT2") < data.find("COMM")) self.assert_(data.find("TALB") < data.find("APIC")) self.assert_(data.find("TALB") < data.find("COMM")) self.assert_(data.find("TIT2") < data.find("TALB")) def tearDown(self): try: os.unlink(self.newsilence) except EnvironmentError: pass class WriteForEyeD3(TestCase): silence = join('tests', 'data', 'silence-44-s.mp3') newsilence = join('tests', 'data', 'silence-written.mp3') def setUp(self): shutil.copy(self.silence, self.newsilence) # remove ID3v1 tag f = open(self.newsilence, "rb+") f.seek(-128, 2) f.truncate() f.close() def test_same(self): ID3(self.newsilence).save() id3 = eyeD3.tag.Tag(eyeD3.ID3_V2_4) id3.link(self.newsilence) self.assertEquals(id3.frames["TALB"][0].text, "Quod Libet Test Data") self.assertEquals(id3.frames["TCON"][0].text, "Silence") self.assertEquals(id3.frames["TIT2"][0].text, "Silence") # "piman" should have been cleared self.assertEquals(len(id3.frames["TPE1"]), 1) self.assertEquals(id3.frames["TPE1"][0].text, "jzig") def test_addframe(self): from mutagen.id3 import TIT3 f = ID3(self.newsilence) self.assert_("TIT3" not in f) f["TIT3"] = TIT3(encoding=0, text="A subtitle!") f.save() id3 = eyeD3.tag.Tag(eyeD3.ID3_V2_4) id3.link(self.newsilence) self.assertEquals(id3.frames["TIT3"][0].text, "A subtitle!") def test_changeframe(self): f = ID3(self.newsilence) self.assertEquals(f["TIT2"], "Silence") f["TIT2"].text = [u"The sound of silence."] f.save() id3 = eyeD3.tag.Tag(eyeD3.ID3_V2_4) id3.link(self.newsilence) self.assertEquals(id3.frames["TIT2"][0].text, "The sound of silence.") def tearDown(self): os.unlink(self.newsilence) class BadTYER(TestCase): filename = join('tests', 'data', 'bad-TYER-frame.mp3') def setUp(self): self.audio = ID3(self.filename) def test_no_year(self): self.failIf("TYER" in self.audio) def test_has_title(self): self.failUnless("TIT2" in self.audio) def tearDown(self): del(self.audio) class BadPOPM(TestCase): filename = join('tests', 'data', 'bad-POPM-frame.mp3') newfilename = join('tests', 'data', 'bad-POPM-frame-written.mp3') def setUp(self): shutil.copy(self.filename, self.newfilename) def tearDown(self): try: os.unlink(self.newfilename) except EnvironmentError: pass def test_read_popm_long_counter(self): f = ID3(self.newfilename) self.failUnless("POPM:Windows Media Player 9 Series" in f) popm = f["POPM:Windows Media Player 9 Series"] self.assertEquals(popm.rating, 255) self.assertEquals(popm.count, 2709193061) def test_write_popm_long_counter(self): from mutagen.id3 import POPM f = ID3(self.newfilename) f.add(POPM(email="foo@example.com", rating=125, count=2**32+1)) f.save() f = ID3(self.newfilename) self.failUnless("POPM:foo@example.com" in f) self.failUnless("POPM:Windows Media Player 9 Series" in f) popm = f["POPM:foo@example.com"] self.assertEquals(popm.rating, 125) self.assertEquals(popm.count, 2**32+1) class Issue69_BadV1Year(TestCase): def test_missing_year(self): from mutagen.id3 import ParseID3v1 tag = ParseID3v1('ABCTAGhello world\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff') self.failUnlessEqual(tag["TIT2"], "hello world") def test_short_year(self): from mutagen.id3 import ParseID3v1 tag = ParseID3v1('XTAGhello world\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff') self.failUnlessEqual(tag["TIT2"], "hello world") self.failUnlessEqual(tag["TDRC"], "0001") def test_none(self): from mutagen.id3 import ParseID3v1, MakeID3v1 s = MakeID3v1(dict()) self.failUnlessEqual(len(s), 128) tag = ParseID3v1(s) self.failIf("TDRC" in tag) def test_empty(self): from mutagen.id3 import ParseID3v1, MakeID3v1 s = MakeID3v1(dict(TDRC="")) self.failUnlessEqual(len(s), 128) tag = ParseID3v1(s) self.failIf("TDRC" in tag) def test_short(self): from mutagen.id3 import ParseID3v1, MakeID3v1 s = MakeID3v1(dict(TDRC="1")) self.failUnlessEqual(len(s), 128) tag = ParseID3v1(s) self.failUnlessEqual(tag["TDRC"], "0001") def test_long(self): from mutagen.id3 import ParseID3v1, MakeID3v1 s = MakeID3v1(dict(TDRC="123456789")) self.failUnlessEqual(len(s), 128) tag = ParseID3v1(s) self.failUnlessEqual(tag["TDRC"], "1234") class UpdateTo23(TestCase): def test_tdrc(self): tags = ID3() tags.add(id3.TDRC(encoding=1, text="2003-04-05 12:03")) tags.update_to_v23() self.failUnlessEqual(tags["TYER"].text, ["2003"]) self.failUnlessEqual(tags["TDAT"].text, ["0504"]) self.failUnlessEqual(tags["TIME"].text, ["1203"]) def test_tdor(self): tags = ID3() tags.add(id3.TDOR(encoding=1, text="2003-04-05 12:03")) tags.update_to_v23() self.failUnlessEqual(tags["TORY"].text, ["2003"]) def test_genre_from_v24_1(self): tags = ID3() tags.add(id3.TCON(encoding=1, text=["4","Rock"])) tags.update_to_v23() self.failUnlessEqual(tags["TCON"].text, ["Disco", "Rock"]) def test_genre_from_v24_2(self): tags = ID3() tags.add(id3.TCON(encoding=1, text=["RX", "3", "CR"])) tags.update_to_v23() self.failUnlessEqual(tags["TCON"].text, ["Remix", "Dance", "Cover"]) def test_genre_from_v23_1(self): tags = ID3() tags.add(id3.TCON(encoding=1, text=["(4)Rock"])) tags.update_to_v23() self.failUnlessEqual(tags["TCON"].text, ["Disco", "Rock"]) def test_genre_from_v23_2(self): tags = ID3() tags.add(id3.TCON(encoding=1, text=["(RX)(3)(CR)"])) tags.update_to_v23() self.failUnlessEqual(tags["TCON"].text, ["Remix", "Dance", "Cover"]) def test_ipls(self): tags = ID3() tags.version = (2, 3) tags.add(id3.TIPL(encoding=0, people=[["a", "b"], ["c", "d"]])) tags.add(id3.TMCL(encoding=0, people=[["e", "f"], ["g", "h"]])) tags.update_to_v23() self.failUnlessEqual(tags["IPLS"], [["a", "b"], ["c", "d"], ["e", "f"], ["g", "h"]]) class WriteTo23(TestCase): SILENCE = os.path.join("tests", "data", "silence-44-s.mp3") def setUp(self): from tempfile import mkstemp fd, self.filename = mkstemp(suffix='.mp3') os.close(fd) shutil.copy(self.SILENCE, self.filename) self.audio = ID3(self.filename) def tearDown(self): os.unlink(self.filename) def test_update_to_v23_on_load(self): from mutagen.id3 import TSOT self.audio.add(TSOT(text=["Ha"], encoding=3)) self.audio.save() # update_to_v23 called id3 = ID3(self.filename, v2_version=3) self.assertFalse(id3.getall("TSOT")) # update_to_v23 not called id3 = ID3(self.filename, v2_version=3, translate=False) self.assertTrue(id3.getall("TSOT")) def test_load_save_inval_version(self): self.assertRaises(ValueError, self.audio.save, v2_version=5) self.assertRaises(ValueError, ID3, self.filename, v2_version=5) def test_save(self): strings = ["one", "two", "three"] from mutagen.id3 import TPE1 self.audio.add(TPE1(text=strings, encoding=3)) self.audio.save(v2_version=3) frame = self.audio["TPE1"] self.assertEqual(frame.encoding, 3) self.assertEqual(frame.text, strings) id3 = ID3(self.filename, translate=False) self.assertEqual(id3.version, (2, 3, 0)) frame = id3["TPE1"] self.assertEqual(frame.encoding, 1) self.assertEqual(frame.text, ["/".join(strings)]) # null separator, mutagen can still read it self.audio.save(v2_version=3, v23_sep=None) id3 = ID3(self.filename, translate=False) self.assertEqual(id3.version, (2, 3, 0)) frame = id3["TPE1"] self.assertEqual(frame.encoding, 1) self.assertEqual(frame.text, strings) def test_save_off_spec_frames(self): # These are not defined in v2.3 and shouldn't be written. # Still make sure reading them again works and the encoding # is at least changed from mutagen.id3 import TDEN, TIPL dates = ["2013", "2014"] frame = TDEN(text=dates, encoding=3) self.audio.add(frame) tipl_frame = TIPL(people=[("a", "b"), ("c", "d")], encoding=2) self.audio.add(tipl_frame) self.audio.save(v2_version=3) id3 = ID3(self.filename, translate=False) self.assertEqual(id3.version, (2, 3, 0)) self.assertEqual([stamp.text for stamp in id3["TDEN"].text], dates) self.assertEqual(id3["TDEN"].encoding, 1) self.assertEqual(id3["TIPL"].people, tipl_frame.people) self.assertEqual(id3["TIPL"].encoding, 1) add(ID3Loading) add(ID3GetSetDel) add(ID3Tags) add(ID3v1Tags) add(BrokenDiscarded) add(BrokenButParsed) add(WriteRoundtrip) add(OddWrites) add(BadTYER) add(BadPOPM) add(Issue69_BadV1Year) add(UpdateTo23) add(WriteTo23) try: import eyeD3 except ImportError: pass else: add(WriteForEyeD3) mutagen-1.22/tests/test_asf.py0000644000175000017500000002713412177421270016662 0ustar lazkalazka00000000000000import os import shutil from tempfile import mkstemp from tests import TestCase, add from mutagen.asf import ASF, ASFHeaderError, ASFValue, UNICODE, DWORD, QWORD from mutagen.asf import BOOL, WORD, BYTEARRAY, GUID class TASFFile(TestCase): def test_not_my_file(self): self.failUnlessRaises( ASFHeaderError, ASF, os.path.join("tests", "data", "empty.ogg")) self.failUnlessRaises( ASFHeaderError, ASF, os.path.join("tests", "data", "click.mpc")) add(TASFFile) try: sorted except NameError: def sorted(l): n = list(l) n.sort() return n class TASFInfo(TestCase): def setUp(self): # WMA 9.1 64kbps CBR 48khz self.wma1 = ASF(os.path.join("tests", "data", "silence-1.wma")) # WMA 9.1 Professional 192kbps VBR 44khz self.wma2 = ASF(os.path.join("tests", "data", "silence-2.wma")) # WMA 9.1 Lossless 44khz self.wma3 = ASF(os.path.join("tests", "data", "silence-3.wma")) def test_length(self): self.failUnlessAlmostEqual(self.wma1.info.length, 3.7, 1) self.failUnlessAlmostEqual(self.wma2.info.length, 3.7, 1) self.failUnlessAlmostEqual(self.wma3.info.length, 3.7, 1) def test_bitrate(self): self.failUnlessEqual(self.wma1.info.bitrate / 1000, 64) self.failUnlessEqual(self.wma2.info.bitrate / 1000, 38) self.failUnlessEqual(self.wma3.info.bitrate / 1000, 58) def test_sample_rate(self): self.failUnlessEqual(self.wma1.info.sample_rate, 48000) self.failUnlessEqual(self.wma2.info.sample_rate, 44100) self.failUnlessEqual(self.wma3.info.sample_rate, 44100) def test_channels(self): self.failUnlessEqual(self.wma1.info.channels, 2) self.failUnlessEqual(self.wma2.info.channels, 2) self.failUnlessEqual(self.wma3.info.channels, 2) add(TASFInfo) class TASF(TestCase): def setUp(self): fd, self.filename = mkstemp(suffix='wma') os.close(fd) shutil.copy(self.original, self.filename) self.audio = ASF(self.filename) def tearDown(self): os.unlink(self.filename) def test_pprint(self): self.failUnless(self.audio.pprint()) def set_key(self, key, value, result=None, expected=True): self.audio[key] = value self.audio.save() self.audio = ASF(self.audio.filename) self.failUnless(key in self.audio) self.failUnless(key in self.audio.tags) self.failUnless(key in self.audio.tags.keys()) self.failUnless(key in self.audio.tags.as_dict().keys()) newvalue = self.audio[key] if isinstance(newvalue, list): for a, b in zip(sorted(newvalue), sorted(result or value)): self.failUnlessEqual(a, b) else: self.failUnlessEqual(self.audio[key], result or value) def test_contains(self): self.failUnlessEqual("notatag" in self.audio.tags, False) def test_inval_type(self): self.failUnlessRaises(ValueError, ASFValue, "", 4242) def test_repr(self): repr(ASFValue(u"foo", UNICODE, stream=1, language=2)) def test_auto_guuid(self): value = ASFValue('\x9eZl}\x89\xa2\xb5D\xb8\xa30\xfe', GUID) self.set_key(u"WM/WMCollectionGroupID", value, [value]) def test_auto_unicode(self): self.set_key(u"WM/AlbumTitle", u"foo", [ASFValue(u"foo", UNICODE)]) def test_auto_unicode_list(self): self.set_key(u"WM/AlbumTitle", [u"foo", u"bar"], [ASFValue(u"foo", UNICODE), ASFValue(u"bar", UNICODE)]) def test_word(self): self.set_key(u"WM/Track", ASFValue(24, WORD), [ASFValue(24, WORD)]) def test_auto_word(self): self.set_key(u"WM/Track", 12, [ASFValue(12, DWORD)]) def test_auto_word_list(self): self.set_key(u"WM/Track", [12, 13], [ASFValue(12, WORD), ASFValue(13, WORD)]) def test_auto_dword(self): self.set_key(u"WM/Track", 12, [ASFValue(12, DWORD)]) def test_auto_dword_list(self): self.set_key(u"WM/Track", [12, 13], [ASFValue(12, DWORD), ASFValue(13, DWORD)]) def test_auto_qword(self): self.set_key(u"WM/Track", 12L, [ASFValue(12, QWORD)]) def test_auto_qword_list(self): self.set_key(u"WM/Track", [12L, 13L], [ASFValue(12, QWORD), ASFValue(13, QWORD)]) def test_auto_bool(self): self.set_key(u"IsVBR", True, [ASFValue(True, BOOL)]) def test_auto_bool_list(self): self.set_key(u"IsVBR", [True, False], [ASFValue(True, BOOL), ASFValue(False, BOOL)]) def test_basic_tags(self): self.set_key("Title", "Wheeee", ["Wheeee"]) self.set_key("Author", "Whoooo", ["Whoooo"]) self.set_key("Copyright", "Whaaaa", ["Whaaaa"]) self.set_key("Description", "Wii", ["Wii"]) self.set_key("Rating", "5", ["5"]) def test_stream(self): self.audio["QL/OneHasStream"] = [ ASFValue("Whee", UNICODE, stream=2), ASFValue("Whee", UNICODE), ] self.audio["QL/AllHaveStream"] = [ ASFValue("Whee", UNICODE, stream=1), ASFValue("Whee", UNICODE, stream=2), ] self.audio["QL/NoStream"] = ASFValue("Whee", UNICODE) self.audio.save() self.audio = ASF(self.audio.filename) self.failUnlessEqual(self.audio["QL/NoStream"][0].stream, None) self.failUnlessEqual(self.audio["QL/OneHasStream"][0].stream, 2) self.failUnlessEqual(self.audio["QL/OneHasStream"][1].stream, None) self.failUnlessEqual(self.audio["QL/AllHaveStream"][0].stream, 1) self.failUnlessEqual(self.audio["QL/AllHaveStream"][1].stream, 2) def test_language(self): self.failIf("QL/OneHasLang" in self.audio) self.failIf("QL/AllHaveLang" in self.audio) self.audio["QL/OneHasLang"] = [ ASFValue("Whee", UNICODE, language=2), ASFValue("Whee", UNICODE), ] self.audio["QL/AllHaveLang"] = [ ASFValue("Whee", UNICODE, language=1), ASFValue("Whee", UNICODE, language=2), ] self.audio["QL/NoLang"] = ASFValue("Whee", UNICODE) self.audio.save() self.audio = ASF(self.audio.filename) self.failUnlessEqual(self.audio["QL/NoLang"][0].language, None) self.failUnlessEqual(self.audio["QL/OneHasLang"][0].language, 2) self.failUnlessEqual(self.audio["QL/OneHasLang"][1].language, None) self.failUnlessEqual(self.audio["QL/AllHaveLang"][0].language, 1) self.failUnlessEqual(self.audio["QL/AllHaveLang"][1].language, 2) def test_lang_and_stream_mix(self): self.audio["QL/Mix"] = [ ASFValue("Whee", UNICODE, stream=1), ASFValue("Whee", UNICODE, language=2), ASFValue("Whee", UNICODE, stream=3, language=4), ASFValue("Whee", UNICODE), ] self.audio.save() self.audio = ASF(self.audio.filename) self.failUnlessEqual(self.audio["QL/Mix"][0].language, None) self.failUnlessEqual(self.audio["QL/Mix"][0].stream, 1) self.failUnlessEqual(self.audio["QL/Mix"][1].language, 2) self.failUnlessEqual(self.audio["QL/Mix"][1].stream, 0) self.failUnlessEqual(self.audio["QL/Mix"][2].language, 4) self.failUnlessEqual(self.audio["QL/Mix"][2].stream, 3) self.failUnlessEqual(self.audio["QL/Mix"][3].language, None) self.failUnlessEqual(self.audio["QL/Mix"][3].stream, None) def test_data_size(self): v = ASFValue("", UNICODE, data='4\xd8\x1e\xdd\x00\x00') self.failUnlessEqual(v.data_size(), len(v._render())) class TASFTags1(TASF): original = os.path.join("tests", "data", "silence-1.wma") add(TASFTags1) class TASFTags2(TASF): original = os.path.join("tests", "data", "silence-2.wma") add(TASFTags2) class TASFTags3(TASF): original = os.path.join("tests", "data", "silence-3.wma") add(TASFTags3) class TASFIssue29(TestCase): original = os.path.join("tests", "data", "issue_29.wma") def setUp(self): fd, self.filename = mkstemp(suffix='wma') os.close(fd) shutil.copy(self.original, self.filename) self.audio = ASF(self.filename) def tearDown(self): os.unlink(self.filename) def test_issue_29_description(self): self.audio["Description"] = "Hello" self.audio.save() audio = ASF(self.filename) self.failUnless("Description" in audio) self.failUnlessEqual(audio["Description"], ["Hello"]) del(audio["Description"]) self.failIf("Description" in audio) audio.save() audio = ASF(self.filename) self.failIf("Description" in audio) add(TASFIssue29) class TASFLargeValue(TestCase): original = os.path.join("tests", "data", "silence-1.wma") def setUp(self): fd, self.filename = mkstemp(suffix='wma') os.close(fd) shutil.copy(self.original, self.filename) def tearDown(self): os.unlink(self.filename) def test_save_small_bytearray(self): audio = ASF(self.filename) audio["QL/LargeObject"] = [ASFValue("." * 0xFFFF, BYTEARRAY)] audio.save() self.failIf("QL/LargeObject" not in audio.to_extended_content_description) self.failIf("QL/LargeObject" in audio.to_metadata) self.failIf("QL/LargeObject" in dict(audio.to_metadata_library)) def test_save_large_bytearray(self): audio = ASF(self.filename) audio["QL/LargeObject"] = [ASFValue("." * (0xFFFF + 1), BYTEARRAY)] audio.save() self.failIf("QL/LargeObject" in audio.to_extended_content_description) self.failIf("QL/LargeObject" in audio.to_metadata) self.failIf("QL/LargeObject" not in dict(audio.to_metadata_library)) def test_save_small_string(self): audio = ASF(self.filename) audio["QL/LargeObject"] = [ASFValue("." * (0x7FFF - 1), UNICODE)] audio.save() self.failIf("QL/LargeObject" not in audio.to_extended_content_description) self.failIf("QL/LargeObject" in audio.to_metadata) self.failIf("QL/LargeObject" in dict(audio.to_metadata_library)) def test_save_large_string(self): audio = ASF(self.filename) audio["QL/LargeObject"] = [ASFValue("." * 0x7FFF, UNICODE)] audio.save() self.failIf("QL/LargeObject" in audio.to_extended_content_description) self.failIf("QL/LargeObject" in audio.to_metadata) self.failIf("QL/LargeObject" not in dict(audio.to_metadata_library)) def test_save_guid(self): # http://code.google.com/p/mutagen/issues/detail?id=81 audio = ASF(self.filename) audio["QL/GuidObject"] = [ASFValue(" "*16, GUID)] audio.save() self.failIf("QL/GuidObject" in audio.to_extended_content_description) self.failIf("QL/GuidObject" in audio.to_metadata) self.failIf("QL/GuidObject" not in dict(audio.to_metadata_library)) add(TASFLargeValue) # http://code.google.com/p/mutagen/issues/detail?id=81#c4 class TASFUpdateSize(TestCase): original = os.path.join("tests", "data", "silence-1.wma") def setUp(self): fd, self.filename = mkstemp(suffix='wma') os.close(fd) shutil.copy(self.original, self.filename) audio = ASF(self.filename) audio["large_value1"] = "#"*50000 audio.save() def tearDown(self): os.unlink(self.filename) def test_multiple_delete(self): audio = ASF(self.filename) for tag in audio.keys(): del(audio[tag]) audio.save() add(TASFUpdateSize) mutagen-1.22/tests/data/0000755000175000017500000000000012213137321015372 5ustar lazkalazka00000000000000mutagen-1.22/tests/data/silence-44-s-mpeg2.mp30000644000175000017500000002057012157371172021151 0ustar lazkalazka00000000000000dXing!x "$'+,/1469<>BDGIKOPSUXZ]`adfiknrswx{~LAME3.98rd-$M!xkۧ$d< "( ? 4d"<~,_a?uA_ח`?Kڟ $d# D?*`ӫ K$d L c8Fz:{?=ai78LUU$dP"`$Fސג٦j84d "pƼ޸gy0ڃ4]] ?MO$d( " DXO)~B>{$d  "`4F e_bMZ $d@#4DOokJ 4dCh(F A?Vg@rqm$dL" x(DQ@$d"@$K}+:o;vVn$d  ",Kjn({jg<$d#,D{You<o?$d\ "D?(N}P4d 0 #gzyb;|YԪ6~b$dh #D Jb Et$d T "`0vPRU$d c(F4]GX [7Bo4d x$OYCgܰ0תU9?3Y$d # U\l/)gomM4d@"#0[W dH #D4d " _Wſ@Gz)dzNm~+tuU$d "D +-q4d #$Fj*]Drӎhh?[OWS G$d (DVcO_ۣJ_@$d"88Ft?Ӡ*$d C,D}~z,C}$d "zR}J4d #0<FߧN֊O7ܪҠ8V7\⫇}_$d ,$cPPD{`Ul(3O$d #<DEZ;4d  cP4F(}߳V"gFZ $d cP,DwM_* +X$d c(ښ6CK$dd cH4FozoG$d p "@@DNSԡ`7Q..4d$ f D?{T14]S5U[mܫ:s$d"`4F2 @4dD cp<F?~wZ?H"?W$d` # ƼGbz?wܯms$dc,F?4d X c<D&l]zYeM gaAd"C 4$d $FE>?~Z$d "FPQ]oU4d$" F{3˿o]`?}$d"x(F9*^QUj/O$dl #4DaAOE$d  "8(DO_څ@o4d"#(,Ff7gȀq?;-_Ow$d#`8FUu $d 0#0F[q$d \$"`0FT[vU0$d" # DZ?- $d C$Dh "w#`?$d( " WG$d"#DFp0O $d "",F۵!85 $d "#8DGԶvoZ$dH c4Dы;)˸V}4d b@D鴷԰?~=_W2O$dP# }o/$d"#DDrH`?-צnPR$d4"DjܿfU3oJ0<]$d, "@4Fݝ@Wm5?$d` c,F^f1tP4d4 c8F4}?oh@[gWuDqK4d 4"#$DջHn+MZJm~ G$d #<FjdUu$d # F l*<4d CX4D}}UdDn?(dt" #P$F4d cP D4;=,̃ E ?Z$dh c DnVPj4d  Kp D_l܋)_ $d  cx8Fk{wvsB ?$d#p$D_FsyEZ($d("0FK?Z(]*4d "p(sX?b_gR#Ej?$d #P8Dzo(U>4d\$"4F@!$d, #(+* :Qh$dc8jͧG^$d"#DrR?E$dP"0 D)Vow$d$#<FHY ah4dp"0cE oH`?*!?B$dH"$Dފo&g] $d t ",F@ O$d @$ ( 8xʫ \*4d, chSzP=8t+sIǮeӤ$d8 c@D{?G4U$d0 c J4d#,F?H[U4貎$dDCDU0?׫/$d C`4Fcpڕ_Q5z$dc(0D?ׇ]Yz $d` #FO2s]{s?5$d#p(:ReƄ4d`$",D?יʙW<W.h@Y!$dc(F 4$d  "<?T-O߬$d xc D?Gw E4d4#@ էJIw_p8d$d("P0DUŀm/$d cXDDG?B6k_j$d#p@?ʕ$dt c8Q!H)$d "`_ѯj"]?oW$dL$ ?̷Oq$d "0Fu_u4d $"H@G߶W_RoZ8$d B0$F?+v/!U?$d$ c(@xBoq$d H$"0t~(~J$dx"0D״$d #4_,s4B0 $d   JDj $d  #$F0$d ",M!4_4d c@<FoOj vO)F;]u4d#@4F6h@ZENW.Gfq$d c8@ƼPM__F4d  "@^>i?:/ڶFoP$d c 0A*Z-}]$d#p(Dk﷦ 54d "P DM5թ ?R^U_g$d`$ "XDW5P$d # F $d  #8~ު`gd"@D$d "#X4DP~4d H" HbF8oG?b)$d@#XDFڧŽU $d",D-զ4df< L&m_ @ $d"<F8 ְZ$d#8F?m%_׳U4d| cPD6?N,Bbqӟ_@L$d4"",FF=?G$d#4F (?j4d  #4DꢫԬ̌?*[?$d\CH8F{?)~h$d  "<t*}~$d c D_(?r 4d "$D}?[l=hѥ $d@#(D_wԕ?/)??K$d #88Fj!Q7)4d| " oGgx?qoPx$d""H<FN֓?"RM$d " "8F"ᨠ>%- $d&Do@o_7+U[4d< cDt]ۼB@Xkp~-F¼P0?$dx"\ DS⢚LAME3.98.2UUUUUdHUUUmutagen-1.22/tests/data/truncated-64bit.mp40000644000175000017500000000372012146763412020750 0ustar lazkalazka00000000000000ftypmp42mp42mp41zmoovlmvhdwwX@-trak\tkhdvw@$edtselstmdia mdhdwwD8:hdlrsounApple Sound Media HandlerCminfsmhd$dinfdref url stblgstsdWmp4aD3esds"@sttsstscLstszw,8KlT@DNNco64ytrak\tkhdww@x$edtselstmdia mdhdwwX:hdlrvideApple Video Media Handlerminfvmhd$dinfdref url Ostblstsdmp4vxHHEesds7/ ] @ @(( xstts(stss(stsc(stsz e2[ co64)`udtaXmeta"hdlrmdirappl*ilst"ARTdataFoobarellafreefreemdat& voidmdat  sN^(9dwAn\[ $B>z˷xFVo%G _R w` _SW[H1k81Ȼ6m#6n4:߆(eʏQU ӣ[ fK5IE^^U+"5>&$|^RJ)%7&.8!l 4a [4Z^kV0Rl-/l9Fɻ99wBrX݆⤂QW:eaڔ[Rxqu!yp];|^gҘR Mxm:*֫.mutagen-1.22/tests/data/empty.ofs0000644000175000017500000000073012146763412017254 0ustar lazkalazka00000000000000OFR D@ HEAD,RIFF$ WAVEfmt Ddata COMP}w @5.<TAILmutagen-1.22/tests/data/empty.ogg0000644000175000017500000001035012146763412017240 0ustar lazkalazka00000000000000OggSۿ;ZfvorbisDOggSۿ;r-vorbisXiph.Org libVorbis I 20050304vorbis%BCV@$s*FsBPBkBL2L[%s!B[(АU@AxA!%=X'=!9xiA!B!B!E9h'A08 8E9X'A B9!$5HP9,(05(0ԃ BI5gAxiA!$AHAFAX9A*9 4d((  @Qqɑɱ  YHHH$Y%Y%Y扪,˲,˲,2 HPQ Eq Yd8Xh爎4CSR,1\wD3$ R1s9R9sBT1ƜsB!1sB!RJƜsB!RsB!J)sB!B)B!J(B!BB!RB(!R!B)%R !RBRJ)BRJ)J %R))J!RJJ)TJ J)%RJ!J)8A'Ua BCVdR)-E"KFsPZr RͩR $1T2B BuL)-BrKsA3stG DfDBpxP S@bB.TX\]\@.!!A,pox N)*u \adhlptx||$%@DD4s !"#$ OggSzۿ;f}[ mutagen-1.22/tests/data/52-overwritten-metadata.flac0000644000175000017500000000100012157371172022615 0ustar lazkalazka00000000000000fLaC"2 B6 n!10؟ reference libFLAC 1.1.1 20041001TITLE=Songs of RejoicingARTIST=Giora FeidmanALBUM=The Magic of the Klezmer GENRE=Klezmer DATE=1990TRACKNUMBER=01E=Songs of RejoicingARTIST=Giora FeidmanALBUM=The Magic of the Klezmer GENRE=Klezmer DATE=1990 TRACKNUMBER=1YkD,U&XQzER"ҕ$UEr_\rqRܮK%uI|]ȪT2J[N*%IWw.[R$u%%"VTY(BHI*%$%˨.) I.VZK҉+UhZR$SH[HZmutagen-1.22/tests/data/sv8_header.mpc0000644000175000017500000000016212157371172020135 0ustar lazkalazka00000000000000MPCKSH*)^RG E}ZE}ZEIQSODAPAPAPAPAPAPAPAPAPAPAPAPAPAPAPSTSEmutagen-1.22/tests/data/sample_bitrate.oggtheora0000644000175000017500000000600012157371172022275 0ustar lazkalazka00000000000000OggS.e Oq*theora-$@u0uOggS.e "! >?theora/derf's experimental encoder library Apr 21 2005theora(kIJs1R!1b!@d.UIvpk@HGәd0K%R,F"d1b0F!`, h0֕UTёPO ̌KKJ LJFFFE‚AAAAAA@!31pSa5u!bSFtт3tvwT'Fv1!6661!Q&666166662&66666666666666666666666666666666666661AAAA 3Bꉽf"G?r"^}jfWbB`K0ՆU)^}4̮؂ȣU62^?9w1+< sTWcAiAuܐek1x^ yh"cuk3)5 @ x]w(=ιM+F_Ţ9"ͯb%r({IzJy'Jl@CݺcylZu+1eG#FidR,>id<t +-1R܄֏^vj"|v}-VK&b'w@k^KYj#8UIJβbQ#9C6W8bǒ/j>FaП2[ E(,y?mB"\t-2{|H纊V/>mD<#H2nm0@u ]P9;DzJi## W`CӸs548r&yF \{Icp3%ΪS{,50r=2\Ta}{ n5NaVDK7N4 n5&6dEc}BF\g 7b.خ <)#wVQ!f (mK \N1f2Biv}jD8˨*R&spZ~z.+D)p=d:0 ЫZ4]%M܃&C:ŔȹӰY=-|[יŕ4tKсMFpZ-_ [Map#r,Y>e*C , S6M#ϓh8}u*YS6Ocx;D?mp71qZq1͵L_Yb pErr2jrEYb{FV fm,& )EԜ}7 _s yZhmR&H7y߀(nxH&gݱwT)|1$jD-:?0+OݠoRp[D%fڥi6LpÔ#YXޥ(x/`ŐT3`]AoռX*`6fr{D(zG+#wʔ~ϤVVF ə4́Qӥg!t 4Ͷ[슠)wbB& GQ8?,"$%m6jR\e߁&I*/?uܶd<*#$%4{8;ezq=+~`SPwfi3meaZ}bd6ʣMDVXOۋx?OĪ'YXg wqu6ژP4' U"'ְ JB*!9U /\eL屃xSA@jg@n"^ui-36s tqaI~BAd@883ȶu&|kQߥU#x!p: pglLGrK+y*FCt"ܙG~VUT8Ox&i#!eb_z.쎏R_, FS4Yot*V8$/J4Ͷ̱ѭ,k#p!:?JL晶"SQI@z;qϡ]7i3 ͵^ '+R2Z583^]QdQX^Lsbm iJ@F,qdD'6,Zf]k 䐀OggS.e ;;P2b$I{MJD4tL&G6!q8Pڲ#"YT xFI`b@G:Ⱦ|4h HHF(Z>"Cvk\dr거1bXl0eu@ʷ\4T!eh Vd+/Y`ϏqZNxQ/ V_HB;@8 eswCmutagen-1.22/tests/data/106-short-picture-block-size.flac0000644000175000017500000001112412157371172023407 0ustar lazkalazka00000000000000fLaC" BzAf鄚0 A\.sk en-us]&EG_eRů[wHgDLz IsVBR4DeviceConformanceTemplateM2t E˖˥r2CiR[ZX0"N)54I"@^PDWMFSDKVersion10.00.00.3646WMFSDKNeeded0.0.0.0000 IsVBR(ASFLeakyBucketPairsr] 0uR ȯ6m*0W # b@BG\3 "@KL@Rц1HARц1H%Windows Media Audio 9.1 Professional.192 kbps, 44 kHz, 2 channel 24 bit 2-pass VBRbܷ Sez@iM[_\D+Pÿa $ bD"""u{F`ɢ ~6&ufblF݀cBl?[]"+4`h F&`h F&`h F&`h F&`h F&`h F&`h F&`h F&`h F&`h F&`h]|" &`h F&`h F&`h F&`h F&`h F&`h F&`h F&`h F&`h F&`HUD)54IF"3I8݀cBl?[mutagen-1.22/tests/data/52-too-short-block-size.flac0000644000175000017500000014152012157371172022461 0ustar lazkalazka00000000000000fLaC"4 Bwx8PDagԄ0 reference libFLAC 1.1.2 20050205TITLE=Mother's Daughter ARTIST=Tunng'ALBUM=Mother's Daughter and Other SongsGENRE=Folk-Rock DATE=2004 TRACKNUMBER=1Y$V0jZ00PP0P0PpPq0PP0pPP0P000PqPp0PP0PP00000PP0Pqp000pPP00010PPp10PP0p0P00pPP00000P0PP0PPP1P1000PP00000аP0P00PP0P0P0001pPpp0PбPP000pPP0P0P0PPpPPPPpPP1P0PP0Pp010PPp0PqPpPPP0P0P0а0PPp1pPаP0P0pppPp00PP0P0P1PP01PP1PpPp00PpPp0P00pPPpPP0P00PPp0pq0PPP00PpPp000pP0PPPP0pP0б0000PpP0Pp0PpPppPp000PPpqp0P0PPPP0000а0P0аPP00pPpPpPq01ppPpP00PPpPpqpPPpP0PpqPPаpPp0010P000pP00P00аPpP0P0pPpPpPP00pP0Pp0PPP000PP0pp01P1PP00pP0PбpP11P00PP0PppP00PpPа00PqpPP0P11pqp0PqpP10PPP0PPP0Q1P1P0Q1Q1PPp0PАpPpPqPP0PPpPPP0pPP0P0бpPP1PP0ppP011Pq00ppP0P0PPP00PP0P0АP1PpPqPp1p0PP1аPP00PPqP0P1001а0PqPpQ0PPP01PpPPPp0аPPа0q𐐰P0pPq0P10Q0p0Pp0p0P0PP0PP0PPPqPpQp01p00p01pP000PqPP0АPqа1p1pбPpPPP0P01PpqP000pPб1PPа0PPP0PP0PPPP0PPrѳб{-i p=Pm[ÅqzPƩCW/RLA2%V~-}Y*i$(Ee4o FqdK+CxMTLdʨ3)!*nesh?&OjgЊr^BdcV₤ y2㻄f/|j\f.`4I LCK֬jZ{v% \)'yTć 4s=0]/Kב6іF~į>D >2TГ, 4/*2qAf3VMN:T0[I^\@ g?:R>W`Iº_32yM}^08G\Xʕ5vg5oA fSG5GYڎƱr2EPh!hk u LjX[\fv~chm)PyRgW;El tb+J/' 5q#VtQH ܃Lj϶4BAօEPze'ǣb&lG虅zw3(YV+),_cfС 3Fav-VX0 -0*OI,DkUtvJ]ߛ(HYqmg9` Q~Au< YHGq`eɱ a)L%PtBˬY`$PU$Ao+fQSe\̼4Ө&TbL]z`HnD>Nd#/D"5Ïi60;M?LxW85X_m.1D#f`7xG9O!}=Ek=ve;Ñ\P0EOkBjӊ'ךq8R8)LCQ&a8\-+f21[N$]m(I:~!vvP,sZ׈mBC`@ `RPwIjYK3O%1M?F, ;[$[<jDʍ #+S|Qf,IXnTkA&Q%)j=#5Ls|z *}&WA3Ն20LeY;4l@V%k!VL T@4}˶_#HsEo:qto Bj*yGaRP%R~K 0AG!^ÕQZ$7 ||2E´1962!jouLd$q Nz'ѫ}v!(Qa0G`4!w|  ߣY) ŽK|,>{bN :YT @Eeva ֿKWo>&XFYBz1t.p-$S7)GQ ǐ7lZ aK!mhoXe [r@OQCS81["~n LAʀUn^1 ` 4 6xO A(^Nhrs$> @cSKG6L ?EJh{ͳ3A$B@}u8pȘNli0yd8mT97;y`%H,i*WH{ ! yh*æma5dhCc@Y+T$+m("i %w4E]jxA"0t3>ص /H|s+iYG=T9 '(Oi=+{"7F?e, ~j@UE=\-8*`d./@e,zD-2DG<e+'n=Z!P.Ց\7NxW~ (Ӡf‡2҅9-R@8T['́8B]L@ @ZށMo)Z$S@0_%EЕw;YAw~17?x(HHH((8H((hHhxxH(((HHH(H((hXhXHH8h8((H(((H8h8XX8XXH(((h(h88H(HXHH(H((HH(((((H8hXhXhHH88((H8hXh8HHhHH((88h((HH((HH((((H8HH(h8(HXxh8((HH((h8hH(((8HXH8H(hhhX8h(H((8H8H8hXh(((((H8h(H8hhhXH(8H8h8H8hXhHH(((((X(((8hX88hHHHhhX8((HH8(hXh(H(HH(H(HXxh(8hHH(((((H((Xx8(H8h8xx((((((((hH(HHHHH(8((8HhX(H8h((H((H(((((((H((HH(Hh((h(HHH((8h((((HH((((H8xx8h8h8hhhHHhxXh8H8H8h(8HHXhXHHh8h((HxXHHH((((HHXh(H(HHXh8((8XH(hX8HH((HX8H(H((HX8(((HH(((((((H8H(Hh(H8Xh((Hh8h(HhXxH(H((X8(H(HHh8HHhH(H8h8hHH8XHHXh8H8X8H(hX8xx8(H(((((H8h8((((8X8XH((HXXH(((X8((H8XxXhhHH(H88h(hHXhH8X8HHxHxHXXh(HHH((hxXH8XHHHH(xXhX踨(X踨8(8H8hXX((X((XXH(((HH8H8h8hHH(((H8(Xx((XȘ8x8(HXh(((hX8(XhX((Xx(H8xHH8hhXH8Xhx(h(h8(HHH8H8hxXhXHXhx8((((H8H(8H(h8XxHHX8HH8H(HHHHHH88((8H(8H8(((8XhhH((hxxh(8HXH((H(hx8H(((H8hH(h8hHHhXh8(XhX8H(HhxXxhHxȘHh8h(h8((h8hXXHXh8(HHh88H((HHhxxhxxh(hXXHH(XXH(((((HH(HHhXXh(8XH((HX((8(xh(((HXxh((H(HxX(((8xxXhX((h8((8XxȹiyjK,\=},;1^3N 5x.H( &Y ;( 2 1_\K]_t$vH&HBXwhgե`n3õ2p(o z)`PF HH ;9p+ja %jq9XI- 2 pCH, BhJA1f5>Es)eLAzVM2)Y%&;)| ^:7SPPH 9{ qlQtOdW&ĸ(Ti|mFb8ŸPj\{rd1&6M# R! ܀)H^[AW6T kD<x4&Wfm}CT.YөyI9 \Pi)ި%,nffO1"=l`)]X(\6CE-9 \_ >Dw8֤ R?ʵ)P I@*ՎvR? PV8GdF%y "|8exH G9}(Aj1nŲj!J=T@>89jP(5=CPs1(HE\&e0Ia/X&v!ǍPŽ Jٱ*n q{_aRڤ,qY]4Q$ao3 f{xgq"6 I3Ւ 3D&6p$9v X ,׌|&JSP'ʇBUS(dZ}IyzO1nm]6Gȋ !k-kNtij |_#1t a +(PoYT'L Hڨ>7 Vz<9PH!@(pPK Ğ-! HW߷*MSP؇t[uiOrXّl8Vp(sn1hݒߠBF A zx?"bQ$5qx Ah呻P ?{g) > 0c*&W`V X ,{S:Lo!|-@$ <;ip@ d12QXC#+ĩ@LoI- UXcS`,ILH#xk]|Iat\Q&N:w*6u`T90\Bx>epA[ڍBGP!]fVnQɸkn4Mr(| D -Hlx 6T _&Voc~ya  8P>GQW ݷv8v}~Gh4ɷ@-Èd&Q[h=\Gi:@;zʃ\Z feBXkAlU>=,pnvE;!EXY!@ aEL&lJ@8&x_ P8_o6*Α7W*w_jUTCf&}ġTJ̰Ytr '1a9.[`hc!4gq(B)F![&ʼnPA+b[!M#Ec x`F 'WSjk&>xߚ "p$8)F)<%F0]8ujL˰9)ΠsNAd&l":WmAE'p<f' \/yCXWqX)0B\SM ^4L2@aY#a3ƀeb1jf9rh IPR%FuʸtwWmF0XB#5͌&B;8p<2em`O,Le(N+uj\A>]0,&jBfkP8.{`m ' /4PKCI8{ P,vhk8m"d ]5FXQK p+B+U1-F!$OX"'P/050  #Fr*߄,ǹdY7@90ZP@ @[3>58^yg[FGA 2 aeH7+&J H(8;w]y,&@;${Hpo8hbh2@f< \4瘙1 "#dT٦3.e[oh (*E@+#U:AV"RίRC@L^K#`Xeftq&>4@~!.٥ⷍ`khbgy#,,D`%?Fp*Z3Tߡ Ȼ AT*6c@T p/ YtLT),Y؏9PS47%trܞoU %4 i5m $gUhRs_HJmz1GbL0?+B|uӖ.zILmdE 53 `?A Ps0p.%t[׈G(`\]ۥؤ gPxbY#Nt(M{> εiՇV tN` Pf @r$+$-@M @y.fA\>NA| 5wh+O61 RZ 'D10tc΀3&f)<0vV-ڂ!L-Q2@`20@ "PP DX̦@h6NØ5 d |4&(#@61@0 @d7x) @@3W31N 7q-h8VM7m'= EwInEU\(%?' ⃉iup\[D |l;sSBX=(3u nUqdFk F#`]@q݁p"hXs`U,i;BJG,P2 :dX: 9ǐ38'c#8!%_` W̏c"O΀Er AnQX +SKx<>Ys}fnE8hr4MM:0ΐ=&݊z 4LPxI)%I# 7P;Iq ?LIJw4vpH;QK !-Q r9y}(8d?S~䲕a\0i' `HXLA/@*BD ds|!BU)=J^O(' IINQbXpp@uWul{at-NS\N?DfFgrjBV1E Pk*#ʍ&YId2 M! @FhE;M[Ѱy~B\X+˅&V"ЍyT XO 1:Lԗ}-wZ èTlYF#E7j7Z)x6j#XBPl%ÒCy;ʼIwf8& м%O'dsM:~cu#z}8j"_nˁ6SHéR6.  ͙M2RKc~ա3@L8B: V$`ĕILa^5HQVU4d>Bm'%~Idy3,THONR#u$ ~cjъ {ȲGw(R(!О[?fzTce,:y m'C"n}(%S"38WYƄGeG[Ya0s_[Qf}j %$׌C{+Рzb>27dN!VC7ޔ@ǒ!Nfϊ:njL<6[ U.BV'V:!OB3MMOg"ov.KO@<>O+vy=:B {DyO&GnBZn9)ޒA8RIA}Ol$͏|Yxt֧Mf. eQcWO%<"xJL3Ėc⫑Z t݉s"^O@5*;Ox8p25v[/{H~24,D[1n7@iWЈ/C?Σ2))MG$"`5[DUQ2*5+깏/>$>3}_l\2Õמ2y3~fA9tECTҞe^ moS˟GTD10(PRjhCUgFdOK=P^v#ˍ;-ŪR$FM!1*"#5`%#1 _} -o]\a`y:nJ&jTw_Zh֦)E|",Hɟ]505os%ӭDš;*³2dx;Pdҏ)CCڎaDf欗 ď"֜[%/e&hG,6$C?= [7$ HU2i_O:\mn ue Q^Ȏڥpmv:W6*bn;"iΙe*^+lG،#Ez̽ge׮z޹0$\ \A*[qa/݅}{UQv?n#~~Cq#˃^+(7:,61)I)FD%j27!WJqt'*.$6(G=`-T-J\!jzvSy>ևE$չ/4pT0ĉQwe(=hӞ-vcRDc^0yb-, ^t:qz/Dk 99 ؾ!oE|f$$[]X+c uڽD.d%fxw`+¼5,'fNգQ`Fscr2|E{^䐕.;}3Y:֛pСWw?xįoR~4 L/}DK!rkU}ك1^;AF Pb~Н.H _[' PI' Q*O,?")y7I8#v+Q 9qJi$K[Wh*:OPY5ZI@Ȃ56lWs75MOOA]7eyWXHK3ޠoGdWcO9NJ=>oȋzNXS]҆B!9+[ [|AE}=ed Ɔr*k.D2ukoBNHB@.5AZ)sB>Jkl( `?_1=5؃MaA1ҫY8\E $)5 y""BsEcƿ^M {_xVnХ[T'u|T6NN9 X[Cͅ0-+;45gS81G@pDh3 BBoWP/![쫎#-Rr+ " %:ˍ Q[H.#kQz%qQG(XQȪΥ!g*c>F/ٵ jCWqa Ym%G' 7 Q*c xK,e#|萬I18#I0 7!f]vv"jY&1ٍYeJvj?F{CMb.^b ,]-=$Y2U|1jS;:Z d[$zECH\dKޑBcd;w)T`bhW,dbZ 9H #X-b.VX5\_ƶL'=U_4 pZN(><'hf1IT#= +5ɡ%]ytJ&Z؝XTiTVF] {zI]"O* f餯Rd5+J /jԦ&xV^9:R&ʾ:yRO@.bNDp-:;#\`?KHkUF(NH1P)8! |$hQ Z SA?;wEԓ;G@#hzIJ~}A mQ)9;B8Q]瑹<w7cAuj ;)E,bFW$eYJb9h;o !M WpivUca3+]3 )$.8JR+f``6y H3)/(~(jf@*POػ.$cf$\f>m(z@1/_F)o&Cu|ix6eR@VY |}18ILqUk!<00DDI,DHcG_DNrpPXdCm}Fʮr @fʠ$`@4dav"OBaд,k\T"`8NrRUeo_.29Jpz"NCڜ䃥D DQd>ɺ$T~"Z*Qv M/2PJF[n>/\O0o8'@2jlM r?B@ B\~ D[DMZWU w#QJn04tT_`<0%+kd ?6O"ۄ <"sU9¡@ ?+]I 0L!`_n L=- ^Ams׋Hpu1%| @KTXkvgOIuˀ߉t@tRduyɛ;)M-O2r$u%Q{Df@Iw ޺4 tXeI?9[ DGk<*he*+@הJA*[~;o"=ks!D&ONn/dGc2`8r9,ezok*3aly\G9gb9,:[aLb)>|qSlJ*+ n:C&&Pi0‹ᛩcX<8S6oK?iwTH|榑Q~HHl}-"q#ً;EO}m:[R QW1_fӧI&Xxy6)g&MJ'Oy7PӘLCX6Sbrcu71QQv[<Q;8VwY*ZaLu)$1|+#kQIO-63"eٰ¾u$%iLɫ#x+ 8{MM<$aR|dg|@NA-Wdhqtd)gn۫dChvFFChIsB`}m"/uղz4$.(O<υv?W39Sj70: ҙH3*bYP r*3>~-{ȈF٫=aT*IC@GtsB!+}qӑѾ[ohʾ.mo |]EBL"$g8S8hˋi8.WA*km ZPmUu "C#yr*vvYk2b_%! dfוlYYfʑԓEFZ9<N>~]ƿ]n[ׇCyeMB&2!|,S9˴"zBѭ-_%9oݱkkDd}#:|0,#+aeq'j#'l$:fh鮮QH2B[AcMs 9VchMjDIMp侮0j71Ly|:8 )ZZ;Uޫ.LFbrs-fn~VN Jˣ:*o}cd{j!$+Su+1qȖںZg/,ImDVZQKX)IbC{ Œ˷OqR;$(I(d_;ȶBvp|HD+7S> f* Q5J!Q9>7u2к4kUN+ jQM2RCYGsN'?(J-^HJgDn>a˖A#7409ўHǦ6] ӎρs.0*) K,+2-9#FѕAfmoj$Yۉ.-IeGZjnf*jcN:!ESᱪe"HX 3#?~GqK ]N: [> |)fl~cš0޴cSKPD0&Pv "Y93qČLidm:l5dzBz̺rjhR"?dKl+;$l2v?{ROH/4w'j\BpNE!*R$:G62g -sy;3?_'檇+)69aΣ@#aw iYT$rCF T8t<2X'F:u G઻Ew>'A)NC(tc2zRfq[ZA5vlV~w/-Jό'h1ЗRH l4s1zH6MdtD/rB.ml%a'a錈i?sBq)QTC+`-9ej06<=mei9 _kD0}[iZ+D\ S9o=c_\PB`I\O(߰K )] GD S/=#$V 85z{QS1Ҳw  aRwΌ7N6`U\Ѣ+yhGSlV4-BQ/UϯC5?psLmĄe._ e-}W33k:V,BAq؊DDP2wiX.6/KsoQqݫ+.+bT2RNZh禥?iSg,Le;3@!lac>)SlEGvuER7Qʹ)f#ڔ7N" dBGeG *ozhe]~TW#!JpDL&s錆lU',֪h>塅^I%ĺ/$Oo-t@?JJ況k̤ C-_9fEDe'2/l1vꪩeGzO2MS^D}9 6K\3Xݳ16yv4.Sw4MuM:dP8Жr1Fe#{cK #s!G7GiGϢ,LKϦ3kҿ͈I!"<{ :U B6!|:3.6JlMުԫ;=иVTllZs26]fY㵓%QRnófwqf.0pkSCy-Ѯi)T)U.e2kJYoif9|nm$Rs6],\eBc6ȵEȐ&_#q;|`88?MuU>>J>Biv1?**{z^K6x7ۧ&[V5]-4=ZǯuyNAn+9uevq<+ڛ([ƼC0bZ6̧KZJaU͕ಲD6#)eo ;w}(F +HHDINtT(gX=FZ,TC5,E]ҳ&DU.Ej4x?NVc0d3/Af#$p=䳹b_w?O&D{Hq-cuAXVS)W[y4 a,oQc>ø{#1`?D\Z@\\+y&hIAe Q*ygb=_뾮 "-"/nPB=S 'DAx<6*drUQZleVG,! ُׄȚ!1%2\*^&%V"Zri~H ❪G~vbzяty[I:8`i+Y*H,olVXӋo3zP$ijz+ -hy4{en~}4 \4nV]44r5?9+Y􊹎V)ZZYS&CYƝN&s*NoͧR+)9Y\oޟ YǠCDLk{Abx&}&,JßǮTRJ'&y$F,~(˨Zb靯I'ڒk2im6VI&V7&:rNj#3z*Ni>wAΧЭZ;=QŹ]Fnipz?iY] IO! i|msr7є7-9q`a־T\5&.Š?lzJ85a L0Ei$PjKcz\h'=@W( oI_[|ߧh\jbKdOQh⌫imF, FBfZȊ"ܫ~Yz\T n"BKF2{1_tGi'VHc&䨆ƶBR\S֎r? 0:%8^Ը2 M<u\hEԟ3#U,$JZ 9BeQecx̢;YJF{Iwy[奷gZ6% 73IU[oT=ީ֪YUc~쫰 ٪zvg=]D,'oqez m7CDZĮn%c3{uS "Ďm7,5uŇѡ]r҉`pٿi̯& 6%+)ǾJ1HK_([vjҥ R2G7;RϟgRA]l ^\C v 4]H)S4>x_ܞE=KUQNjgw1>qTR9Y+Y(sXOD+7PʅU M6@B*EM U9ikuyfHH1osK,; >e2,Br#r#ylLMkFF14ľLsN6XO3X1h~!=LWU,q$n+rZwbj dDCRJ" u=dHN@8q6i3FHօ55WJԢM?h2L ;nfAASg %>[FSg )"~MJ=18o *Vl䒧NT-*n(/cxؚn~4Z]x@n3=G6bQ!BTS,,+g1rpYޕh#%D!#F0,RY#"w jTwJQjg'-; hL#M<`%0a;D"IH̷3R4M- E]BͻlQ}]Kbs!HW<{7,ت囪鳩sq ^Fy&.` xE?5huf.LN8q8<~NS˕<, 3š~GFnjA߳?+iC- CPgርI"ZRWmlIMԟ,ƴb+x5iRSPAe`sH#%= 3مeLԶENB,79ej (Q۔cIxEo`(tveh5ݬ_vO Q: N`5Jr-ոL&WK3iU2?n #K]͝u*qJS߹jA8tȁT[ÝZ2]l׿ rB/֑ԛϋ:ơ.UqL F{2]:=sz${&O@+R.:h>]_}. do?ǂOG"w*bU5ԓ^18_~M$S{2D<n [Fx],Pذ =W)\Jh# Ij4ȴFf6j%s:[X(WW{xU 6rK^_|EFNR)wޕɾVn4J-¸Q[9CIGǾ9iݙ[;~ʱ'T95PO[q~GaI⧑ h})LЊw%0wm0~uDo{)l[qKm4t<=).3/&.TY-ɌL9MoTAHT Y+tqg. jAD(%'Z &dB%0`1?xAN/~2wwٛtҶkiȖnЬQL߄C)S[n`jOaQs(T"{%IfԹ-ɍ$e^gN6X .-AID]4ZZz)JY T1xD2O;Џ_rΒ}5#+LY!.8KҰ0Lt' r8UIK1/yyJR]q!.gۉ}CN7v=+~=Z6ey5zúFnpVߍ%.z][.m77YK*}NOǤdTwUio$93ô#EwUUb(>ЮʙdD&2$%VAKg}lPs dbF<:2:OԈ3Xs8;ɉU;@c <3l1ljx ߉]Ef.hkQ)4fң[ }rp"cE4#oVKHW z"ްJ>*84+\)]1R4\,\g#'\in>+FD+#ؤqȌpF-*G4]SZvF}iHH2EVeFt2GdkHGIܶcߙ0I\WD}`(%P2f>xYΚr'+zgҒ@MA5,ij<cYzN;L:9Lev"C i1!v涳̄a(LeUb9lX=O9\OB:#b*Ǎ!q?Ѭ1L2zLߺI`4Y}X!E@3kaCjF%p L!uP泌#E2m{4&O5h0Ff 1tg֫4ꤩ3\gmNE*6]|%(x&oы|{CVE}-o+iw[,>䗛x=eڙ:kqwdH[kTw v0 ^`JO__#8T5FgY{@ 2zjӗ~C_ΟcslbKX#l$Lq*TfHFD2[ i6S"2O'z`JR+2BH*&U!,tX>~k%XMytKKN bʿBs^zIX[ۢ*:2Kh~<i&ojn'#0%qlbu-"KUݥY(mWW:?t"lU ٶ@ Pt[Eh1it3NVzgIiv*T nݥDq0Dnxv +j-~ 1ʲr0}Ƣ@7m?v~)&X)Ƚ ֞ȴa,&ݕǝɆv#:Og',I0^dDY\rBtv?2$uO_с#F]3TMTC!.ʕ(ϏG*/wH9$sB{q[@ *)ӊQz}&1 (nK/H²jK{+%#QtG6rõ|\tL%E=&!#fSȄBrɈ[CR[L:Q@2aYC# 3 nrT+ ihdl1? {W5R.P)o%xbB'r'<βWy4-,Y%'tnucf|dUpKSk*2U쮊Z14s%5V8 <rJ` 'Ava>vQj2GMH;͎^*%*mer? 芣(M2vjTR϶)s'|C%3e 2A |cZ J%͜ء1ۤ ^+K֝tX+.6 4j_)&j4) b{,PL*bcٳiCz5+ **I_U o{9Ċ ͒u3Kk$ͫԀEך6Y-J~QRh@,^>ϵo1PE055wΚ-WwvRpb1:)2GjVI  Ҟјbtxw$Uךؓ'q\De`RАNʪ3J@KNGCF[@g;E,ja1]+FŹ#"QV%.㢣]+) _B֞hKDnKXV|e?OMr6F whs 38lΓnOjR=s ;Hnc1_{ qqNUQ_29/!E/+ l76~BR]$+Qd":3]k A}r~_$ݚR"Q_j`Y'S0£C ܐS@ +CV0I< v)` |9n]rD;HViuzh?rmbRSUGG)3nGnbe2Pn (.qE!5UC!fO@GYIXXLh=B?p8& UAAƥf#2 ÅIZno`M8iX*};}kYI$'kM -J],⯧v͟ᙱWeg&%aE]qJǽ6H5nfn^. ÈDtq:~()[yY [ncUY<\ ~{Ĭ䠯%|;(է⁎X(&W8A$ Q"h&>R\D$LޓYLѪ` f5˓M؊EQVHToluqu:vIہAlz0j, l^-Y!cIK7a=uhE!"ȬLst}ա(V|@\+/C"ǡZ8H鿄ԇB` ]o`MGz;Ya«Q03 s3ݷ˻_~{{ue!3wnޑ:;2y~r"A GȦδƲS>Ъ`KeR?3YUtޓƸc05/5 "m!H[3L^>+yI!KN@ $wmD }a4ngBٷLuޗM\tÅcPo%~]_2>j/~|6vO ~ hc`1J"ʨSQPX Ѫ/5 c쳍_pz2iKϤ6/49|) '+ w(fDr8P8R |6wD-f~g+QȢ_,=&5* 'D*[^"5["@-CD4GL8o OŤon.QE3$fXyXx[v&T)Z^,训S1>lmuVq(lVƏ>rW ءN[\W"=, lzӛ]+,M>] H$ul ꆬP+̮.JwCv %M44yMghWRXzzjÂq!Wu1Wk\\oM3 ?Uy]C ;erFyyG`!SRIRK6Keͦgc%b#KIfT//^0TNIt=OGf[ J6ߌEVkPF|p!zQ*XS(󺋹 L\}B늯I+W' kD^lj % ^\eq{4ogVlGἵB!=zmĴ^L%vC5qZIyP,:o 1ggU=ZDDRxz8H]{d7E4$>7]iŗmf!r]h{.:?6ωP* !`T0k2;Y06FZU͏ӤW3聾^\(Z.ZiRmI玍]FWHYbb^ԏ,}O܌aLDq4";ۛگnnT@! AX;Agj^1/Ve>U(PSQ֓.~;Jgs #F I7 FrS] yOMIxjVN陼:0)&KQ yLҌ씶ҌןŤP^\(c]T_j @׈ΜOy4rL4 zVJ{E]!iUjLrsX S[nB2勒OTa%ƟN,˺l}&Rd*ޡ!wA,x` :P]>]CD,W u= g[-aMu7i75($%TjwJVս #gGwK'iw3}Bbt_0dG|/ݵ\t4xe푯j, ֜-nObڒҕ&R͹-'M]_whA'|HĚ-Y~HDlPe!QTKfgV_ۃH];6w}FpixX'Oő=@^f]cp.7lʼ㗭?lI30BM\I뉊eb>АNKTGDHh?K[rl./lnY,vʅ(4č wvaR{*4KX_H$SW,R~]u[jkQ4lޭ!$, ̖&jf1۾$䤢kˁd^g"7*!r~i4Vxƒ`b.=7_q͕GY2hdRcV[/NUXFC%=bI 0sO4eUnU_5V>-U,$ AG5mF*|J$^74S\7 @>rXKh]M %T u D^,r=.ORE Qg[ Z{iɯU3C]h_MBe!h vT.vHߩ *a OZsEMf#zաq;&&[.d<+z^ ξ#CH8UUhjY0<9NMбmM\`hh#EPޕ[#Sbx&5&<'4r䆈C DCiKx|73kW*>y8-Z]/*c J#ZT;{dThC-"TiHaD7S&K:E9 0Fo#3`dTCFxFHE0_,lP˝g'P˾zz'Mw4b**llb׬egEa-MA@X6ĖJB^C^TP!>v^ l q(FIW(`E6H7Fh|j!EpƕdHPf,=Kz1>_ db$w v5Fuț1^W*gR .,ſi|[B~$.0@%ƃᵪ'NIJxV3MX(K.[iT]kԩ̮NVݐ`6B"0x26E} nʽs@%!658.Ђ 2W<s,ƳXs $ 0C#YDE`*h"A\@~O'jė'F(1jg*BGi0wun|.Lk:=όv>1[jh+*U@(9DŽcl8 AD< \N\QU'(m) 2QŪߘ(1 wTPmchLX4gX8`߼2Bͭ&Ckn1[A%5N(A.[I'LJ" $ /scz (bQxƠE`]ȆedB27o84EoR,7*iȸ4١k0Cl r3^[30tЀ?zA:M82Fi1栟4c4B+m%y¬7K]D+~jZƍO0̖ y(乸VmHKbuZQu,52nqFrêu]y^ _l?#Rжo314\p>R$}f5fGRŜ/X|" n1vyfYg [Dn.K!>}Y%PwI7T ɐíϭ.%} ZlHe3VEwӛAy`.=. O:~O QZat4njze]҆ոE$eb}OjE"yO @ ׭(Fp(mH/#kHطBtʟSBR&1U=,~V|>GAXAdvye'wox,xMD2pS]В $݀BzǞD%D&JHTްNmD  KTlp .E0dk3{DQ9G m۹f^%eΠ1R85hn˟8$^BymA]^W/yS6 &#M\#d= ^J|tAFZȾGZig?pbT;V Wg#\XK|ՙmI^ӞT^MI"meldY%U r37+T-ݤW2i J^3;^ie2Ykdf,o|r"THn#Bc!`T%J kf 9~ȁπԷxxçlHX8XčJ-GSlGʱ崸Q,u!m~-;S:Q+E)Bǒm7i!dI_ihO#*m-uD0pA_t-RqJx= <7?$%C̄%YSL2ߔrahqo+3V瀳F`V\\G3uQ/{O}iAmDK5?|rȠࡇcq`?,f*^iL!WC_UKQ`;.4JKYDDQCr9bģ)vyLJuD#taF&s"GyZ/N]8b0HBV B tP` ~-9_H9ȇ'kA$p8!!I6z<9X68>GPxm趖4C(2-PŹ'~֢O, UqD}66Q_ʄE d+)QFF_TRȲB>>[F}C.fEmf5t7ݤivsΪ= QOrh뻥 4L6yUEm-n꺘vIիYD6CY:.dD_9GUr$ÔlEtx%W]^ 6F~o=RuٗD/׽ 'ʵvQA&Yg)v-!CU룺&"]uL gON WD`F`V EO>IKGӠ񷨔:f$/- i+GT%RөDbS( JOrLۤ"F`ޮo1/B:_ v(#:K+=.喑sAOjH jz 9 <D~VPƹ[*Y>G1!_z4QPx-ⓔTq}jyyKm}u3LcQhG5:p'NӰ~?*qs.z $yΆ xRBJF@Rn O>c:? AX2 pIfЉ}R$parڇ!xp6^=Ţm"RNb"l\<[`NQeB]vz7O>շI2P+fV'L'aG%1Ivkqb*3^~L}`Y\ t,V swBW(lh] MǧUЌmԯ&kLczf "V mSG̺vՈ1!›|S,0۲cZuZn6yQ@kr>d!,@ p ^y ]EFj5;}V':_R#;7 *bg21ZSE1I%x%[ [%BBQʕr/ %v ~Eʔ'T覮,UkY96m^ߤcn&8e4D<݈-G Y$-o|vaq۠rDΓ^P}[JzoXڋMrv3l`EE 7i"$) mٖИRJDOC@m8jќJ=yH~U'jaz&)~g(mX=+7~y!~۽,\nu ]ϲ'c w]pHe "M,;'Hb.MTe=u ŷ4HmP@Vi}BZ)" 6j"ah1`'?w+q]I]>NIdrcu3/88'7Yxi0ɝ|V;  d'σak;~8`2:&,L_(HXA{W#V[΂e*f6S);۾}?N"ft\"y$`Jc,։D[2 F$?d*b L:f5%5*VUlOwi@[T-|e  !5E/!Ю@i%jVg| N]|ߺ㖨/]-uWB+-9nJNfOQwKO%yKVm%" pEJ Բa2V&V*{3VIJ PrIYR 2t!|+"ˁC EZ glm. DҽQ~#!)G7*/cJ%b"*.ƨT'`gކ#޲ukCI<+Ұ-+A&o5V8B:B6"ЋC %1sHt1^z,CFE@&w @x I' 0EC'!]M@5P"ʢ|i"ȍM'Cɵ <_yP1CPx T?߲^+l9k^_6 C)L]ٮA*gT)48;7-TA!! S-\Cu!Jlb D9ėpt,P;xD*!0L4I7EK9jO pP4_ {$Iw&"5QQVFFI/ܩ!2"sA)F0K)5kkdBS L(vĢ+enf#1؎=N0FI$S1=ɍu @6I즅,af-7ahaGUʲ|?Hvi)R1"T:m!"JjR}#J|QhW*%Րhw[;2q:V/u#V Zoi>oOj5 pTFb!V``g2<ݿR0qgvIEx'ɸhݣ؉nC!ẩeJM˺g5[k=%ΰetk-ngk q?vH&]{̆He=W9t LWYQeo.:pJ?2< dұs%&;BuFoJBg #K“**صNp)*nQAhI@ RHi+ ,HU_/DZ;gUXu h Iw3Qe$8b'=TKyךk0W_pXk4fiמ_ġ:]<_F[bB&=է_"w4&,~*!준-Ji  U.N긲'][1[jɢ`;ɳ]g $f9LPz wLh/ E-D|!FE:۝WS5%E 2K*D<)e[,)]Ya5ڕaҍmSg%?TXQG/lĄe֕)èf cp%)ɧ hG~)c68h! Bt س(v  @Z{+~A*\L8j ŴpLෲ%wa)_X^ }b&1…^#fqϘ'TXj"Fs;Y,.*QE%*;<pwHWqC(1zT?b%i;v н*nk앦Mg&Jl%KYĜUgAvP( An+[{,>Ũ9X$XWk.M'!ź"D_^Zb^Vwz$e;t-Uh7LLĹbCge>9z{nkwhڂ˝`Ӥ'gbܣgd&,.~x_W^|Q9Q/흟~$ is>@>#rDf']}j/;Wx-<$*k l_l1Qk}eosp? EVd.<^fGلKrc6Ix9oxmFLƺmCBbRSg!W[PPφSr{* !X˴jjSJ}L?) ͯF Q@@_ rn6u"{ (lܲ_fkFScm&Q1jH-8kE@QOcL Fm^L+SǪReʯ{l#_9 $n_v/һfW,4 ˢCJ[AiSъA1FW D2mT/I$]sB]k`MYbsgNNlIRA_k-!) V 4\Sf9`jpykJ 2&9Erؐċšp]c=0)n<2 tnj) (a׊O>łE UXW꒝NQH-wNIA׸@M t`hz 6p*{ݕ^Pp(H1'X_TYYU,=mү/U)NN YMLп@2YYWPѤq*8w<$^pB5_q׋#c}`я8[ԷGʋڀ?zuԔ6!D#.~ȻDC9dleM#Ȭ9"T`.^ zX Z-jY,$ aJBr2 0 `; L*y లWA]oȃ,pCP\Ag.A}1 rAF(^)b |rhwlPyG4]hVQR2Q\pDejIk/:I@(0 sFGā E| DPmR ќ k  niÈ9|a;bpP 9pySZRHPm#&r .wK"M㡡Ap#  u0wR r@~AM1"r@!{/sq !sMͨ!b ]_Dña|9~Q=Tw7!bӖqC|A}=pO/TJ'}4ǭl*LmNYrRo!Kp8 =q0Y0AN19z~?Ҕ,`u u3k oF-V9FgoM2ua!|$ 'x'nj[ŽP܉*`ԁA thK߳tES׾ (_%bBD owEuc驀 \|L.:kTQ<ޔUAE+-ռuǒC(I@Y1e-`u1f[fQ C2KӠF~ڬ 6'˅6zʤ/v/vN\NHJH0_OodUg i :fU8Ԯ &;Y2(N#PY"a&SIUY( 9T0HL|`(K'בAܫG\4vC/:7.B dYI6lΧqO]EMLK !췉1H/NC8B^[\4F\%mm "+]u+mtx6Ӻ҅mR T xK %t&hA)w0،33GQ(Nb1[kZovke i]Og4 Cs1Jx?]oFG%8 .#hQ>-"l泟rDa,c0>@0h4hj! yӗNYF5#a0L۞ p&!䨸e=hM >MP A$1Qg+㙬m[bh+e3uc[H=#CN!MK *4*\@J%Q.i/γJe pMV[/4<%e :Kfم-z*X@J* P]0ut6laHv-- e!(Xн@Ľ@Z;nP+p.إ|f@x*Ig)l\XaMf(= rXIJ(9F *8 !4`[8$M%5;. QB-0:ѓj)^P9^hv': Γef!k3/.{!~ a;dfB@(rbd-4Ѩ: fepаXVB&?Π8 1 _!*$27baPWWGrAH'6C *0yBɎq$5+ KaLg_So wO!:/6I嗔,z$.-PS"ځ;H(psZL 7rXIh .78[+#6UiĺGE4v<=RQh c e،*;F]b&"mdA21FaHcMn: N*<"h9DB`_mH]# iH%?BEpi LSYBqҏȠ_*[t:  ]C)()3 m%L@&!767-&i,: `" Ln2V@HC.aP;mOnSj|S B ʸFPwwuqb&\iEFDCi'.m{..6]5dXB .9Vo#fz*e7΂'Xz֌6'C;NuK9 f P0CwM*7n,^-_IҶ )g(jVH5!uܨ 8\n$XH`Oa1u1[%U;D#8>pY^̈\əOww<0kp[E*a-IJ]U* )"6REz`)5f1q,Z8ťwCq;BHj-;*L/E;^pĹ! i·y)\&Zhn$Dt l^%5h_AgYorG`8S@b~T=o3:1g2A<s4 K $w`z{~RcV2L?1||\%gcʟ)r#m0uxWaK_h/Qȥrz1+!>@j UlOOlk PV͠ĥKKOYBI=W{eCٶqh*eޮSSIєmmw]鼻:RȪkU\Mk{#rX)MFb YkDdd02ڢywn*m.C<A\.sk en-us]&EG_eRů[wHgDLz IsVBR4DeviceConformanceTemplateL2t E˖p˥r2CiR[ZX . ު|O(Uݘ"@^PWMFSDKVersion10.00.00.3646WMFSDKNeeded0.0.0.0000 IsVBR@Rц1HARц1HWindows Media Audio 9.1$ 64 kbps, 48 kHz, stereo 2-pass CBRaܷ Ser@iM[_\D+Pÿa aA  * u{F`ɢ 6&ufblvCO;J)rJ ]U  % "" w=  ]UU AA]U +(AA]U 8AA]UU HAA]U + XAA" w=  ]U hAA]U U  xAA] U  +AA] U  AA]U U  AA" w=  mutagen-1.22/tests/data/64bit.mp40000644000175000017500000000012512157371172016755 0ustar lazkalazka00000000000000moovMudta=meta-!ilstcpildatamutagen-1.22/tests/data/variable-block.flac0000644000175000017500000002400012157371172021105 0ustar lazkalazka00000000000000fLaC"  BxLß f 9أT[eFlake SVN-r264#ALBUM=Appleseed Original SoundtrackARTIST=Boom Boom SatellitesCOMMENT=Original Soundtrack&COMPOSER=Boom Boom Satellites (Lyrics) DATE=2004DISCID=AA0B360B DISCNUMBER=1GENRE=Anime SoundtrackOJAPANESE TITLE=アップルシード オリジナル・サウンドトラック*ORGANIZATION=Sony Music Records (SRCP-371)RIPPER=Exact Audio Copy 0.99pb5TITLE=DIVE FOR YOU TOTALDISCS=2TOTALTRACKS=11TRACKNUMBER=01replaygain_album_gain=-8.68 dBreplaygain_album_peak=1.000000replaygain_track_gain=-9.61 dBreplaygain_track_peak=1.000000v #䀀~怀Pe耀 萀@?; 蠀L@??@?߷T#谀@瀋????r退@?|)>????ϟO̟s'??=>4)?gsfI32rk鐀@1?OSL|ɓ$&gM4OҔӥ=?'iO:iϔ?33ϒL3=s?>ϙ??Oҟ鰀p@9g#=?eȝ4J_~RY?OO?M:t?L8ra!!'?)&J2y1t)i)s32yL$LϧܹO~D3'9̙I$O%ӉJiJTy@>S첚tmutagen-1.22/tests/data/145-invalid-item-count.apev20000644000175000017500000000135612177421270022365 0ustar lazkalazka00000000000000UU_UmV+]m۶۶mիkUUUUUpVUի(UUUU%U۶im۶mUm۶WUv۶mm۶m۵ݶmKm۶m_*8APETAGEX8 ArtistPink FloydYear1994 AlbumDivision BellTrack11/11 TitleHigh HopesGenreRockQualityStandard:PiMPIRC Channel:#PiMPIRC Network:IRC.Freenode.netReplayGain Version:ReplayGain v0.84PiMPed:Friday 2002-10-11MPPENC Commandline:'--insane --xlevel'ReplayGain Options:'--auto' (per album)PiMP Homepage:http://vember.net/MTRHAudioExtractor:Exact Audio Copy v0.9 Beta 4 Album ArtistPink FloydBPM71APETAGEX8mutagen-1.22/tests/data/id3v23_unsynch.id30000644000175000017500000000050012212323252020546 0ustar lazkalazka00000000000000ID30TIT25My babe just cares for meTPE1Nina SimoneTALB100% JazzTRCK03TLEN@216000 HI ,^% >}G˿50}KR"J\ĻC圂gmutagen-1.22/tests/data/empty.ofr0000644000175000017500000000020012146763412017243 0ustar lazkalazka00000000000000OFR D@ HEAD,RIFF$ WAVEfmt Ddata COMP%Q_A@5TAILmutagen-1.22/tests/data/empty.spx0000644000175000017500000005735512146763412017316 0ustar lazkalazka00000000000000OggS'ǑrPSpeex 1.1.12PDOggS'[0!Encoded with Speex 1.1.12OggSn'+-]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]tNmC_mm/mmmmUՅ]]]]XUՅ]]]]\ֵtNkmE_mm/mmomUՅ]]]]XUՅ]]]]\ֵtNmW_mm/mmܡm%UՅ]]]]XUՅ]]]]\ֵtNFS_mm/mmwUՅ]]]]XUՅ]]]]\ֵtNqm`3_?%/mmmmUՅ]]]]XUՅ]]]]\ֵtNam}_mmcmҵ0?omUՅ]]]]XUՅ]]]]\ֵtN<m_mm/mmmUՅ]]]]XUՅ]]]]\ֵtNmP_mm@/mmhUՅ]]]]XUՅ]]]]\ֵtN(խa_'@mmUՅ]]]]XUՅ]]]]\ֵtNm}_mmثtbmmUՅ]]]]XUՅ]]]]\ֵtN(mmB_jȯmmsmUՅ]]]]XUՅ]]]]\ֵtN@ma_o/mm[AmUՅ]]]]XUՅ]]]]\ֵtNmn_mm/mmUՅ]]]]XUՅ]]]]\ֵtN_"mt"_jVm mmUՅ]]]]XUՅ]]]]\ֵtN,mj_mm/mmo5UՅ]]]]XUՅ]]]]\ֵtNm^c_m@/mm(m[mUՅ]]]]XUՅ]]]]\ֵtN_Dg,_mUՅ]]]]XUՅ]]]]\ֵtNpm]_m/mg8տoUՅ]]]]XUՅ]]]]\ֵtNZmd#_m/mm#mUՅ]]]]XUՅ]]]]\ֵtND+^_mm+ m`mmUՅ]]]]XUՅ]]]]\ֵtN;mk_mmٯmO#omUՅ]]]]XUՅ]]]]\ֵtN.m~#_'mfmUՅ]]]]XUՅ]]]]\ֵtNPj_mm/mmrWڵ5UՅ]]]]XUՅ]]]]\ֵtN/ƀ_mm@/mm۠mmUՅ]]]]XUՅ]]]]\ֵtNtmx_mm/mmUՅ]]]]XUՅ]]]]\ֵtN1mk_mm/mmѠmmUՅ]]]]XUՅ]]]]\ֵtN[m@_[m/mmڎmUՅ]]]]XUՅ]]]]\ֵtNmFmR_mm/mmޔmUՅ]]]]XUՅ]]]]\ֵtN'mW"_mm@/mmmUՅ]]]]XUՅ]]]]\ֵtNFm#_m/mmUՅ]]]]XUՅ]]]]\ֵtNCmu_mm/mmXmmUՅ]]]]XUՅ]]]]\ֵtNm\_mm@/mmmmUՅ]]]]XUՅ]]]]\ֵtNrp_mm/mmmmUՅ]]]]XUՅ]]]]\ֵtNcmOʀ_mmm@mmUՅ]]]]XUՅ]]]]\ֵtN5m_mmA/mРmmUՅ]]]]XUՅ]]]]\ֵtN8mu_/mmLmmUՅ]]]]XUՅ]]]]\ֵtNzmR_o@/mmmmUՅ]]]]XUՅ]]]]\ֵtNPcmU_/mumUՅ]]]]XUՅ]]]]\ֵtN j_mmfm$OhUՅ]]]]XUՅ]]]]\ֵtNYmV_m/mmߋoUՅ]]]]XUՅ]]]]\ֵtNmp_mm˯mkmUՅ]]]]XUՅ]]]]\ֵtN(mU_mm/mmmmUՅ]]]]XUՅ]]]]\ֵtNq:kH_mm/mmܠmmUՅ]]]]XUՅ]]]]\ֵtNpmI_lm/mm mmUՅ]]]]XUՅ]]]]\ֵtNZmq_?m'/mmcomUՅ]]]]XUՅ]]]]\ֵOggS'B-]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]tNHmi#_@/mm`mmUՅ]]]]XUՅ]]]]\ֵtNP^ԿZQS_mgpmUՅ]]]]XUՅ]]]]\ֵtN1ma_ikAUՅ]]]]XUՅ]]]]\ֵtN_oڵ@/mm mmUՅ]]]]XUՅ]]]]\ֵtNHmv_mm@/mmҞlmUՅ]]]]XUՅ]]]]\ֵtNmc_oow/mހmmUՅ]]]]XUՅ]]]]\ֵtNKmr__mۅmmmmUՅ]]]]XUՅ]]]]\ֵtNkmj_mm/mmmmUՅ]]]]XUՅ]]]]\ֵtNp_mm/mm`mmUՅ]]]]XUՅ]]]]\ֵtNbI#_m/ٱWmmUՅ]]]]XUՅ]]]]\ֵtN+.O_mmu/m mmUՅ]]]]XUՅ]]]]\ֵtN7ms_mm@/mmԎ%[mUՅ]]]]XUՅ]]]]\ֵtNԛ_mo@/mm`mmUՅ]]]]XUՅ]]]]\ֵtNfηz_mm/mmޠmmUՅ]]]]XUՅ]]]]\ֵtN{_[m/dm mmUՅ]]]]XUՅ]]]]\ֵtN_mb_mm mmkmUՅ]]]]XUՅ]]]]\ֵtNEmT_mjҿmJmmUՅ]]]]XUՅ]]]]\ֵtNsFs_mm/_mUՅ]]]]XUՅ]]]]\ֵtN*ԿR_mm"mm×UՅ]]]]XUՅ]]]]\ֵtNmp_z'YmӬ5oUՅ]]]]XUՅ]]]]\ֵtN=mq_mm/mm߀mmUՅ]]]]XUՅ]]]]\ֵtNwmE_mm/mӭmmUՅ]]]]XUՅ]]]]\ֵtNmo_mom:mUՅ]]]]XUՅ]]]]\ֵtNP`Կm~_omoUՅ]]]]XUՅ]]]]\ֵtNm[_mm@/mmmmUՅ]]]]XUՅ]]]]\ֵtNfη]_mm2/m|Ӄ'UՅ]]]]XUՅ]]]]\ֵtN'mq_mm@/mm٠mmUՅ]]]]XUՅ]]]]\ֵtNmX+_m[m/mm@mmUՅ]]]]XUՅ]]]]\ֵtNbm~_mm/mmҗ[mmUՅ]]]]XUՅ]]]]\ֵtNm[_mm/mmmmUՅ]]]]XUՅ]]]]\ֵtN mZ_F/mm`mmUՅ]]]]XUՅ]]]]\ֵtNT,v_@/mm܀mmUՅ]]]]XUՅ]]]]\ֵtNtFO_mmmmUՅ]]]]XUՅ]]]]\ֵtN8οmp_ummUՅ]]]]XUՅ]]]]\ֵtN&_mm/mmmmUՅ]]]]XUՅ]]]]\ֵtNmOO_mm@/mmmmUՅ]]]]XUՅ]]]]\ֵtN3mՐ3_m/mm`mmUՅ]]]]XUՅ]]]]\ֵtNlmJ_m[m/mmՏUՅ]]]]XUՅ]]]]\ֵtNQm}_mm/mm[mmUՅ]]]]XUՅ]]]]\ֵtNmo_mm`mmUՅ]]]]XUՅ]]]]\ֵtNJTӶS_ml @/mڀmmUՅ]]]]XUՅ]]]]\ֵtN~mw_mmMmmmUՅ]]]]XUՅ]]]]\ֵtN2UI_mm/mm,[m[mUՅ]]]]XUՅ]]]]\ֵOggSO'r&:-]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]tN mp#_mo/mm׳mmUՅ]]]]XUՅ]]]]\ֵtNBmv_mm/}WmmUՅ]]]]XUՅ]]]]\ֵtN'Կmp_m/mm۠mmUՅ]]]]XUՅ]]]]\ֵtNpm~Vmm/mm@mmUՅ]]]]XUՅ]]]]\ֵtNPmn_mmCdmmmUՅ]]]]XUՅ]]]]\ֵtN7mq_mmomխmmUՅ]]]]XUՅ]]]]\ֵtN:Կ{_mmѠmmUՅ]]]]XUՅ]]]]\ֵtNP\moO_m3UՅ]]]]XUՅ]]]]\ֵtNvF؀_mm/kMmmUՅ]]]]XUՅ]]]]\ֵtN=Xm_mmݠmmUՅ]]]]XUՅ]]]]\ֵtN$mi_mm/mmѠomUՅ]]]]XUՅ]]]]\ֵtN/ _mmGm mmUՅ]]]]XUՅ]]]]\ֵtNvmh_mmomUՅ]]]]XUՅ]]]]\ֵtN*mz_o/mmmmUՅ]]]]XUՅ]]]]\ֵtNhmx?_m/mm mmUՅ]]]]XUՅ]]]]\ֵtNcmJ_m[mg/ZmmUՅ]]]]XUՅ]]]]\ֵtNNmq_/UՅ]]]]XUՅ]]]]\ֵtNqmd_mm@/mm mmUՅ]]]]XUՅ]]]]\ֵtNukR_MmmUՅ]]]]XUՅ]]]]\ֵtNkms_o/mm mmUՅ]]]]XUՅ]]]]\ֵtN{Կ_mm0/m[ mPmmUՅ]]]]XUՅ]]]]\ֵtN1mn_mm/mmї[mUՅ]]]]XUՅ]]]]\ֵtNFm[_/mlmUՅ]]]]XUՅ]]]]\ֵtN'my_o@/mmPUՅ]]]]XUՅ]]]]\ֵtNrFKX_mm/mmm5UՅ]]]]XUՅ]]]]\ֵtNmQ_mmu/m;ͿmۅUՅ]]]]XUՅ]]]]\ֵtNԿmN_mm/mmݠmmUՅ]]]]XUՅ]]]]\ֵtN mE_lo/mm׈mmUՅ]]]]XUՅ]]]]\ֵtNymi_?/ym֐oUUՅ]]]]XUՅ]]]]\ֵtNƿӚ_mmٯNmUՅ]]]]XUՅ]]]]\ֵtN8 dg_mmm٠mmUՅ]]]]XUՅ]]]]\ֵtNP9ԿO_mmj[mUՅ]]]]XUՅ]]]]\ֵtNyƿ|_mm@/mm@mUՅ]]]]XUՅ]]]]\ֵtNNً_mm/mmڨo4mUՅ]]]]XUՅ]]]]\ֵtNfJE_mmkm۠mmUՅ]]]]XUՅ]]]]\ֵtNRm~_2[mٯmkm@mmUՅ]]]]XUՅ]]]]\ֵtN@mu_mm/mm_mڵUՅ]]]]XUՅ]]]]\ֵtNu>ˀ_mm/mmmmUՅ]]]]XUՅ]]]]\ֵtNm@_mmͯmdmmUՅ]]]]XUՅ]]]]\ֵtNCmj"_m/mm mmUՅ]]]]XUՅ]]]]\ֵtNlmzH_mmmmUՅ]]]]XUՅ]]]]\ֵtNGm\#_m/mmmmUՅ]]]]XUՅ]]]]\ֵtN&οm_mm/mmmmUՅ]]]]XUՅ]]]]\ֵtN9mkN~_mmUՅ]]]]XUՅ]]]]\ֵtNsmn_omڗ/RmmUՅ]]]]XUՅ]]]]\ֵOggS'ț-]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]tN=mr_mmm+sUՅ]]]]XUՅ]]]]\ֵtN/mm_mm/mmmڵUՅ]]]]XUՅ]]]]\ֵtN_mm/mmzmmUՅ]]]]XUՅ]]]]\ֵtN)me*_ڴmM mmUՅ]]]]XUՅ]]]]\ֵtN#mg_mm@/mm7mUՅ]]]]XUՅ]]]]\ֵtN2Կm_oڗm@/mm]UՅ]]]]XUՅ]]]]\ֵtNԿmQ_m)m`mmUՅ]]]]XUՅ]]]]\ֵtN=d_mm/mmo[mUՅ]]]]XUՅ]]]]\ֵtN"mTc_mmQ@mmUՅ]]]]XUՅ]]]]\ֵtNbmx_m_G/m`mmUՅ]]]]XUՅ]]]]\ֵtNJms_mmHmm!տUՅ]]]]XUՅ]]]]\ֵtNm__mm/mmoڕUՅ]]]]XUՅ]]]]\ֵtNF][_m/mmvmUՅ]]]]XUՅ]]]]\ֵtNPmoj_m@/mmHUՅ]]]]XUՅ]]]]\ֵtNI o#_/mmCUՅ]]]]XUՅ]]]]\ֵtNzԿ^_/UՅ]]]]XUՅ]]]]\ֵtNmX_mm/mmߠmmUՅ]]]]XUՅ]]]]\ֵtN.mI_vm@/mm׿o7mUՅ]]]]XUՅ]]]]\ֵtN Y_mm@/mmޗ?UՅ]]]]XUՅ]]]]\ֵtNƿmH_mm}/mmmmUՅ]]]]XUՅ]]]]\ֵtN mx,_oo:/mӠmmUՅ]]]]XUՅ]]]]\ֵtNCmh_mmCZHmmUՅ]]]]XUՅ]]]]\ֵtN5ֶm_6m!/Ƿm͝UՅ]]]]XUՅ]]]]\ֵtN9T_mm@/mm mmUՅ]]]]XUՅ]]]]\ֵtNom@\%@/mm#oUՅ]]]]XUՅ]]]]\ֵtNjmn_mmׯmmѠmmUՅ]]]]XUՅ]]]]\ֵtN5m}_mQm mmUՅ]]]]XUՅ]]]]\ֵtN`_mm/mmmmUՅ]]]]XUՅ]]]]\ֵtNxI_mm!/momUՅ]]]]XUՅ]]]]\ֵtNW_mmٯmmmmUՅ]]]]XUՅ]]]]\ֵtNBԿO_o5@/mm֠mmUՅ]]]]XUՅ]]]]\ֵtNu6V_mm@/mm'mUՅ]]]]XUՅ]]]]\ֵtN#㏀_mm/mmmmUՅ]]]]XUՅ]]]]\ֵtNimR_mm:@mmUՅ]]]]XUՅ]]]]\ֵtN:VB_mۯp WmUՅ]]]]XUՅ]]]]\ֵtN@ֿ_m/mm,mUՅ]]]]XUՅ]]]]\ֵtN&6_Vm/mmܐUՅ]]]]XUՅ]]]]\ֵtNPXm\_j呯hUՅ]]]]XUՅ]]]]\ֵtNdοV_mm/mm`mmUՅ]]]]XUՅ]]]]\ֵtN_m H_/mm݀mmUՅ]]]]XUՅ]]]]\ֵtN|Os m|S_6m@/mmڗmmUՅ]]]]XUՅ]]]]\ֵtNPml_Q[mK`[mUՅ]]]]XUՅ]]]]\ֵtNm\_mm/mmmmUՅ]]]]XUՅ]]]]\ֵtNwmOʀ_mm/mm`mmUՅ]]]]XUՅ]]]]\ֵtNsmE_mm@/mmmUՅ]]]]XUՅ]]]]\ֵOggS0'm,-]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]tNmh_mm/ҶmmUՅ]]]]XUՅ]]]]\ֵtN>mz~_o@/mmUՅ]]]]XUՅ]]]]\ֵtND,_m/mlmmUՅ]]]]XUՅ]]]]\ֵtN%F#_/mmԀmmUՅ]]]]XUՅ]]]]\ֵtNSmI_/mmmUՅ]]]]XUՅ]]]]\ֵtNLm{_mmkm-@ mmUՅ]]]]XUՅ]]]]\ֵtNma_mm@/mmҗ[mmUՅ]]]]XUՅ]]]]\ֵtN,~܀_mm@/mmmmUՅ]]]]XUՅ]]]]\ֵtNcoƀ_mm/mmzomUՅ]]]]XUՅ]]]]\ֵtNmma_mm/mUՅ]]]]XUՅ]]]]\ֵtN8mp_mm/mmހmmUՅ]]]]XUՅ]]]]\ֵtN|mY#_mڵ1mmUՅ]]]]XUՅ]]]]\ֵtNgfZ_jbگmmUՅ]]]]XUՅ]]]]\ֵtNm~,_/mmmmUՅ]]]]XUՅ]]]]\ֵtNgS_mo*ȯmmUՅ]]]]XUՅ]]]]\ֵtNEmx_mmQ/խa?mUՅ]]]]XUՅ]]]]\ֵtN'mi_mm/mmܗmmUՅ]]]]XUՅ]]]]\ֵtNmO_mm/mm@mmUՅ]]]]XUՅ]]]]\ֵtNmO,_om/ZUՅ]]]]XUՅ]]]]\ֵtNXmu_mmmHooUՅ]]]]XUՅ]]]]\ֵtNO_m`mmUՅ]]]]XUՅ]]]]\ֵtNgO[_joA+mmUՅ]]]]XUՅ]]]]\ֵtN(ԿmH_mm@/mmmmUՅ]]]]XUՅ]]]]\ֵtN mT_멿m`mmUՅ]]]]XUՅ]]]]\ֵtNBm_mm@/mmmmUՅ]]]]XUՅ]]]]\ֵtN}GVmm/mmڨmvmUՅ]]]]XUՅ]]]]\ֵtNjmU_mmm`mmUՅ]]]]XUՅ]]]]\ֵtN"mh_mm/mmW[omUՅ]]]]XUՅ]]]]\ֵtNFm _mm/VmmUՅ]]]]XUՅ]]]]\ֵtNMX_mm/mmmmUՅ]]]]XUՅ]]]]\ֵtNrmu_mڵ/mmUՅ]]]]XUՅ]]]]\ֵtN)mx_mm/mmUՅ]]]]XUՅ]]]]\ֵtNmb_mm/Ӣm%UՅ]]]]XUՅ]]]]\ֵtNƿVƣ_/mmڀmmUՅ]]]]XUՅ]]]]\ֵtN|mc_om/moUՅ]]]]XUՅ]]]]\ֵtNJm_mmѯmmmUՅ]]]]XUՅ]]]]\ֵtNCƿcE_mmζommUՅ]]]]XUՅ]]]]\ֵtN.ms_m/mmڠmmUՅ]]]]XUՅ]]]]\ֵtNaܿ γ_oo5/mmԀmmUՅ]]]]XUՅ]]]]\ֵtNIm_mm/mm?mmUՅ]]]]XUՅ]]]]\ֵtN<m՞_mm)m mڏmUՅ]]]]XUՅ]]]]\ֵtNNX`S_mڴٯ-ٿ@mmUՅ]]]]XUՅ]]]]\ֵtNW_mQp mmUՅ]]]]XUՅ]]]]\ֵtNdF_mmf/ ڵUՅ]]]]XUՅ]]]]\ֵtN~mb_n/mm׀mmUՅ]]]]XUՅ]]]]\ֵOggSz'F]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]tNZmH_mMmkmmUՅ]]]]XUՅ]]]]\ֵtNucG[m/m܀mmUՅ]]]]XUՅ]]]]\ֵtNP6NӶmK_mmqf[jVڵUՅ]]]]XUՅ]]]]\ֵtNmf_mm/mm@mmUՅ]]]]XUՅ]]]]\ֵtNImw_mm/mm`mmUՅ]]]]XUՅ]]]]\ֵtNRm}_mm@/mmԠmmUՅ]]]]XUՅ]]]]\ֵtN6?F_mmƅXg IH # !Cm%b!`5oqsBor9!h\#A:&N'DT;!dATo'diXxW~5oǿ <D2lpoUMxzc}e:V!5-=0g)@iitt"9 2Pܝ  .n $DzC&M8{&L{z @ {<6D9avDAo 5ļJ.%Vp ?DĹf2'UM'R%W&%#թ c\g"l:'#VxL`4 F{E#icå 1@C08: t"R9S'YW`ʢUx|,&`TP~UƉύC =X8v4.FD%no@ƸYo".%7$U\6|"M<\۟BcS<!'I,Qi'#!сYhi=o7 yK]R_)!}B"´Ao?\5jk+4lp<AMt Flwm5"ֹǽ9n7e0 k9 90Uh :lbwbh bPaM6Be5Z:rk3'_tF&У 1 J%1@g$ m'eHfv1`Z<JaĊ:" !fa`= z86R|&-!A+lTUk)x)o$b6VJ>wrPN~;[Blo>-dl!NW59naAit(@c/gAl?S('Ĉ᪀Xw8vJSyU&宭.r#`aI-j{wU&lq0E"jE&qz=?j?T 0rKaΒp/rxnVۏ/IIcFdېK7uRsyϸS=hF4F & &p8fhd( @- @ Fx4 )&^jN0DNM@ѮPf,( I03,ŅU`)ּx+fA :sL8@o3fH0A,eD ``0b.9oe7f8qeA&:k ܽ-ߴ.YJRm\8 89^_nެnV~W2/0RRaϿyTo>)0Ϻu%7Ica%gڔ+%ϵ%|F%ry 7ITJ `#13aIZ[ fdmuLy+02&J}jCM1,2B'f . jEz43A+ (`%ӯ-TxՁ3fJ}1 \V#,N3=51T{,췬%iTի-VdYoV>{| -91A .¥ޭR]ʷ￟;,sg?XZ= dBQ.J37,A)類;c,gVJ3 ,H\8Pe,VH)0e`Pc(=?;`Ёz P Nb9VL׻A+8K>Y(1[4jїJSkAC/,~.ۍƥ5igbZ8srj?~}϶h?R[>R zWs}vj}e~ֱϱ; *2pAijCppAa9Є`Ѡ%SdxhKs@r'Yp<#6a & In JB|<7$ <̱g҃1JFfMP:%$X)Ѐ0PB$˾tF#P8PGTkI39oJ *׊"m@jEH&Lg-߂apohidƗpG`-PvܕfQR[FkWžr[ۘ][[}ky{?omgX`LH{.??g H  ]oT'OH=\K>hbHCcv|O$ nkQz*V։-.߈ #caQq¯T]ޖL ƞΤdm+X`@ ѧS,!Jc @@Fl4B 21~+C)"@D 0a6b$ &, : @ab1"% `QS(*0D!"!,$r@NEc.cOԉG13dn`kG,*I09* n<2XhQ.5ru@qbK "#Ӊ\ЃTBZM̈Rc :[%J֘I[,-V:l)P Ug J(X |LXKkOR2 RŰ1I  Ě|QX"#tj5%,d钂" [ɒ1XVY3J4aB!B,Grz*)c#bV@JVBm4w zr@fAA.ۥ)dhLYerGqe"ջ.-eP`UFPT ?#i<1"vÄ#d1ƒSem"{GSJ`)A xHN1Kğu]N.y_irc֗CĮY9MfU:0J&DA |2.A1\]Rx Mqi둾ZB+N\]JbT'C֬Gfӑe̙EP8{Yf }HW=D6!J@B+x*ӑQ$LgȌ׫rp [V쑂¡SGW3M@x1!z&*,tE1{s%]VmyZt>"?Z8!ܷᰔX_ x~,V,`bF 0C350@QE  @q2`-Pn131b`8 _1>*'4Rm ͈L 224xNQ`TncB!RYfJX_aRZXuW ؄J$dCML`08G&ɬ>>~ԋ8+[w^r1~m^r3\0ΪFӆo-kE=bƲ!( 357V)@#Ntq(RBhTEV",+G8Ô-*>Ho"7}m{Y2"voѺDҊLxZ7 c =aNs7i` C(\4 sL(D311΀%B(0d!xˋLgYe&54Ey#AZoS(˘0L<ƛ2%ыzR}1ƨю(I3R$:5u@5b*Uy߸g0U<;KR[uG\Pl2ۭ\#QHH1FrM<1DbCeSa1J ˪/d~ .vQ&z9vQrDaIѥ9r^{N l׭%7N v"C2sdxUۺ_/uV~[r [rH:L D!c :.w"qVPOsdѢE1kЈ$@@ WFf&抍FF&?fFCfC4lhJuP%fF`䞗BMD@ q0f)zQx˗B>m啓WTÃPP.g5ŁQ ;Ů2T#B'G4('G@q.#TU&9g㛑H1WGRͨ{\+ѕee}Yw25Cn\7l l#,WDoF?푍n):ndMԐYZX ޒlB;xI)ZE8WVIԬǒM*&&Zrt3$m,'l"({PZ(>{~gH$Ai)NPm%ᢕF+iccЌuɬA!dnLmaN$=:.iɧxc@a2:I2@ c#CewL(ABd8j(2hHhT @HT4ҹAJi'kq[&lسNz(ZyvHD0~{po$Re*&).o:5OXIɦd4*/MCPkVlgg`&5#4EA}rW}HA+gʆuuz"OX^65H r:‚y!PNOkДxv.U գ*hv9'ʥ]n)2¢b& ')ak!c!(A |z@~Ыl_WiZY^֢=Ni܏1L@/ *&A 7  K0({1xM BM6Cpndsk LoYe"by:nif P(&^ E|-0T,AAfHh_׌\hPXBxj% C0 pCb \,񉱕R2U\a`c. $uRA$Yt %t -T`/E EJzgnwVlc!Fk,jHI[8GbLMCMȖc')FXU6)@Y·*彬Y kcs0ͦ#y4y-YS>wu? &hc ^8`4<(-VN;,vn"2$t 1 6D*rH7D{`=APa &g<%ь!x', \O3' 0-1P*M@V11@Q`"&Z\BK4ϝF)j5h)4 bɘ$$<,Y`l͑RWiNY~ gȊUJiv_igmz8RJXcc4̏:`t1|ً:(FƜ Gx;qʡF5[)!"&dIպVM,2i "擰&tBjB ؄O>%T v]l֯VL `2"((``U&Tl@y(,`xVaP<1H 0DZTLQt|D0PP%`@i 9 `3+T!R&wcP(%hm)(NRS+km9i*Y1 1ìW0;@F..͊80*D ʵ77z+sJ*@3:kdJSdM4pw'Yak(^\;2m͝Axq\%KQ/m%xya)RYx+F6n5[ս즖wt5ʴ3D8  BU+o=G` XA{AX25KMF'\j)[*%a Xh& Ai~k_cPJvkƭ} {xt`x>&nꡄÄWg2:[8L,U0pP0嚐X"c@  ` 0ɢS C`b 5*"AL9\A S !vAP9* IX( 醭q՛UQeMZ6I kY% ̰襈am=m,,sZvݧVzskw6T<,#mpck rSѡ]b"g%0@`9]*z]cCmmZmD.%+ ݮq, Nř4:k~3c3}Duъw]~סW-N@bXd舞`8PP, C ᏣBc eU5PD\Jƍ c *#AAԱRiwB B%n~ݚVQJ)4 řFaoC΢MN!9,>v-c˱ Zݕ(-\atru Rp^ۋKvjz@x*Lr CPPm[)@P FMOId}+Z̓Dpt'Pi 0i̟ŧyAzgYhd@19g A ihiK@gp#UB/GuW?BUÔlW"AսMrX%BLC0C4*(&9)2 Lz\mA@1F !,0hHbUPHq (%(׺8rF( wG.u6\ K:e#Xؔ1[iMwFB;p#.Ǝol E^&GY\̥l8jP՞UExc[hyS4ϡ5^kk|Ah?M1}W^Oosz}a*dBd"5 meR<F3^ض'Y1Xf8SQ,. DƐ2T'ZGWi8 Ơۻ&i܅O=ܿ{NPi}@1O1110(0 90X2+*憁D(#<&3v֔HbnUU\ IhpfH4h;̓6Œ8 mI]@RhiL<99I@Pԣ3Lvpf?R蠪|ځսef2u* {kw7e/嬾-Y붱s(&d4yHV|: Lf JwY\Y~yV/XU".r "؅Nb1AɎ 7ird)X㯃 04>("Zw(,pAPhIq@u,e(Ռd IugI`e0MqņP̠KɈi:$8AlT 215$e5` xњ4  ) >bfH ɥACV1lӍ%BZsq֒s2sXǯJIJslHʯ{Zsl_ŨMu$Rq#qW&r HcsIݺ KhڶD%9?Wpf[n,E"u[2/(T^€:m60vetiܶfn,Ϝ>00ҲwcRTN9"\#5(|p2 ȣ Kh 1P`Q"^md\ 0`χ/0` sG 18\<kDc"/f@Ƅ20"MGN#IK0 ,ۓ.LP8`*9غquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbaOggSۿ;g"zquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbOggSۿ;KazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxOggSۿ;7JbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuOggSۿ;7ޝxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquOggSۿ;?"uxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazqOggSۿ; 9uuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazvorbis%BCV@$s*FsBPBkBL2L[%s!B[(АU@AxA!%=X'=!9xiA!B!B!E9h'A08 8E9X'A B9!$5HP9,(05(0ԃ BI5gAxiA!$AHAFAX9A*9 4d((  @Qqɑɱ  YHHH$Y%Y%Y扪,˲,˲,2 HPQ Eq Yd8Xh爎4CSR,1\wD3$ R1s9R9sBT1ƜsB!1sB!RJƜsB!RsB!J)sB!B)B!J(B!BB!RB(!R!B)%R !RBRJ)BRJ)J %R))J!RJJ)TJ J)%RJ!J)8A'Ua BCVdR)-E"KFsPZr RͩR $1T2B BuL)-BrKsA3stG DfDBpxP S@bB.TX\]\@.!!A,pox N)*u \adhlptx||$%@DD4s !"#$ OggSzۿ;!?q]x mutagen-1.22/tests/data/sample_length.oggtheora0000644000175000017500000004000012157371172022122 0ustar lazkalazka00000000000000OggSzI:@fisheadOggS[}_h*theora6VOggSUEH6Ro+@fisheadOggSPi=Rvorbis8OggSUEH6Pfisbone,PiContent-Type: audio/vorbis OggSPiKvorbisXiph.Org libVorbis I 20090709ENCODER=ffmpeg2theora-0.24vorbis!BCVcT)FRJs1FbJBHsS9לk SP)RRic)RKI%t:'c[I֘kA RL)ĔRBS)ŔRJB%t:SJ(AsctJ$dLBH)JSNBH5R)sRRjA B АU@ P2((#9cI pIɱ$K,KDQU}6UUu]u]u 4d@H d Y F(BCVb(9&|sf9h*tp"In*s9's8srf1h&sf)h&sAks9qFsAj6s9j.s"Im.s9s9sspN8sZnBs>{sB8s9s9sАUA6q HEiȤݣ$h r GR TI) 4d!RH!RH!R!b) *2,2,2밳:0C KMXckZiJ)RJ) YdAF!R!r)BCVذ:IX`!+TPJkB9'*sNJJ9!b˝s B)S霔Z:*)[、Zk-{ )ZVSk-{9K1{b[KXfD # QJ)Ɯs9礔1B1ǜB!R2會B%RJƜB%RRsBPJ)sB!PJJsB!J)%9!B)B!B!R!B(TR !BRRJ)J!PRJ)RJ !JJ)J!BJ)J !JHRI!B8A'Ua BCVQBI-H)&H9'9"BRLJ -tIK):HS)@` ! !2C$V2hp@b" pAw!Abq$xnpNQXh*,.02468:<>@ > "   OggSUEH6x_OggSzPfisbone,[}_Content-Type: video/theora OggS[}_G" theora+Xiph.Org libtheora 1.1 20090822 (Thusnelda)TITLE=Sintel TrailerARTIST=Durian Open Movie Team?COPYRIGHT=(c) copyright Blender Foundation | durian.blender.org0LICENSE=Creative Commons Attribution 3.0 licenseENCODER=ffmpeg2theora-0.24theora(kIJs1R!1b!@mSgVx9[l*hT()$Zy9fS xU)$|<AV* b!|< @86y,a/ǃbVB2E瓙e0%R8H!j4 b@F"`,@!AAP!QQ@!AQQP1AQQQAQQQQQ!1AQQQQQAQQQQQQQQQQQQQQQA!QA1a1Aѱ31pSa5u!bSFtт3tvwT'Fv11111111111111111111111111111111111111111111111111111111111111111!!Q!AaQa!aBBBA!A"BBBAaBBBBA"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBA!6661!Q&666166662&66666666666666666666666666666666666661QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ!!!Qq!!Qq!QqQqqсᑱAqAq!Aq"QAq"RQq"RRQ"RRR"RRR"RRRAAAA|^\t}^?/f'|{rE7~DR4̮؄ La Rw;i]/GPld~rbWGy' UHAUHƂ3҃]XVy̱R"P Y4GxþG+1g oPY]Jw #͵I2ߎLbU'"Lcbusi.webHVr.egEva4mʣ(TF匷} Mi;:Zt|*Rz1jɦtavV=hK 5='$Mu)UsCa4{Z̷?;G3+\h&i=Z _|G|YeJXu| UsYixmG'=pU mvF~a nc SP cIk,:=E d63L塎?#=Э4\XM?hSP9to8R:>! ڤM.een)PM+mt<cAedzRbH6+Iʈ[u~a+W`APޥ0k෧ވK ͵Jl)FJP$^!g/9N߫xcU8$/Jm3QsVFǕ)#H2imqKGBbimh@R:iIMa4 p{YY`DHJmc*˿0M+mt4T_~ 1AmyUHFHJi3ms,p!v/{W 1j f˖#´/z8q1mF /Gʚ>#q<9U$OQ mY4/끦m07$hOeDO-a\8UIedBsᶪ^3c^Jւ6+I8c 1_$@AXE09VapmZ,H8$wYb; 5E|<C 1W^I `h'pgZV&9+& `  Pw,xCM1ϣ; 5EC !c:^[Hs`9 K2%CRcJ ; C4w,|xCM1ϳ; 5E?c!>LƋtӢ R;B$h<#ɥdMp` 4`\~w(BM1Y.C 1Bz [-@/; )3Rp˟`[Gz ́ Yp`8 wYxbG%vjE!C @w:#鿮S_1(#saÚ`uj>xΤ Ug  @@~w(BMQB!bo 뻿bXځK` pZ3%\-V8~@ $@p~wo-BM^>C 1cm_pGhU @ ~5?Iŕ~ä p8H`U~wo-BM1QxX^a11B9\LppI R64%!$"'| - $ (^ww-BM1Rx<0c1BmzDf2[pל -/| AÁ48h ^ww)BM\x/C1R|g^~1o[ #Bx@)EH2S9R Cx&1(p8 $^w|)n_)][ PSK_c !0ڬߛiM[OsF J0@Q"((qNp` ^w|,1e -o=G` s4UGS\"bцD d>C3}>ϟȾ_3*RNG^@O2x"%EO*~/D?G@?7|7|7|7|7|7|7N]ꪪ *H$UUHBRUP!R!@ JT%TU@B JR@PRBUPJ IB!BBP!B!!B B!B!B>=z{|7z^Wz  +]|oW>/'/ \0kç>H lY 0 =l~mz`ab:/{ O_:[>>z{U<8-aư:X,oza>W>\ìK:;L:ޞ`zz[\6yGsu[&z-K:>l0/cAf:D9o_uWJ,nn,:fsv 3`I_ 0R(6BZ7&[ GO(LOLC5]|ɔ% 7 U ແ)'`h06dX'#fѦEJ'b& 6TjQ5F4hѣF4hѣF4hѣF4hѣFF4hѣF4hѣF4hѣF4hTjQ5F! @. $I 2FFFFFFFFF@w`}%I㍯ LӇ:<\q kyw~,l}ڽjy{W=ojj+uB.0@8D9ɑ9;.z7ʮ>B(';6 ƀOBFcbH|gSPxcV 0\tuzڟj@>YJI5P{kM9{O^;ȺQarI h;~efl"D@gc`@G_ KUUUUUUUUUT*R)RT!UPUQU@EAB)Vb!B* X` Lj` #cm`l=>>c|*~Thj߽}/W، ( pSZQ>_h t14;OsOOP}}AK=% 2&`p4`9}篾{ja/pN|Kyz:RŮAy.y `G,[ `EW%5(2<+.RQYx[5XH+zYGRj)"Vdd}36xn>}8;9>}՞3oώodw3.s 7=_<o{W{}ڽςڍ08;5&.̈{b nwJ(l/f*,1)J&C!t}HpaO%a>p rjQ( [Xp ? H L &t4C h0|0 #=(Ȝ?#xJOִ\Dz r@DI9t 9qSAҞpB(#.GȨ;!Kh 0 ~:#]EÁx\ft)^$P@9 Dz}$DvC.:3z<8sD` 瞟ǿ7|7|7|7|7>o;UUUUUUUTUUJ5+UQ5VJJjUX Ҩ*P*U{wMwZUwP jݹ!Bvݫ3t !q V@BD 9j!4H$Q*P!1`D%P!B@!B ꪪ}3B?]]u]tw]ttvu]u]uA]u]u:͂$7<Ρ8N+0izPo;>C!iЏ ,_m0 B+%\6 8y?L'M;#jgc?0v7οb9@N>)î}."x/ /Y]t 1+?͛SxؿaN`)<9<K͓&^e";'//G/I~~`L^n^zoٿ~yazox7й"ݟ/x7<vYH>b>0&k,m~u4R? ]^w8z3sHwʑE(/p'2wa{=dRqqqq̯N88888888wqqqqpqqqq33333v'3i x̻2{{Wڽ>{W'"(c"$ I%W jΔ ~J[Ʌ7*8" t L2C|T*)ѪxKG3Q'!J-(.p Czx: pD~ל Z'@ۺ d *=`y|rcN6pP n؁'"M|`Z9NπYbҜyluNxG>;u7_V7oP|3<77j]ꪪRTQ(UUURTDEUUUU@RJUՐU@EUUJm`#V֪TB%UVT1@aQEVҖYkX (H@!TVZ4-6 "! Vنml`@ lccj@! c!,!}I{!pϟ>|~gϟ?uϟ>|k篩:8Џ/8M-(s;yv~$08 t/n\^)C98stw9^~Qšsg3ѲjlNTQ _; , ~`dE#UvG.(c@`yts>@w?3BcD8=#9n#^=~ wҋy/#EX6g X8.-=~qDp0X_8G_`p؞l!]@[.[.ڐ2Lp2;<4lGʘ;령C 3qan6 Z%G9߮Ğ)^-|۸=lW@C'Gq=S;|i~G؟`u~e恑~;1Fv;C~WAt;=-j\t2#ۜ`"oB??Z]Rwϳ?hgsnm۠q{OggS4[}_ک<Dha j^5xf w0f͛6lٳf͛6lٳf͛6lٳf͘ ^+^f323>ܙʥvՑ1*'),Z" l,& nx(]Q,815n]*@ഄ@!t*ZA grBXDDGOY=%hfITD"JF n&`@)ٕlY؃O/G@@yWp4Gi# )J%;#t]#g'ô!F. 5Rb,1<}%;K!"̝g: Ea.'>'q.#0QXs{L<66)Ow{/SG0q=XHz>}7o7|O`zAbwp0xTUUJ(CFJ*-@0pT@*QC URUR* 2fR֪ 05誙0`nȊ* l\(6]gl3ulm6*URUZu[ZTPjU*mUSU*EwAgڮ?S ϟ>|ϟ9>|ϟ>|F|jX`c/йbW?r):9د;Xi"x}+ 4m=tOU9S5 +N:9`9;aXq~f#P[,B%cAb\PG:cE`GݑF\XvK6bź6aqÖ!\?]{GAXo>'c ]=)Zx@eܷ[忴,u$xr;rm,-qKSav?:/~NXh>9`psqgk?-D/ M:w_ W@{mߏ ?b}t וxf f͛6lٳf͛6lٳf͛6lٳf͛ב5+¼+׆l^ bJ9!E#jљf01 .~0{_eW+#Th4Dc  9(.Q P aaaaaaa`Xa`;wnݻv۷oC|>fffffff`}=K3Y}\:XdNN\fyle +ڽڽ=ϣڟ~xW4^b82*ObQ:x$hi"R9@`"p/ۡyé‘C8dbЉ$@Ȃ@Β讔GsOry@ $N/A@O7aȢ|z98C !DH-#5pDZ(GOX郰)Zpp $ |e"C,?<2m1p4N 9𲧕K$B/j815 paCyWN-:DPb!6QSp']=mutagen-1.22/tests/data/xing.mp30000644000175000017500000002002012146763412016765 0ustar lazkalazka00000000000000du=@"`1 @?˼N(|rrDO xKإ2Mw3ŕQv. sR܄$dyA 0#4` ADYa ^O-c7 EbJ!SwukrA%5:=ʹjw0l%RPn@MD] uؕd oA` 0= [[6mes| !)V+ -.Xڤ4ϹR[" !mdj0< =|TVYrK>͎pu1D{-eRa7tfTb)JT*vdkB@0=`PTdZOz SGm=F*=)2gK̘Y +ci7bpynd dA@ `00aU^!62z)}H:]X7NqUlλcjk)h{ech辅*kTd(jA@ 08 {T:TVuw0QlKJ48'ދ}D9<!cW=vEFbJd/z 09 \E7i*of81CQIxD1,Q,<_/m u,V)JȆݕZkU d6SB@ 07 o*ikP)U1u(1wR+TQԠt뻋<޻ j29N؄[Jd>lB`` 5  صĨ3`:4\ȴ"bY Ӟ"0-d/uҥmguΔB(tndEyA 0#4 HG*P̿b7xxrmCMMm*̺t A"(BT.?8EdK`@ : Hm~˩2 NPrYЪ1E叼XRZa饔ƠmV߲-C8غdQz @0P@8 In͠*Y#V$.9sgnF̛?ԺbR\Vko*U(VIi{dXXA@ `0/ ,`rј MvX*<>YLzJ1ץ/;  ]&up+C dbz@@t: :D\=XYzSsWN}x^w]!F U̐FY׭$FЁte Q;1F!-UPYoLdgtA 4 j6VӔ*{۟8^(rm&^HXRFKA"Դð"j#:* dmv 02`̯ar-Xʱ+84ΕT1XqǸQs醘–ݩKe+ ]S*cRduuA@ "- |AuOހom61WϨp9 Xѵ9}$KS<Ӹ啇Bd|`@ 05 iud5'2haµ2$!/&p4  vZc9frdf@ ;`@M93bRqό]͹(u2"*+bխǘ?C\(tsKըV6']9[h;s=PdqA  .`B2ELv6dgA  #&< (krqt))G yN Jlz C)Ňʹ3*79xdrA@3 h(Uƚ,r ET7_۰}L)!!I;Ifw=p^S _do0-amWIn qѭ =gy+0V̪'L9 (ֵHsoZkklh0,T>d@#A9`Y̎\fmy$'@WHbFbVyၢڌqel) g jdcB(G`@z'8jF@, ZY&&BrVI킡g?]}vwo:-[8VJd A`? @1i2iy/9q]UB Lː8bF@SX2r˫w~xyd C`wlx:iL)_d(fcSIKi&=A׳mR_|(ed@B<@?`@t ;L&jVy#3|2mbb6STÙ vm <e(d6>*Ld B\@Hx('p( i^ZÝNF 95 ^A)xf.pnJk*X|ukh}&dj  #P`H`@?l5=F93/`tIi&4͇aQr__{Pd  #8A`4`,1{o:+ile.Yn B)VdV2R.y˸^X-sgʿQd`A:`krFRN;vuHe8C-6*IlxH<+lqL|貔p9rKd@7#$ E`@;}LPF͇ cLcZFVS:Vޛ :4*&⬏m@d@` C`.]P iUM2sT0q  i00+&ctdC4dYhQ" d@B G`Feڻ̿UU |[,aRmj"l 2<7H[2w2Ċ<uJd@@ #8I @N*Ne<}^qG_Q./My};(Bs.&Rd~@ 8N c]Zx)jm6Dle(At: w)SEt4\95h{uOAQ !dmA #8hH @ :IgނYY;* }ܶlIr.;Ww{F=/ =MCdA  #$ I @eG&/j7 (ʚ_](y9 b(.hRS⡋ k>,FXjqPldA 0TJ @&Tִw&igE2l]Hkuu%ԉ45Լx,Yd` "F @NJ(ʞPmUF@*vJC7wl;x`v'.X"eg81d@"8D @.o&Sdɭ"+]"'סyh ...lP)C>AXSjHtXdA#&B$F`@!aκ^Y%ϵC([uB7kCb]Ej)Zy iA&=*+isOd~ DI @R<6N-/ۀ;,?'jYMʃBBŦ^OJViMdA0\J qnat9B QzU .*4Ir9X(Y_ed`  @E s-^8ysK!qVd:@(BR)`7,s8Caf%&\M ҙd@cB$<`R[O*Y+T9<v)iWj+nyuk1朗k8;,kv}?9_ldA $E r>Ol]ڹ~ *zXA׵.JE񖔧LBjԊm,L> G{zM37d}@ p`M`"o㯘wș6sz}ۈ Kf( SIsݼ`vlYA&P]UE.d`#&@ mF m|MmQ7R;A-EkDEڻ5[tjwShCbI8_BdA B @D`@s+;)3gӭD o\kxFaeE`PjtikFiJr hqqZUdAP`J @\t-{onAŚ[Zzw])SL"0_:y?RƤpo%owxRid (@@ ;o-)dy2U zBfQ ԲH׹'Զ0,fG6R̙TTR7d@ BDH @9 0vPW6ANufm( r-x5ZuY#nʽ̎b@dA@ `8L`@၃dbD-|r$$XJP-Yȓ29>IT7bkȏ)J:ud@@$I jA/._Z0XY\J5*ck qΕk73}MUߺǨQ X duA A@H`@=k!g\/d,fg19ԭ ׿td"B @H @/Mc2G~M Pj]4iu`pAPB]1/75AR%Qހ<xUdpA @p@L` pv8b*ӲyÈ NCOϣRc;ooAԤ}8sg^Xj_dA #`?`@#i2Vt2KڅYas$Aık9ʠ)g/0wG_5ta c6d@#B$`F`@U!.=j]| w8[e)l^lIDpUUrӣ+Uѭ35e)r76<d "E @yOXingB@B@mutagen-1.22/tests/data/CVE-2007-4619-12.flac0000644000175000017500000014333012146763412020064 0ustar lazkalazka00000000000000fLaC"y+ BzbܷH2ĺJl.L8VavL reference libFLAC 1.1.0 20030126album=Quod Libet Test Data artist=piman artist=jzig genre=Silencetracknumber=02/10 date=2004 title=SilenceL1234567890123X123456789012DLXz image/pngA pixel.PNG  IHDRwS pHYs  tIME  6D=2tEXtCommentCreated with The GIMPd%n IDATc?YIENDB`Yk?O?s???????9>?3y?̟>?O????ϟy??ϟ??'<3??O>I??Ny||??C3'?????????????g|????s?|g?yg????|ϟ???'?'????9????sϟ>?'Oy?4uYl??93??>?ϟ?y3???O?y?????9?93???<<3O???<9ϓ???9?~O??g?~?ϞNO'Cɟϓsϟ9?g'??|?ϙ?'?3|?'?????yϟ?g?9?|?gg??>|~sy?|?Cs|??N̟?3g???>sg?L??3O?3y???>3ϟ?'33?~||g???O?|gg~gg?ϟ?g>sϓ9?L><??g??<~?<?'3??g?XYw??gϟ?|?s??y??2gg???????ɟ??s>|?9???3?? ?s|???93???????<O?fss??>?~g??'g???~g???3g?3??ϟ??s Yy???s?9ϟϟ9<?|???9??|>~Ϝ?yg?3???f3>??''???|?'????$??'>'?'9??9?9|y'|??|@O?<|??>3s??&~3???????<~r'??s~???'???s3ϟ??ϙ?9?|'?N<??Y~???O??'<?????O???9?????|''~?y???3>????s?????9??93?ϟ?y3???O?y????????<<3O???<9ϓ???9?~O??Cg?<3'?9gy?s?~'?y??9y>?>O???3?9?y|'s?<9O?Ϟ|9?~?g?3???zY T?y??<>yIg9<3??>|?gg??>|~s?'???f??'O>?>s3<?L??3O???s???ɟ???y<????y?9g??s<=Y ]&gO??|ys??g9?>~gO?~????|gg~gg??ϟ?>sϓ9?L><??g??<~?<?'&Y Z???y??r??gϟ?|?s??y??2g~?9~y?yO?g?'O<s's????O̓?$??9~?>???|||9??=Y O33y?99ϟy???3s<'?????~<'s???????g??ϟ????9?O?ɟ??>?9??'??'?y??y??9??39gsϟ'??|?ssO<3'??3???yϟd~?yϟ??3???~??O@1Y H?'?????>Ny?<N??~?Oys3?3ϟg???>s?>9?ssy|~???9?s'???~O9????'??|ϟ?|?II?D<<O?<|??>3s??&~3???9??<~r'??s~???'???s3ϟ??ϙ?9*YA???|'???3?sO?s???????9>??s????9<'??$?????ϟy??ϟ??'<3@?????~?|9>s~?9ϟϟs??????3?g?'?????9?????g>s|~|???~sO3kYF?g9?<???~g?|y?>3?'g?OO??3??ϓs?>g?y???'??|<3????gyϟ?|'Oɟϓsϟ9?g'??|?ϙ?'?3|?'???ϟsO'gL?Oy????ϟ???>yϟ?g?9?O??|y??<>yIg9<3??>|?gg??>|~s?>s3<9?s~??$3y???>3ϟ?'3Y'y3?~~gO?~????|?3ϓ?9??>L??93????'??s?C~?<?'3??g?ϟN???????f????g?????$s???y?~?93>y?ϟ????$???9?f~?????'~????'|?'3yy>??ϟ??><?3<'y<~>g?>3|>|?????|???|?fg???|?ssO?9???3????s?9ϟϟ9<?|???9??|>~?|ϟϟ~'?3??g?<~O???|?'????$??'???<<O?<|??>332g>????9??<~r'??s~???'???sY?|?II???|'???3?sO?s???????'O??>?s????9<'??$?????ϟy?93ϟ??ϙ?9?|'?N<???????3OI????O???3?g?'|?yO9ϟy~'?O?D|~|???~sO3?33O9>?s?9???r??s~rs?rs??'|?'?????|?Y?9?~||?>s$??????3>I????s~~~~s?|9?~?g?3??????s|???>?'???f??'O>?>s3<9?s~??$3y???>i Y#'~yy?????g??O?9??g?ϙ?y???>y?|?9O|?????????>?'?<>?~?ϟ????'??O@3ϟ?'33?~||g???O?|gsϓ9?L|Y$33??ɟy??????y??r??gϟ?|?s~s?9?>~y&>|?O??~|?y??~?'??????ɟ??~?9~y?yO?g??~???O?NY-?s??9?f~??y332?'????'|?'3yy>?y??3'??ϟ9?????9?ϟ???ϟ?'?ϟ?pY*???ϟ?rg&|??Or>??Oys3?3ϟg???>s?>9?ssyOϟ|?????|ϟ??>O9?s?|??O????9??~O39???gg3Y?9???ϟ????39???9?????<?9???????9>?3y?̟>?Oϟs???'???s3ϟ??ϙ?9?|'?N<???????3Oϟs??????3?g?'|?yO9ϟy~'?????3?>s9O̜??'?3~gO?y9?'?9??3~ϟ???O?s?g?sLy?????<|@y?'??9??y?y9?????39??<g?<3'?9gy?sg'??>gO?3??9?y|'s?<9SY1??~>O?9?~||?>s$??????3>I??C>?9ɟϟ??y?yϟ'??>?~?3????|0|??N̟?3g???>sg?L??3O???s???Y6'??s??|ϟg~gg?ϟ?g3??>OY ?Oy????d3??ɟy??????y??r??gϟ????|3?~s?9?>~y&>|?O??~|?y??~?'???@?9??>L??93?~?9~y?<~g???~???O????|||9???'??'?y??y??9?|>|?????|???|?fg????????9>3'??3???yY"?9???3????sϟ??'|sgg3|ϟϟ~'?3??g?<~O???|?Bg??'g???~g???3g?3??ϟ??s???????????????ϟ?|ɟ??<?y?y#jUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUPTUU*UUUtTUWҪUU]UUURUUU]UU]RꪪuҪWUUUUU]UUUUWUUUUUUwUUtUUjUUUUtUUrҪuUUUUUUURUUUT]UU\uUʾUWʪUUUUrꪪUU]U)URRUUUUU%UTUUUUUWUUUUU*.UUUUVUU]UjUUUU]U*UU*UW*uUU]URUW]UTUUUJUU]jU*U.ꪪUU]UUUUPmutagen-1.22/tests/data/sample.oggtheora0000644000175000017500000004740512146763412020601 0ustar lazkalazka00000000000000OggSK O4 "*theora , @OggSK O4e 2Wtheora#Xiph.Org libTheora I 20040317 3 2 0theora(kIJs1R!1b!@8L&z&\Q@K$9>cp2 *BX#xd`V(F: , `"`&_QTP,F<p2 д- #0xdAP@P<( ( @P@!31pSa5u!bSFtт3tvwT'Fv1!6661!Q&666166662&66666666666666666666666666666666666661AAAA 3Bꉽf"G?r"^}jfWbB`K0ՆU)^}4̮؂ȣU62^?9w1+< sTWcAiAuܐek1x^ yh"cuk3)5 @ x]w(=ιM+F_Ţ9"ͯb%r({IzJy'Jl@CݺcylZu+1eG#FidR,>id<t +-1R܄֏^vj"|v}-VK&b'w@k^KYj#8UIJβbQ#9C6W8bǒ/j>FaП2[ E(,y?mB"\t-2{|H纊V/>mD<#H2nm0@u ]P9;DzJi## W`CӸs548r&yF \{Icp3%ΪS{,50r=2\Ta}{ n5NaVDK7N4 n5&6dEc}BF\g 7b.خ <)#wVQ!f (mK \N1f2Biv}jD8˨*R&spZ~z.+D)p=d:0 ЫZ4]%M܃&C:ŔȹӰY=-|[יŕ4tKсMFpZ-_ [Map#r,Y>e*C , S6M#ϓh8}u*YS6Ocx;D?mp71qZq1͵L_Yb pErr2jrEYb{FV fm,& )EԜ}7 _s yZhmR&H7y߀(nxH&gݱwT)|1$jD-:?0+OݠoRp[D%fڥi6LpÔ#YXޥ(x/`ŐT3`]AoռX*`6fr{D(zG+#wʔ~ϤVVF ə4́Qӥg!t 4Ͷ[슠)wbB& GQ8?,"$%m6jR\e߁&I*/?uܶd<*#$%4{8;ezq=+~`SPwfi3meaZ}bd6ʣMDVXOۋx?OĪ'YXg wqu6ژP4' U"'ְ JB*!9U /\eL屃xSA@jg@n"^ui-36s tqaI~BAd@883ȶu&|kQߥU#x!p: pglLGrK+y*FCt"ܙG~VUT8Ox&i#!eb_z.쎏R_, FS4Yot*V8$/J4Ͷ̱ѭ,k#p!:?JL晶"SQI@z;qϡ]7i3 ͵^ '+R2Z583^]QdQX^Lsbm iJ@F,qdD'6,Zf]k 䐀OggSK O4v`Ėe/^?gJpXTźC#΋tap+ncdS%V D0 zJ=RUU_ ?9y9A`g O70c \ q!]*xF%T1 *BfB}|Tp`M< ?hY[0|+cI-ThbP*}:QyF`0"B`> uˡ߁RWd6filp͘<_08H 8pYj. BO|0:4&/Y<%" RCl+6mSπʌ>a-h03, )/aƎ߃Ck~?D8?د:hѮmT?hٷ<ٷ?ҔH#d%%(뀁MQa$r=Π/x:bSBBCa;J/[hGoblImFl؎#%Ys,)pɊpc* %AV/,pYv%YcbYe1P-]w:-4)nL.KFrfH7丼0ș dɓ'~2dYdɓ&LI ͛&LJ}o4&z"@{Q]4hѪ|iCW*is?w|sE3  Ow `4hѣFFs&8q̺Z0Dt|v?ԏʲ4iBcFC4k-䋒kZH=֦y_юi_ڗ)Uьt(SF hѣF|4k5& %0&TJኪNq*檌'H:B Сnf^熦,^ `, `X x0`XpP x0`X x0`X x0`p `$ H-^% N~gWQ2f>te:w+[/H}qw2]%[=|aNy&NuM۫ۻG6BxT&|,p=ooI[}۫HqD~QQp+eV6665V>~g9ʴyL޴'U%*LLSH-fz_ҕ3INtiZU\i&fdSEJue|OR|*&=Sr{ޙ_%'cgps ~ʩD\giwfg VĽYv&,4[+++%cl c1YYY c>X=텅zd8JQG"p%iLd`Hm%w{( 7SA8zkUt,O8Qz3%T/tݷC $ibN='Ib8rEÉq9<9ks]9Nv 8詯=8p8C9?Ç9Çf?Ç&h抅-/ZҋGM3BDqj}ga rǝGJ̏ HW\b^`{UG//tLUtW+qf]|_WbIGV$bt41"NIRtҫN1F B?r>gե&%&#QйR8sApßON8pӧNN|pӧN8pӧN8pӧRQ*h[].7écuRSYaSO1SS0`B)s D_^@)[JM8]Sn v2L __S(|y]CAd.WBR›%O)ijUkuZgZp"bQյLS)L1;szάst'&R|hv-%2!\Se0ESHj|oY7-sBmmmmV)YH X+*u9&yE~5 NUK\󽰉bUE2L*L) ʟ\e+IN;6#ΝjLildlCZr)mj`X^w!Nqkys;y _yX)ۜqK/ 9ijf 1N>NgNmSy/rY>|aqYq h`K8>|*]ݗIMOMϟ>X|gFCTƫH"|d}|l$jkW;5 ϟ>|ϹV*vCq{'W\w+ v o!^n n37XIcץ1ΡcMǩDž̋xEǾȇsGRpt~'~Kv»QG+N4q-/8 JcâE{r7[؍Q޽GPLQI7Ŧ8ij C8[y}.^.Az IK5-NږnrIdzĊA%-NC++11ڜ,pI,|XUW7o> 1X <TQSe(R@"U@(1|E:|/Wx|T|qNj|?'V:Q˅Q/E}|e#.f.gDKkDk+h뫄orRzJC9uŞj&@ o\Ns'9s#s1ps\#y;1` 1̏'x4)b{Or7h';0SΗfd)lzE̗nGp<_~=z޽zeg~zׯ^w^,}{9Wn04FR "}ǠF~&>v_pDG2܌)9-VFb.݌i`:A3<,rd.6gXr: 8zV<ݿC8Alp 8=Apup&Ѭ=zxǡmDSX%QSB)g[;OggSK O4+ pZf{]ߏ/\K"x:"Kz<w7sTCK+I3l75cw9MjkX۴;g9DX=%"eb;=3ޑ&7FYYYYaYYYX[Y[[vY[YXYYYXYYYX[YYYյ-}wtȖyCh)olbj)dg""" \wN6R Csw̾kvF >#yDADA"" "R.xSc`e##XأRhhJ\Fn.$SJ-ohn77l{ҥwɖb%y[h|ĮGqbgrlC{rlewk.`[o!b .Iyi$qm6ck!r'sZc/3b 5\bh~*P5/r]n,j_U9ޥ7m_ֆg:s{{]??_uR-KU)XT@b'“ikݦq1g\ 6fBxzz-{ڮvӢYBx;ۧsd7K<)J~}O)JR(Ǐs~x?QݲWOZ bj-2p8|DU8i+wŋ哗;=O;X$ںuto#y et ^82$% .t$Xmo H$KRրI9`!ew< o7An[5-{@BF\)ƫ̋em|i%E?[;\B)RRbċ-|%ۅ֔_nm~#~;QϾ>5>ï|2/4|o8 TR3u7ٹ F9>ȜO.*S&QIy(ƔcY@bl.&יPE!V(݇wΚ^OREĨCwwwwwspq!>7/G*yYcEĽ,umG4~UxeW+>.R~:ǜv?X* ꁦ:Bʼnc~|,8/3-3ܣy7|y>c<_ QVʅhK+-ٻlȶܟa.lڶ!QnT{8aE`E ePP`8AX;XX1yHU@\ ųijZ(?^\M0c6[n-'dZ}ݘq6K˼P"yf!Lq8bUO%L$FѸd @ZdYO[6z83 t<"{W{WFl<0*3,SIe9XCQe<xiZbZ^@pk@Jm^Ŏ="d"؁*v)@ؔ ,tCayCbdP RQEB(j6a ծJ)J4[AB1 Pl1k @par!Q:*B6?)¢nH*XrYRZqc @B 9aSBpɍ(ƚ7! !U TaT]@2-@4p PZ|¶%0*B6Ƙb$1Xb Pė0 P`l-B-BxHT0+/9aQ>kh օf¤*>:Q/LGaVhZZUе PqgeXF+޻o ^!POh=Id `XyPl=wBOggSK O4TKl-C+BB NXZxZ[ǡNA6x#>aiT ( *p (d X(WH6FA ޾zY4B-3WajjZ [VZl/”*`\l#Xl&[Ш {X,¦@0 x&XH= 涞JiBUUe˷}UT%UT$$$%UT$$%Nŕ]tHB+B#Gk<c Be&̄FN'[5f~~~|)j{(Bbqv뷄Db.K,o=čvJ_unhU7m7L;cqDJwX>%K$$&OhR &$XL$=$%TT=J$Ҁ/B\B ,bI&I2IݒKh=I%^@X\*)Ē_ (eLꖅ)O޹ÔBo$^$Q T5/zZ4SiA8YngwZ:Z6,lrncv]ЏR $9$Ē8g1CȒAҖIddRR0I(k q$?$*$U*JxĞ$TJxĞ$U*j^OzRU*kORT'IOX >Iv<>Iu A%I2I)'G&)H V+VY+b͚XVYY +;;5eYgggfV+vvvjbYggg&"q-`Zx.5ׯ^}GEIMl:]{ͦz5B0@W3'XNQxd{rjoi\kLO<ԍOF4M 1yWtG̤5P։&@گ|YV!=4f͛6Glٳfiaٳf͛6lٳf͛g)"&naF?<\ٳgf͏ٳf|l0_;f6lٳfB6 S$"j&{KiUK7t3&Z˗r|rϟ<,ʗ.|˗.|˗.|AS 910~'s9,X>.ۓwǮ>>|̳ܽ78hcafOD_ϥ7@6~>O\0X @A'zH8!ChL b1A`2-f32sdØe1TGUQX%óDK#8p#HA#$ 8U '԰Ά8o L=KPs6T7D8H pF J0>Gϴ"ThE{.../Q{\\\\\\\\\Y:W9ɓNy} P)<ā<|p9#҇0! 2H:%<ϯ$oS;D4P|o#嚒;"q(@hiC)$25jF9A$A!͡ ڣp#_ ~F|5pcGjt(kϋG ވ[Š'2b3 70Nz'd#:;Od< Q@oÀ(,s9 NLϩISll{T1|&5iJTjVZV-U@鑭;G*I$ 3&CyJIP֫F NPC4VI$Y:q%Z! *vВDPH\PD/IR")CPdm~5Z"HJr q\h3AH%J>S!\ Tv LgAQFkU cVa/ݲ 'U')v`{$tIR)HwBv{(GO2# p䯄Gr{Tr ;րĩ@x!Wtg 55<kC.WCBk#CtGԑ~Tמx] 3BBjZAvAMqHƼHT<@!pB(: H!A~UJ)T5RPJNʵS%^W!)Ȏ43 ˰]#iY2C`E9L)j`舞V'+݈C+#SSvRF5e'~(тDS$aM H&Q$I$kJs)T*T%D\E RtH#$5jA0U5iMn ㎩J< ҿ*+QK/BP0A#^a._:l8_4 2UEp=BxέS7.g+Rnw 0|Q~!N5 ! pu;4 TZpBB{w|U0VM) ieùkA.9(3qƧOPh(*qZy+pvp_Q8\rU7zhWg?auͫ+N)1i*?nlҁ _*-8 k°<g9y{~IxX(r7Ppxc.c|Ҕkh?#?#@sj(Pi?]p6M骮HE(͜g&[Az]"9n9 "ȮWķKuX'#^y ]P_L'ӿ |OggSK O4:VTWP5>}O|$J** :?93k]~ȱV"]  P?w+Eh6Y!苰 >܆,5(hp^'PJѬٖ*H9y)wƫP?ô`YMװP?W|,~<%:DpjSjA#G9)$ ٸI HBO%֪T{u|V8|P?OWo2W?)U5) =ȃW5I 9vVIN,zO%VMaE*C2W 1OggS K O4C3jYjoFBcP?Wmdڂ]8Gm>sЈ_3Wy <Ja41Qܜ a_n P?OW mbi='aju5=*$aG՜WuG圐O/`_=KU;a+~6茗P?OW mP!h-)UTh W"1]:|X\>Ġ;g/~7Sr8;Y;3?yqO9ޡvZ5ӎݫOOP?OWm[k4 ^ Y]KK*>]AB9®ŝ۟fNG#*'"0vP?OW[Va= lW!>~2HA\++/}s5?_L4ȾP?ςWmtܼV'bb3r1$7y-1,oz֮â@OggSK O4"tmmJ4?]P?ςWm3x"H3|Ӛ[k:,^8=?|wcT}#^D[m^Vvf}(_XyhX`P?ςWmgg\ڰY]CLVV5f \ y0g6~% TYb̍0P?ςWm2Ɂd޲[Ư42\^IU/[hP??ɴ!WP?ςW jLіR)2x$Ԏ\œv{,g)cX֙G P?ςWoS##-R Ճ7E"0ެ.D`ZlT/gmn~.#8erTM=Y,OggSK O4;JbhVOcP?wMhPՅG—'NLcu2 ~KZ~ G}`μN5ؑxKZצT@Yuk9P?> _3mL1X\qc"唧 D*򸿏 @tO! WF; 2k0EBߕ=s'tP?#M P?>WSEC 缢.U0t9#ʕ`T@=_ >X x;BbSP?>WMhO1/ptfꏳ ӔDW魗|?ȑeI_MxQ{&adP?>~%Ȃ-wWdWhLPz]pnx>J{" /z*1 w)>AG,{ YɀOggSK O4 ?pzbIP??ɔ`<P?g~9H9nI”I) 8̀Y))~v.؃0V#:ovhfcݶg`E/ l0w,\_0s% B >`1q0dZP?ςGnIȔI&an3x;{_~s Y'=5JdQl)?^,1 /O2Yq+P?ςGnKa 1{R3g󷟀 dBL,Ǜ|5FY?:>{{ z;G]OWhj58Kŏ]\NްPP?7W%J$9p_p-pE~NF z]ӖiQ|%Z #`a4 )I~}SklP?ςGeJ$KDs/;Z0 HDÿtP+gA޸^ Ƀ7&SŦ[dzOggS%K O4 ӍWhvgoP?ςGeJ$KEʃQ׃EAʓc}/ ',= H-Eh ,6mh ; i7@P?ςGܖ &E*IͫR (%`vKL5fa~}՞NoYbԞTM0', {boDGnoLf+?P?ςGYG-7:˓{;y/3Vθ> J٠Aıhs#Y1Pΰ_f%:?'|}m>.CKP?ςG[YvDȀm;eȂ [cBGϯ1z ? $Xо|t_yRo|f$dfP?ςG ZmCV3rg.Kb `\ \m {7Z&w}%>%![;6 $'"D\LQ P=g}-y Nh*ROFDH"dUZqZ "눊8P!<=߀^%=?>'eJUUUUTcIݙzbY8StE(*׸3?f:C  x*܄E" qpxl  r^IHPE9-X4`z$Rz"O*X /+ e򠀕 _30)n'fۑ fL |,w\L{o)=Y poZiF >?>"'m?$FKO۪1Iby(&+bw4xdYc b|Phџ>d,>Ӿa7{ F7o9KɝxtS1;3eeș0OggS7K O4 ɦw_(#CP!{vlk|~ }NR O|G 3_WUWS\3cKרxFuR(򎹍&xqMtP2|?w@1Rk84|GS9W׏R{nByy=A`b1U`$6ݯTP2?Gp 8`; /ŬZņVR̻;P0~Cym&aldP6~*!K]J" F:G_er qXP8~^h|z-cQX&o[P|:b-ޱ"N!H$ Yˈmutagen-1.22/tests/data/vbri.mp30000644000175000017500000002000012146763412016760 0ustar lazkalazka00000000000000ID3eTRCK01TENC@WXXXTCOPTOPETCOMCOMM,eRipped by THSLIVETCON(3)DanceTYER 2007TALB=I Can Walk On Water I Can FlyTPE1BasshunterTIT2=I Can Walk On Water I Can FlyVBRI dbۑ!:@6λ7Ϻg7׻Ϲ,58cZO4ķccmY*0Eλ6p7 Ƙˆ8`h0h088Ƙ8ȠhhȠع̻p˵}X,˻fh*e\ phƘƘhhƘƘȠ8ȠȠhhp(7DK p. %I B2|PmHPЮ  קNn``07(}xD!K p. %?ݹVjƀ@1V-܀ 3o,ɿ@(kv<] /DBK p. %?Nnc% Wy|JB [cP@@@d>cK i c$ '\=PN8] E#Lu uT;`;W@RincKDdnt~ƨ`=l:dpft7?AۑէV)GH4 -Ơ,扖"&&& &&GdʣoHA`),L/ Cp)FF#!`ƢPa <

mܤ Au{ T " [Qb縓n'1dZAvƨ(<d)A%apr2c`kQ#@D"M$BBIłHCAa)h])dN__.a7?yqcKd SȚdCM dM?KYcl mg o1J̰~zeG L Jx@iɷi@3G ^BCL4L(b QVh2Q[:}8%`TȺ4q,r&@'vekF2ɅP߁#q3R-vB^Q2v,'; [O !iUxܪ#ڧm.BNV@|=`IФ@~&{;3/) 钕mH9JaNQ#HZ:@@ V9.QVNOilMBCS%A8N"q9/ JDxד8W$DnFuͶYb317]Csmm'̙5"9 o2 $i BXi Zo 8ɠU@JQ jxEa?41 TJ`\[a aV6,(1[^ ZŪV.MYe67圚xLgwA¤3Dfgw28C8=9%P5ݷڇ95V~RTI 1(|U'Wl)Y8 }Wf5sJ< ^V_ d1(S3QG[W„I M ZEZTMrůVkeXVae쉲cEoy C*nQ :Z]C_8 2)HN5$xiP26 iBzQF*4{_.Պ‰EšJS}pW^uE>fSj+E讠mt u `OEҖQ+҄W3 wmj7m  ZTC-8do JSd^<կ'Gĵ2IY.6MnH$AפQ4(H^7]dnf\ OVmc i3ו6ѩ(t/\ljgYj*fRյ5 k/11 ߯ qU3 ķzFŀq@eȭ.tfOrdLrwp,2g%_%v-5aT`lĪK@ηD +5#bY,fE7Zxq^ů֝=Yֵ=5^1-SFؽOLb )'CFdB)L`PL& j.z/LBSJ95H.Iı3 [ZS7LKq[LN1(SX 6(Q33zR G*@cl~eA [X`Iy"M؍kHRSN{kgV.qpMGlrM(QEIe۾Ԅn{SnM Z^͛lasv1?s -1K)†;$KDJ ۷mﮆ =0V<0# !FscyԞ  P6iFb|Qv8 P&\~(=P:,ZF4:Xa-^Cڎ1~*2qYubP np=7ff- ! l!3z X8 {ae˱B`X1RQTgj[@bNG$j+2j &iwAޣK/]Y,2ܷouRENņln 2x$S&Žƒ.rxqobV1/#UJ@szpQ]I됣$aJhx)EHTawYeZ*3WKJFߎ1 Y~'}YEMވ=zDdACLUS/LZʽ=)!U=3j9j[q]0gAG@eg `!44e#ud+'ӧE0WԷC.Iޚ(ˣ l laP4ijBˇr•#RWQWdȕdH$iߐ @Պ%Bt3 $%z|<Ǩ2ɨ4\"DIMM-ąCǔFT--h/RHZҸCQ&%;ŻH"x^U֠q2C̖g(Oq"\IKĜ3j 49*\]5LDp۹#MYS^҇dubh g#x;*H# !F]" OLѷI֗||+k:%&<!!P\#}0h%@pfRa298$$\md& !0 3Cv &Paesِ32O29 3`393 3932C(0 (sِ!L2C`LLL_z83 1W{ 0sfG ˆs'APA!2030sfC(0&dR3?*V'3.0`LL0`LL"!F@PFj 3a&g&eLL00L0m0@V M 3 $arfd@ ?LL03sٜmɑhE$ j$<&rfe$<2sL"2e3?G- B*Vl8<HxfrFF29PFC 393`<[T0_#P  O"@&Ϝ?ơ$#E@593` y'&HxȄarf@8g@ ZACˆs~03`.{nYoMjbO1Ih`1HxHIyGʔSo ) *P[$Rf0ayQA֖I 5C03'09y2sddp eEϜ3 3j@`LLly8y+ 2,19&kLq@*sIms>A0wX՟)y&9<e31VEBʂǾ0fRTQE|HF$p W6!DTE`6syjDL2GWA`p3g 3 3k38PFa,2rxC`91G@*:L9PV9aʨogY&y `99@ -'93(#gϸVUa''(#0a愙9}Th @9<0 3ԍ99 Gsp>!-a@mYTa1hc@fM*"`9H9zpH1V*"0 V{sO*BcA3g#3j 0s& 3ϸH|8 jTjPFg8g&f|au#HF|1`朧@F@2b 3gN0)?xUi10 jLxʮ8͢ 2B͙L9̉(#fy&@9YF7=h,H9<33kGc!9)sNPU#Z/wi@d+VBD GcPa@0V"3u@9<!V9a90ảap9Yi9$/w` 910F¤vvT!PD̜<@F:Xe *زKPĪ *F?gOZ=0R9fq]bBQ#9j޲ !b0mLErV2"Ӟ5cPa@BUqs TPul€` %WIf> &Z9$$#O VcPSvcaĪ)aŪ 9c@3Ta@ kTPFGle5HAԤɁ"T{$X 3, @c€sx0XPX!Tg*xS3$0`3\X5B:P*X砊 |  _Tg$|?V`;Ö:U#cmIP'Ϝ8jy@"\<35|jLD 膧p:Aj:j@OcU|Xkݡ[vV Lnha1Ta@1j c,g>F,EDYT@@UJM380S?Q0bPe\!0*Y]uz:kWX51t !I |j"VP9@@|jq,ޭ5z˂00b8Y\8$H,du1!2Ta@RiF nIa.0 ԈFQN91joX)ӌ}*@8ͨªBTxge!e#Vh\X+}v1"0.@ƢbU:Jƈ߆~ْ,V!eXC%Ee왾# U81.=eF)[0bmg k}<֊*`vb@eBT1F!rh=fע,HBjCFa@c up:F L+1@s!pp:F@1ƥǸjDcU.F`q:Uq@08jfb@@#V2I7Us9 HVy@̧FPI~VLx3.v*E@#VO9P*FŚ~e*De3?*v-!n3ŀB>T_݁W|U)SUdE@F`RMKc*XuUA2ƨ . hqa( :B@c4PW}0Zq@e:x4U¡8b1i0,pbExY G]1q3wiBWLt1+-U:VQjBjaq@H8!uXx@c|spr1jDS.<\ZA@2QXc]z !d#sq.񔍺!X5B`CF u*~jE VR8" u#X1b1ZXcTXu#V0b)FĪ#0ƫ>UGd.T/ĊUhĪ#@2Q`1 `QGisAxueP+"vz0b1 #FD:B!XuW}ejĢbغB4"ZCVbĪ#cXWP+4Zq@Eօj`6u<$ª#c(ZǏx@`q1*jq fU 놮8bF}YUZpuQ8 h *Vz"b`ݿ^ǃDH(вA0Z11ՊƷ ?LЪ1` 1PQGh٨/)F\uDcvWQ8[w>@F)T#P->HVX1ԊVQ1w?EP,UG#C8PD#uSp@018̙[!C-:úl}AěfRa8 P[X8V ,C[Q8 c KeEUGq$cq@@G#z Bj]UǨ9]jXZq@1 $bQu@Z uiv jq1 @OٳkǵR85pucQWjPxG\#pB(.A٬Q/:@@(:ЈƤP+cq-;_Hň=h Z eYX@(V!Fak;ή IrX+j1 dG]PQnp@0TUG4ԭ~UG 0U#d !ưVa8 R7 1BPţcUG(v׊c9  ,8"ԭO0Ckq@W! 5j#pcTT/?> @P֏ǶQI)aX+p#c+~UGPW!Pţd8@2.U±:<&Y,@ l"H W!18Rt^Z)U^g򺆩‚P:.]WLkEP uG uO^:BDUG kŁa2 Z6֗~DafUa`+N!k  *R0 cX+ uſǰI P \u$82g]-z6~{wĪGȲA@88 v=õƈ=/cmg*e/A+f&$$W!ZdAí  :a8 Haq@p6T;a8 ūZ Bq#8p@E]<Ł]z<$a-z1>^.J 0!:BdG]l 11x@eX+>(1U ŁW=B (<ǵŁQtV=>BCFs&Aka V@0eRHXa E=WaZ@ (  @ AErQi@KNtDaZaZ =ӿ.I` [e ChiaiIi¥LA0v2à0HdV:Bp͎kK`xK ail1P%='U@EUd<>r2r06,opƕؘ֖p=ņ 2& 2ò Qh%ijg}g p-i8UA8Z,ed!%$$ _:WRþ3(P% 8Z\.qZ@P'MKaZJ`iZ@ƴ,iB++.d`L˒Ro`ư,i` .7,[dH-L @09 @Ppѐ.e „KXiNVf ,aL1ZZ0ۮXW */cLж.6( -&r$$ $ ( MI-L @Pp)SJCPvG]qK@$ ZBI@̘6< 0-npeP .eRN'XYka 2%Mi r (eIcZ! P`1lK vb  n1,ჼD,VR ukhp[9LTa0`i)QM$@B3(4Ű-iX ے&&\dwж0 4%l"Ű-i -Lp*7,[a8l- Jp+ Es[APex]PpXha`ia '(Y7' Km Ű-aA:L4lY+ % &I .6@(iZ4 wm)HTs0@T M S{ne"8,rR[9 l-Ll0&P-/vDL @F\l"bږ4Ŵ-ѹA6@T e [9i[B ʁ@I!p&bؖt8,@([wN-F0Le5\h`Zh[@t _^Ʃ&K"XL"&DbɾX-K@ö`HiۆN %  q؆  l(g߶`[4C i,i0r @Ib 0ʂԺpY@&H .6ٲAi+@@ .6Bce; Lp Cm0$P,K@ pM pM @NK:(%djQEs-i@(ရb,CUs-iBr8$DBUlX~{Zx@CyieI#L N M"2p[2@([98lá`XH ŰmY 6 öB0IAscӥ,muϠQٲLP(aۆ99† |ML$8X4Mp`ۛ2:% 2! % @Fd"@(/2b[H`1l0%"2l{`8X4t-- eIDPŲ0(NeHa/ Mr)d &D@FDmIazyɎeIIn2MC{+)0+G|Lla 8/0:&u`i)6M$XLmJa/L &x%+Ҷņ4ؖa֒[#Jc P@(7ۗ'ىtWXTm0؋L$*Vm&)9Leܰc-!*m IWH0(NFA܏Mvq0ۙ!ܦC0!) E$"xaf n4@@q,%" d͍]mI8 " Ul/e"vb&"8Z8 " E2CF ӢJNHF$]4 &xqQd8X%/A@"!,qodI`/2L@qU9,]/-f_`mbld/2xBIx}$t0E& K4b- @(7hya0(NzI0VH@qrY`1]1ݖaAxކ#L,e [n6d[Np*0^d"!I`H,̮Zrk$[D,6Mb, `a6@@ͽ fcn( E& (Aod[B7vx[B0Ds a%k68ФA".0( nixD 6:2 `Z8mӄblJ:ȋLQ2 $2Pl0Y5Ǝ$%jv6"!I^d"@(ZY` C($/2X8ȋL$e0nξ VXT{)\ai ذmtJPA -L"Eia$/22 7l[Za/d[Aa-1-@(餗v:U Z1/9Y8ȋL$ gܗ LI%&`[ i/ vXQa  Y8ɋL$b-Cx, Pvކp٦A؇( IkYfWh@V d%T]g"; Ha"^`4dvvA$l$re æ)0MX m:48ɋLFy,ձryDq- @FrM$E&{`1\i@(78}DqrKAҭ E Y8ȋL$ jt& ,6Mbe\4jM Iov]LRWYD0D5N4(CD$r. ,A 2 bd /0 MODd /0X caH"Io8Y %m^iӲd/b" y'2 ©\ 7Hݒl  a5Ht:6l}$h d /2e&blA%v%p7:W]lJ8ؖvc"([8#Y8ȋ:m&)tH2 A^`"%&ZtT)p e tľ\`J"m77\.าPZd J: &*$/0 o ::D6݀3Ă #-"uLnE`;~ @mpڦI,6:/Uf)-}FM򎛖] mndIvti*/0 t^a% n0 ,2 bM tW$-2HKr$Odd*L%;&ݱr&%+)} IL×)D *w@qRdSEnKvLn0P;~>@AѭMU\a e '*M x H$Y0dd`aTQ&Io8.2Lb2Lb(NGFcr[2LPaS y/  ]0]\".7D-` bxLK{Jo6Pҩcp[I ,6Lbd`/0!^ʳBPZdaoidaFeZAI'"? nvIѢ*%@U0,&2]6|}Y@Xna{9| dQE9@VvA@@T0r h8!V2|/`" { @qpYI d@(Tr& { ^`"%7 #.0 %=`"m& '{*.0 H]K2c/)hoؘdV$3f6IdRB `[b "sS{[`1ܶaS\a0þa(pe88Ld2Lɾ2 p^nɤA*FCѠn$_(Efb8T$bibFϵi [@qrYarYarYI , &8,1e& '{ pd.p4W^)c_QR8GbX mJz(_(^P(`"+ E2BIF)MD@N &2* p7, hi 4zh{GU$BÙFXn !d_XE&AFE0,$e)Ӄ}#G6a T23f :nA Д2bm : ddt7G"9 .6I$ (‹MdvT| i-'H(Ⱥde&1pۆILq>7C3p^[$ al LI @qrY Z80L$N ^@/}E G$LqLD`스(Cy1D 1:/و"A,U4vMl!m@@B v022a 21222@P ibӊ;$zE%XKF@ƙW(1EeR`Q 4goi6 !% 6Q$ @@Aإ Jqje"Meb@/4PlA&q,q+Pl,`T| v<2d2b3b( FŮ4opqİH(S d3I!@-h~83I0a3 2ClIfNmpmZ4Yѓ;}  2ld& L`rfe 沙 Qm8pF9Yo3Hǜ@f̜D(0\Ɯk'5@HAjeo<&Pa&@m Lj6e _$S8sĄ0a0 @Ն\ouc& \6 R$M & L83$<93WdA ee!lL2a&0ɄI@0j30Ljxe L09l9g&L$mL5HjAx R3 !|BxY .{8<*9(Hf39Ph 3\7 I(#!vL0m < <ժZ3?s&(#am'm A2j 9r5&y`LLY[2,<9P٪c\C*C[z" ΐXɁ0 I.)(;dHx&LʸZM6"frX[պ#(`LL03Cə frf393030sݞ !Lə P6̫}<0g 9h@$#& v<bDfI?9I0sA0aL"0\ۓI DM&*L>GlTa@d9穠lɧֈŠD"3(#_dDf}Id<Μ0sγPF|5B`\ӬT6 -UDZ,BCT#":@ am$͉!0aE|g%̉ 3'393&1fyXR&*y'A#ԨAq@@2rxO<$0F .l٨_ !*eRa@$#|*RWxT!̙%QnMBH^`g"ed;z0 P sΣ >bƣQ̜0sΑr F5 zXy?8 4)bD Vb$#s+u 10 Jpuu:T4b1c[vCq]1jЈU#ˆU#(b]Pc\):["`@E@jFUX@2 u1b8Dz*XuU:jTpqpcdPCd b)FЮB2ղ3ˆU#0agX ˆUj!lG\5B.)"{<HiF 0hW!H8 c5j *jj4p@jVQP Qq`Pc!@2Qj `#,kN80@]a @cVDDCaЮ f:ƁQuu(hbxD1Eq#VpbP Q+V!@A-qpAj:8XZq@@T!h;C) \q@XZ8 T:SBHC#b10c \AW}lgc-1.RWXh`U p@,!Tƫcm3C`D]c@Y0zq` jLQ1bXZq@Hg**:Ʊ>51b3Oc #V!d  uQ+vvKqT%m]J *n-:ے(XBj Y#~]2E01t C$c: 1uDž1 㨋6>4N3^C¡e Sj%0  \z@P RFڍi6֊#F,ňXf9lN4bR?4bHU ƀ`jPPc!(:` .t@@T 4 k*TkDDZq@HwUPl7oC54ZqbWcc\6H@z뱮#d^{G]eGe5tx`Ɔ(㈇4h Mjcu5ja,CxfX+z PjňUG4zBqh Zq@BB0Wu!$cq*Vz` PS~`tG Z1e9ԊP ,VqhRxʎv8,F:HXZqC:B u.ԈW! LuY 1 @TVjxzmZqaB c>KA0zmB\vʮ "V4@0;tphᨷJ!0hU cW! dgۻG, P+U:Bz#Eg+*FV(k;kjѳxغ!RV&H"TˆƈjG]nQW11kP+u=czgV4S8 *Xxcu W:1v1.!gtwb@0W `Ċ:B8"jU?#PJ 8h+:BH> QqUG@(1٨ Ԩ:Bau [p l *2Gx=V  uvX8 Q%q<͎G%'A)@*AX8x]_G;&d2E lJ"P€ $*a8bŁW=B!d1ٮjUh k1 cXٱ^PB5BŁW=B@^`,^>N1 KJX"C[vʂƠq~ɉ &r@T8QDZYC Q/u,, j -pUtI',[n0I% ZcV uB5,ú2+2+!P.|sC5Tc8n:6HX Unh ҁZ TW$mHKNd`1,aò  t UcZD@hqC[Ae4S  $bhK p(S2+`Qb1,K8ZW<䑖 :fMR6[2Qĵ MQoŕ@0UKjc[& n.,Kx@Zk=ȅW-+EH%C2i $Aǰ]Tn@0( [HcXp]6 ` m%=VCkaY e0LAa(XUCNk\le Łz1-{ԷchP8* Aq$p[m ix E֊ x)T k1e-"8qR>KMBDh)i ˅`CZ.4) Jh!HR` m ֊PLw$ ڄ"le -a -|[hIX D^hA)0%Y,Q4|,6( BR e2,6 i0(A[hPB@(aVPeX=cB@ 6HX X1WhΖev:&H, (Q^5u}LI^vjih!@TxiW=BzPx#BqU>zƈoݣ^񠢇,z켓uF8Ί)@֦A ^6Űqj]qð.zMӲȉ*0 0XLMpYJ $qGl4RA/;HX\C{ (iX)ch[z HC 2( ae0e d,lm6IL $kT0i*J 6$r0cZ0lBW9x׿  ]v)06MKH&KpH 6 @BB HUXC,Yk4€L ^"H,6$Xl Dƴ 2^A}VVլ$ rv8tZ*/@HP^$0eIT 2$MRi ( )%+>v0@ư,.61A!].[ *S$q9,K XTpQp!t0RF-- .e \*S` וWaY9(4n]`HphW*/´@1,K@`  Aј]cX0A@PpiTJJ *j(c}|*O`Z@ `@aX0$l ò ò "ư,i` ˒& ( ) 2Z6=L¤4U[y %Ե ǥ&TT]8} %l XiJ 0q R'Xa0p &@B\l h./aR \l QUl TaT%v,lK@0eI*6.A6; Bm0l0dsr9 .FoS{q B 6Hl6 pJS@d [cXZ8 \l  ( vFsr#h41[[Ҥ40(MIkaB "! /c[ \[pJ*a.WXL @ KK [RL9 V m"%)/x0i- 8A6bd}З $ s PQ?ƉuY<遊妵- 0-K@+=^4P)He Ӳ  T= e@aZ0 tt@LZ{yRk&@h (L)0eIȘ4Aư,icڭrVRiCt]QW=BH;li2H1+K@҆AҰbX4A2-}*e jLS[,idPR8) jSò 4tBLu9L&J[ K 0R` cX4A% 2"K`]zٻp-i++8^#xM$@.60! hHyCY fh]4A_2/Ia^2"e xc|L)(^(SYP@BFlBIkԨRC_e bxKy:.6(NPF}[@h^ZXZ0,eIB .6 ˒2l6,aB .e KOtJZC X }Lmheul @f&h͆P(. 'Us0@koa鶡1,K@0R$0a[m6 (L %MPPpѐDĶ7; &1,Kò @b > DiѶ ƎU Nat+Od3ZcsUaKaZ@@aZ )bH@1Z蘶0)kR)@qe 7K fKcsڲ'DY4-ј,iZm/`ka2H`&Г27- 1[[An6[5G[ ʆK9JVۮ˫]$%p-/vL@P‹>F-- 1lK Ll{+ :oiebH±#N@7yM$0lK@شB?M$$0͞hSʣj_,1T r.e"bcvLaM斿52%@B,,"PI!Mf I5%ЋL$@P8r9 In2( vKνׂPr,K&Ki& %Lxс,mJA d[ ``ۗhbn2(N%mIm@T豂%JBS5u4aS{HxK˒{`J",f{[A X ض@ICx" 7q,)êe1I/rd &D@(z A~QOmgX,KDŰm$P/2$/Cf@F$#PTKDb48M$@F#rp˒Qa öM&DŰmaJ8C& (\.pr>ȋLHeI\'yhRm䲤eIm @IKxBF#D@q,iB DKyLbض4UEm ö  pЋC{CDrI^t ư\"wiŋDlͬrE&2/CVX4`Y@&ɈeI"p&`1l0Aja+PA/:!pЋL$UKAh N6- PhL0PlKD ߮.K&&D@N Xl_$W`⠪Z\]a⠪V-&)p(:E&x_ ]Tym!`Yd= 2M&2& dF2As7\M"dU//ТU/id[J0Л$FAAhRDZ &xM˖p˒Pvs"I"xс&ꕷ4p؆ -" .v~IOh82:M&-;ے2a/d9,i#v!r56]]mIi0Mvgے2a/ Iے2̶% 8X.H 8% "mIrыA@I7J8M @(79 P@z*KR r:؋L$+VJ8bo pқ$nB 'Ɂji,2|Jom(nNbO& (\nd !D pJ[Vcö ё" @(iE,qpkOk&KJa/dcrY1K;% L$%&PB I_$ey.iYmwa P1[ \A2 "j$ iF\i p&xyQ T.|!`ɶ 0a7 MˁK A&e(e{L@q-ـ](2`anrIzY` A$+q54 L4HPIo0AarpنA`"jit:6w۞/@B"(tH,˜xLX.4IX-O;H]E"p6F\Y \AhH&yDq-Cٶ fےe`1r4U-i%ze`o' 8YTY$B JJm; "mmd/6H%[3 7ȂXafSr\in4`1ݶ o/Cj0}ED e.E bmAm; "XNcso˧HB-<)D`cm JzX^NE&poBI2H,8%i}sae`bJo4pjvmJ/2Y% 8, m; 6cP$vl&V !MTE(Nt۾@ Y8L$k 7cտV%$PݾYLY0eEE"E:s%"QaStOlI(N.-&P҃D2 "B7vzs@Y8e$T҃QF`t.(#Qa (.4Lr &n- &2:ٛ:/4(jA,i jm([D 0IPFkui"ɢ KM PC@(| tۦA t]`J"ڿ-2 Ndw(3U¢ep7@@qN $–h P@&j<^. Q0I%=/6Jv"]qܒa 5I̲KyѪ8](U@ 2P,,$ONbV~PFŦ(j ;TKЋb0@j$$rmA^x:7]^N HN 2 brY2 "@/PTvh),MH lLvUdQѠLpP]D"P1- .&ϩbLW$d;H,L/ r[V-Ku3Ր,rU7D`qM\yYFe9:7u#(w.;hTTP"do`"C ,wk֞}LL(4oA&4nnj&@ɦ U&* Tޤs5%!P e\'&r~3Nf" ]J;|(_`"X_H@ɭm+K a5v{h@br[A@䶌זi@ ~(Ŧ6 "*"C2x~uo- br[ATLn4Mm1O*s!Qn`0nI@І*C0- UmE$TҩZ:7B 1 ^2:&/4^pjo0p*o0rd2Cx Yii*T`t/lNDdo $ZzjoLd ԾDPCy ap[fW{JfCSزAivvVR[ I*.C"; 2 bTҡt{ hAcD eB-d$QrI6p`S$m0rP`bvT`9l vIa ×L@TCygbL,  sG7w(* ;Av0If22"860HB:طY@Ex(o0mbp[AJxn"1μtݕD T ^mi*᡼0- X-݈FWWXNnK @KDTCyBKO Ld@-=7E[:Tl1yH T n4lb'D @"1Ld^0um)0$//0&jboB< NP*` XN/4‹Ld2&2b\ut*o0ܖi*oAb bho T2 be;LbeHO M%T\0- bD"#"x(@F@{6HTRvE(0PXn$!Fukae 1@EP 5A@"sͿP<o! 6U92eݖi2:-D'P-Mb{iM$HyҲB  &2оM 0m9:7@qp[I ˅o'"0UL3;7,L @r Meb*xEHR,6 14,"_.CytpX= 9+ e1{YI,-<7%=_'<{R Zxho`" MeJC{*^iP,$Te0Sb6$! $,xho4 SaB+ DS{e&1B Fe/47dj"" (S{Ú/4ĂQ"P` ծEliyC`WeX޺A&em&[ʇI8XUYY@Ly$b8XUYVM2U;ʅٞK\0@en' %hi+X-ip_P0Ɲ]F@ȂQ0[8ٮHЩt 0p/Q` *E4l`Q$5k! @e&1P1xYI `9|UFe!5&mFnO r6Mb,i", oU0XvUX( lwܦ   H@P@P@@(@ ( (  @(@ @@!@@!@@AA@! R(PH@D@ H@@(@E @$ $`(&&)` `0s P@a& (LB`($(@(I$@ Id(($$@@A)(@AU(LP `$ &0E ( a. @Q eL(c( L `! `(  &I\6 0`H`$@L@Pe0 L2\f2`& l@jT0(c2 `eC 1 eTl.00 2`ʨV $eC$L$C`rP$CP $s5N 2$ee@IA LPeL 2S &eCʆ0e0A5&I!0! Cՠ`@P0 OC 0L P @T`0&I!P I(!LՀ@Ԗjf$((j LA0$T.d@5`&(C `1 e I 0 Tc!LBY0PT& @5`@%L & T!@2( 21 C`Be@@Y@`@@&A (clSap(`P6L`eeP6ɔ$ eP[P$ a`e@2 C&` $$ PI2&0 Wd&LPPV ՘P@YI`(``0@՘Y L.@0 Ԩ0'`&)Ce&axLCjTc @22Ȑ2@3I$ a@e@Y5eՆ&a0I(Q 2d&$ a&aHH0P\!$L'aOZ IeC`:Lm!L@&aHLd (L22!BPj @WR (`T՘dL`((T 0ee @&aL `p̩ms5e&0LI0 CI@m ɓF `YTC  a0$L`TILf0L 6C2$0&a0ɐ a21 0 @ʀ`0P&KIP6&A`PP)3Q!L@Pe00e!Tc2II0 CIf0V L2L2 e00 j @ 0 3P 0L0@!L\m&, 02(00W0ɐ @ddP6jL2C\ < 0 dAL&Lp2P6 CU Aʨa'ala0bf2j@f`0Pa R AA&a0'a@(0$ ` fd0\3Ղ! 0! e$e a2JHdLRF # $C.{Nf8A0 CfIP dQ T<  e00l8$ A ef`00 Cd(M5 KjLʆ00 Cj !`rِ uj3@2kb$ L“3OP60 C Ս A2& gr2HZ 23@A2I y(.0 L a C& A LII($ @C1@\ə'@s2O$2HF ea L(̈́ˆ!L2df &0 # 6fee0!L 0pjd,L``0E$3&a6d!g(d&0&0l42HfL`3 j2IlHfBڞ$B* I.{0 !9Lj3LdA2 5Hf$f8L2HfpfLpA2&a8$ g&LMb `n3 @YY W=((\$ 0#$32 3_O?X4R e4 *"&09V$0C`@ A2I932pfa0WӜ3P|IPdre'e@fș 0g0 lCle?GBE P1r!@&L(:y P#ၜ0ə 2b2&e3'arfL&C6HfL`H|3)Cf.O"@$3(\$ a8yma̜gL(d&0і(Hms&&GՐeL 3je 0h@HXF>9\L-LL(dF <  $<$02Hm0 L2Hf2əPo L(d&LفkSK6'D00&frfPL2\''3՘@\!<Ty0(b&'gp <Ԁdmf 5ȵMf}jé a&LI rL23jfgIBL23@Yy&!3OefB՞`c.1T0$2+Km8yLb !k1j L0s0<&̜90ả&̜D&̜"fNۃ# ?k=bcPae51P5BrsdD0 IA1F:RW!Lj8 2X+@0 PF\s@E&̜9eOīC(8V$F rU#4C %1X(#9p0`F2rxL9(s[<" 9<5PsaΜ@9s"0 c*(Gd5F]Z#d * XGZcj"$3'`.j@3'f| jO<5B|dD2agfy~C,rZ\Z(!b]! #FFTQg Tfy*p*"930dmb@̜!f3BPFO5rx%93&̜30s>+|<#H<ՠ0yNIf 2rx$3sAޠ fWۻB,6 С5B1 JҲb:j uMd1'f 0 9D `LBfes>q}PZ# !T8@9<\FBPF0a朧$Sy> 9‘vҀ \@Ö2IŨ Y5B dўp ˆAO

g1gn@i@Tht0vUPAam3 #?g` * 8PJ1V1s# `9OڒC.#_eɈBTft:{H` * d`@M]XFACq1*R1*)U#| dj#FڸnW83j#9 `ԛ^]ԊJ1+{ĭX.k @B@cF20 #,@59穀9|u eÃ9$L-?*@ˠ0sN!sr v&yCgU#d `cձuQ !2If1hp 3T@I~>c@!/vUqY * @Uc0@U#@n:* - H1XjA ͟+U# *,QU!0g1!TP?,^!D١W}B(aĪ1 )Uk;*0bU? ` XT8j!X5B1b)"̉˟@2bp)K@M݊F!Ѳ^3;dgѰX5>Ve"f<5Ơp1.$E1Ҳ3€FݵPEYaP)B y׸jR&-;xU#$Kq1Ts! ԘpWЈ[7pa@R8 ab- *Xuԭ:TB@o64e8$Wja@cHt!Q6k5iɲcq5B "u<$0*Fa_aˆU#`lRz˂@v ui iĭb@!X5B0b*xfVHx.]q11.s+TQkǐ0b!X5B,!,ˢ}) :*CPA뎣#!dczF\X1JG!ĊU#oguрP qEPQU93ΨzUZ8j zjgZtTQ pMP#VB͡Z#@#V!^x# 0Z1ܵxˎX$P+aĪ2*9JgP#V@B&[#$u!cGll>VLLPUG HFuկW!VP#V!!+~Uf/CD1ԊF:B@Oz|=XYd BV:FaĪ#$cuA@:B$c\DKg#  j,|UGˆUhĪ#0bF:B1 `QG@+nݤKk=Ŭ!Zی" eXDT6aEgX+!۳ !8j1uaĺ=RؚKua2[P+s14FĪ#*Xcc Vj T0ba8 d#PQKgG ^4BPAkab#4f8a,VZw|` YG!d+*Xu/hW}k$!XkT+&jb@`bx:FaZG!tW}dccq@aZG"@Y^!0b#C8 Ƭjq!ec]Zq@H8"dVU0q`8p@PV#b#@Ez::B$#:ȡ #uA՞a-{p@,C-{p@qj|zvA*R^0cZu#Hac< V;{E3a18 18 5bĺ=]8u$P$#:-[ d mp@0F<#uFi_4EP+X4S8 H8"H8"H]Ǡ, !c!c:T ۳Xl1( Y4`ZC8 _=;$bZGq+eU[}W;) @e'@ AXU@2Q/iZG@(FuDF}TPv4jC,CV aĵP@DZCdUigNd$DPVYJbDz( jŠ݊Cau{k5kPQ+u@Eqq:B&NH8”]HgV z` =[ b*=]8YPSvb 4bbZ+4-*h k1 .*xz!\B uS1*B0'xZgZF*غ c@Tpc=BcxE,k=B$8ꂌ11+ o!m K9H@an(#X#zЪ-{Ԣ SvGPuH5etw@ʮ3q3zC@`!# z_xQ zL`+n(2-펺@cX+@2^ HqXpŵ>#1=Ut@\8^k2Q(Akc\rV!qG0egkDZ-i '\-sA KahESV0 C!#oG]0k=BdGXz|,DKHa0h2Uk 2!< kr KH"c pV)lw!qG!qG֊ ׊Z עHYըǡk=Bm}i! A+9Ly͵}l)H$0$8EP`\!8xê٨?ZjFh`Y@)\ P [OBSH(Q+Ҡ:@Z֊xhі( $chٺ4@8V<qc])\uHe*9Lye@VQ8CZ9H@W\gqx@0UI*=+@G\AJ5eZ+dG,HfGEqc`ZpPQW=4;qDU$8Z#c8nl]z ]ABfU0T҅m@cֽ+⠻ሷC_,%㤊@2#z3z;` zGax@X)8pZ\Xq@B8kA[HZx@tc~MA+2 `eRM#M Cx[ͦA` ^ &@azLZ%¥ $1&ʖNb6 maRtP% ]Z!R(tABB:Ha]Q:>i%h (ڃȁqtYҁd)Ҡ,e ܲAZB $&q\le bhK̡-ᶋ 8+|xA,zG}@Юn'-aB1.$-aQa X bk=ƢgR(4誃@hUG lU 18W:H$x  *xh[H"@E]!B!Xw\HB8 a@ahKx@ź Cĺm9R ڔ l( $$«0DaPZ$,A0jD 8e:z!0&6lYjbҲР4(p&b^_ueucq& (x)V< Pz€MAZkfk)lw+ԸQ=L K7p˥:pFP ȰriJI}$BB 2( J jnxp[7G4XQQ`kSPc%O$B 2àl F@/ 6!(P@deZ M 02*`) (Lڱûf'X@[H0`kS` m RzƣVBq!1+欻Ǡ%C[hNˎA ULۄ5@`1AWCkð'@  (:_SN'BF 2ʂCX   q6 ,u= %1`Rq$쏯1H-L @(qզ ԥǠ.q\HeXo^cX@@q<`҂òLA,WjK>e G|Y :H\6q uŐ1_@xNk$DucZu,qØ޲QN1YV4n6+ HJ" [HX,޻JlUri ( Dm A!c%@dLS @ NwWXx@00hk @P)T e9 $aVAa֦#?h+ $aY%L+ u!\xG1\ 6ŁjFv5kZx@@bB#< ԩ_Ѡ4h% ò^8Z,uڲW ?.C0= -0ipF (LDch[z $&0jS*zԛ-LȘ0A * $ [; e\d[HU찴)% KH0e i Pq '-YL[; Mm4g/a*p[@U0(NZŰ,a՘rS p) lm ,e +X] bˎ_@@qr¶=L[&e Gm,z ThFCk (B8mciZhR&sk K_]ɔl) &D4 e.Lے usRaYŞ`I$%%X@B 6X zIk,e BI)Tm=PH ikhNFQeBPT>ԊNRrMfZ:6X,a@iYdNBX4cf۵2HI$mK@j |?pWB,V0@vB'K`1-K qLȲx]Lڶ %X`,e ^4PNBX]q@8ZWQ4ઃ1Ԩ-ugz-a@81M%փ TWHBG]q]PxQk}S*ҶE"- @(lڔZ% [&lm"eI1v4)-*Ĵ`mc4Z+@ `MKHB&*4b)X@`1L6 n!h0à5@XaX@$4R21P6lm _dư,1mK@` ˒&StsER` &Aa֦ i 9[[@ òҝ]y1y)TX4!jaZ!Q@(^|)@ Q7ֵ0"^L5(zT6m ,eI՘4A%6=Х $0h- c`) [t9ZZLX,K9HJ >ѭ $$4RPh rL 6òIiXa0`ilNZJZU}0Pn5@0& jSp`cXa 6`L˒& v.-Yka zMm/vLe i 56zKH@qR2%MP(aUmT.`0V2Dmt*ȊHZq`8W$B_# CvP Tt$x૛ LC%M 1,K @bכ|`+9 Z  (LDqeel+  v@% 0R`+Ҡ0( L")V&&\ 9r9:f$4 viDȘ4A@\T6\cᲄQiIaY`1,KٶrxM$BV& .aYdKabj %PP,i[ "㰤 (L7,00wHE%Ddiltdž//@8YZ@hL$@Y2YғD 'ud 4V&¤4Y D@Fle"XLے % %@HhL$@B,0ٕ7-L 2%@ˆ.h,KzK;Lv) _OMR2r&KP(4 p!$a@(a­HDpiPpkŴmaJ(t(2t04'_. AaM0%=m_ rIM$0n6a`,\ȩ ōU/m 3hŴmimas-aPZ#PQ/h A/me `Y$a[Hö2@PBijIL$!qn g˅1ٖ0`Y¤4X0 wYeKH$Lj@ D1l[J4M$`1Ea[!UzB6㰥Sѡ\´@hKBv8iYfcm#$,K@Hh]lDcض!Q&Da>&wk]@ٲ HhMH@a`-aBƆr..@Bl"Q,Ka,aB\l .6~&@*vNm5G&`ÖNEl"¥Ka,aư]M&*4& \l)mK2J+:̰M$@ [9D 2gcvnm"0f 7xƕeasڲ'xdadd.76p i,i%+2Z貤f ( &0f Im0ŦPh !&0) C(4kMNeI v>Ћ8[4rML-a, .6!M%M 0m&pp& eI = EEg˒&1l01lWaD&pyK~<Aရ4f @P8&XY^y[=] BΛږ&@P8VlKaröIr4&'y@abNSn27H j`0 t D8k@Զ40AHhP 4: p0YI^l"[6`4+oj4dY$4Fm/^,f˒& /6bڶ4 aZ  pnoeIeBqI.I^l" vDcڶY8l@Tˎ] Ҁi   n65Hd1[4lYưmirK9ö 0`1,[axyK0P]iz :5'T'Ւ&0B t0&2@(ŴmW` H6LPhdۍ %4PlKB D@Tˎ]A]Lmʨwy{aXhizyN`mIamA0 Yii @q-i6abf鲤A^ A^d"2LdM7-!oQA^d"2:ȋ;F%Mb a8`1<-2Q{%%8ٖ4䪗dd/չ֞ś#[aIln$bi)Q<&0 DeItٲ@N4Io6XLm8BIy PRMD`1]e(J:͝C׶4N dT0bo,j4D¡H,DB Ȃ$#SafUp V"3M$`(N. &{ tL3nltr)Z8] `~le1d\c/h6*diB$6p%SZђȃRm&j / /2ٶ 5"D.Oy Rn ']e`d"J:M2@_ޢRUd Y8kVXZ.1md /2ٶM;Fm%zL"{Jwaf), Le&8ضa͞"3.DF`UD"#"M&2NjORp dۆ Lqm 1i ;'oz㢮 `.P0lۆ 4PB 'D`1\.ácqm8t9F$]M`@&U],2# &mn*0ضap7,M&2Pڇ22`a\ AdB],2Pt];Pj38 &]8(St]4X8ț;F*&&bXLrE-& Nd@quQ04ٛdd`a\J:_ʳcvulܢ @qnm3D8l@(/2()E1۶app,a y;GvnlLB  f"E`Gp $̂ atZDY84Tȭ)HDX8ț8M&2Pl0D@ɿ&Ʉ)N.AI'} t9 tD!Ѡ^Y$Fm&1Pl[v ڇ;YRl!+_)J:5Z8444D8ٮ 0! '6Lbd o0!Lm,䥜N(Nm=I(f6LbNA`"4}[fA^&J.&=w݆U^[7&&D4bea`Wp &2 t DF~ )5Il`,,Ҷ&C%&2vɎm& Ȓ&1ۗ Y8Ld,6;vuHXw%"/Bq-ZZ@$HDTcQ!!aM!dܗ L Z8K9H , Tqrن%tLl- bi(oeM#W`FwŜ1=&Dl m9@qmS$FU,6!!9 .0(Nֆj-0v禗߂ (/r{vW`E2-.$ T[D H76Uܸf$abrٲcrۆI {M\ vIe ɾ,o7g7b ۠ZOUM) HMr&Q"RA^MTL.0P y)p i*o nl/* "dV&"ParYP@R `ČKӦ &b2 QvqMP da+1 Pat XP@Ą͕"3Pabde0jmRbr٦I v&IT #@h$_`"* _@bg$*&c6  Ld@% d,gmJ8E|+O]oK[^@ P1l$vmo_)̀Ne^@ \a E2P 'ۀ2vFcQ(♞ 2d: v&brنIanm+2$1@(ط/I `! L5Qd ͕"3I&JarI VswɨE sm+$WZwf#&("ParaV|.$2ف etY&tb,ep@MP 'wOlιk[!žYHDl@B "BP '{I '{e&1XV_Av\"pǥ2Lbrn[xHRl!,dor P~Hdo2 "MA 6xfi)F ̂˖6:'^ZJV{ -/1ui)  'b@҃~ ?m`P<Eebo0dv ) ("" dkB/6 "BE .@cG7 ()Q.sWlv0 `b;)NuE|\٤NJf*ɄP,J,)ڮ XZHNx*`9\C@^eE6(do0 SapI Jxh/d[:wjob@K"BK @wE"3km-IL>$&j2P '{dQ0ISr ) @h|S{[!",}D=.Pd@&V^uI" `%0*d_@X&P d_`@4- m2BKT nK8i , bNdbpۦ Zxhw73@%;p vI4@2t깣͛`&D$0*}b*L^,`iz 21()|9r154 5P^2Q 0tqj]0p$tdlEc}Qm]!C*d_@bpۦ Z88P d^tԹC TMGؗ1չ喒E+[H d@-<8P d*FV,xu !lXL C Dij-/Yt<3\s063&ə eTc!2Cʘgrk<M"ed&'՞9 0d&mr @mM=8@20LLd&g&1'ϓmr^`2jp$3(d& m |4B 70 HI4Rd@.I pS328ƨ( 0ELf9REIfr%RIe&'GʙsfNd K@@DA=9PFeuN\oGc9xqxLЖBFr&g&C`<*ə e&< 39$px"4ehL͆ A!B94 3@0B#"fNŐ`gLUǨm@ir+dTP5FUe3:Bdd1'uky@ g&LfM>CcĠY`E!b&`Wg{7F("jT"0ả2rx0 y"k*"`2QK>ə aCPF2rx̣KPAUN`eF~@m,"?qTPo a0gOULKDpc G`,;Gar<9<S_#TjƠVPDA2z yr`y7P̙#`PF&T|*U8”IƠ€Q#cU# C9 0sS /{"C0axҡ5Bc5*vͰx`CoQ5ց""F`FCl䇡cv+a]X`Ú6&R 0Fd`U@S"PA9@91 9<2rx*j;pٲ482r7q5z"F@R;0so҈֠FϤ99L@9G9nZ"bb#0&̜)Df@fyj2oQ (#9?cQ5j0szCU>Zi&1I9g~*X h0aǨ[KaY=xT95  3糊`<TvDUf<@ɇ ÜTMbP39<4@ xjT9mkz&!YxPU ux ]T 1H1v3, Q4 1 \q>LmCF@Lynm;0P:n£k}>u/|TBfRY12eqHزH𐂺EpQ:B#!X8pHHk,Yfn=Y2c\k @\@ꈧ҅#H1ƵRGB@Zjc? c@ꈃ+ u/)bX Haˤ#"rЃOgL3)C:P:t!b1b1 Xk T8UˆP#C0;p PXC8֭B: !kƵ L"j0 pCkC@#:0Z8 XZxHq\G`1:Buܺ  Ef_1k !!aY]40#ԮHtˆU1kC@ :1@2E$c\Xǐ1VZDZl dx}IA*Fĥ\8(א xq! cQǡJ@ٸ0V{R1c2ղgX XUR ԨUq RG|׸4S-eBTX,C-<kCQCbP`b[Wz!b-h pC-늀0VHd bңc B"JFu q,=tˆcaD])9JgpCDa`=C0Z1"ふ!DcC_ѭ@T G@#:kC0f*1L_.5B 1^j( jP X*FZu|H06CKzE qFu X5:FcPDHx]8baZF1F]CF@W{cT8Խ5>u F07k}J=Gʔ2H @#u$Z1ƪ"Bm6֊Hw` p@@#u 0b] hYѪ`գFD\Aw+J9B1JX@@<>j@ĵP 1h67k|H` 6;2J+pcfQ/QG@6P-%:Ƹ1eJz{vڽ h@a  Ċk0=0ejW @5Fĵr+>k +0 ,h  1q/|aĵe*zlw^x@4 [QF\1@d#]륏Z CYx@0Ƶpu](+8@ak!C-< öXz8> P&1)~?RHGX!q$c1Exz!*+J3)S-< ` @ V=+9dC? X,5>V$6Kb]I,0APRiͅHX *Ƹ1H#vWbԭ|n"vKd"墲*{eiy\=sy4g/cX@Occ{҄-y^g2;ZK'?I%.]$Q(w%3VI9_x{zÿLvZhߞ0|6H }7ܤ>_I]Y-&D.;"0I,H)zZb>sϱ ӷ]g;l*,/*'o֕ 5H6P&C(jAon]~пB8 xqKrJy=A8p:P1(iĝ7X?ZE?l:яӼ1F8Tp=|'mX/D*H hrn@{j-y-B^ BN9qL6.[`[&dN"hG Nc].h>;R'=2L<N7nc͔K H^2{fUt R{:r2q%L&9$ws> ǽ>{E0织F6/""K{|̜ [ĞK~zw69UTUDDWvB}|*^l] wLxInL sGSTI*}{O nQi5urNl im;IZX\^&ЖV*UAޮhn ȇYQ|8VM3yll mS33a)IqeZ9[f[-+gրOE uG&WG5&6|_{;V]sϤL/O} ۚ\%1b- ZU̓RT}:pFg+i}WiY y럻+)2n$pմs9doZW dǩdoz۶mےcpz_UUU]?Vt.\tپpkU`__G뼪j$ҤLTԡOv_ Iht G3z|8Cp‡z1: Yp1ѳ00X8Ef?<n~PgC=xG0 )\O8NX3GY9S3SO8q`'PAzh3 }8Ss< f@ǀe D >l0z`88sz!x iO'>h#8 ~o@pwc 0z-`3 @O>O q@~VO=>'z4c<>A |yO#CHG=9ҩ=#=HO~d?7=`!|f`8tqH @?:= `N}=FcG8`HOp c=x< :t pcezQx!: @zF xhF taCO0z1|6G=c =Y~D0C= >!:u0/v|ѣpG0:0a84F8<Dl0|s9|0Sf@Ox}.LTѳ~JϢs?2>ѩ|408:!= h c@C~PzF?3k9E?2GF342k903=jpOЃ~V< 4z1#8!842C?Ss<N= = I?*:0x ':  9>}C|~cFFxo 1@|4t~n:`|q:>ԩcOl?O2zփD?1SO(:(|2O50qԩ=z/#=h~C3zc? |s?6ѧ@S 2~Bf?z s=s=!<c>< 1d3G!@3th38>f$x=1!<|CGtGzIp@>gz38ty Mx'z2sOcf E40< s>d4zpi1c@|tq <038pdd< G0C0£e88C0gffG2?00C=f ct I>ѡ'> yO03~N3H88C> Yc`q?4shx`1}0O{@OCh@?g= @8S>>'n >13DI/hǐ}:S >=~d f_ >7j`f3.}1FcO T<=>}:G}>'x=!&g~` y  a@3 9j|s>A>`u =q4x?94"zO}C #>42OuH?1>ӏaz/ |2COH4z u/1}8'T`x8p/ "=8@i52x9'~B>'C:0T??~@4:>tC}>0>1Op Ⴡx8D3hD  ~ahCdCg}rd@fOz҃Fl`40FpG#C}2Y Գ~Bx8D |F>Wc?~`0|OpzӁ1?4C>1c@N=< |:13gz1h:4O q@e:у\x@z³|c >h@4x}G p`i?`O0:>C}6pht3xz ɀN| 2CtYhG8L}EFODQ= г5: |2LOtG0GxГF< 0Nѳ~H 8>hh?Da}:s?i@tcfO:5x`@q i/xA 3tԡѣ<гcg}O  h~b@>ԡc`d@Y ycq F= y?2c:uSgAnzgx9S800h`8DL1C4z< t8l?'^:!1} =}2'֡s `` ѩ~@~j`'z9z @4`30G}'x9`8N=`88Fp pѳN}4eO5PT~n >G:FgF `x|2s@>z=00>G2Gx`0|3CC:i#:~p`  i4C >>ֈԣIˀ>1 'l2CCx9StY ID =' N0/ }Cx5CAcf 8`@0s|c B|310xЃN=t #= `z Q >gc '< @xGS"z##4D3Qz'd?3f>9:0`#ѣ!80xԃNу |.>~b'|!wvpkpz"V"V1+&ȋBWWGHC  <`,HcHpC2їH?¡G>~`Cg1g}8t ~f?0KA}231 xQ40 p p @H?թc=I 2`0z3= A 9zA~j F~f`=ԩ`I?15HP4Ќa83xСG=k4O ~D4xCL38o> c=~F=xԏNF3|O`|9ـ|O i3ft@#84s2gx~lOzԓ~npj>Ca'4xѩDd A}0C<Cz `0@d~dHap2cI >gs?2 `p!) }G'z z9lz`4?@>8O>c?W:S:p }<|3/D?$0XOz>10`>x18|:C?h0lN='cpEO@`>C y:~P`pux}g= `000`4`Of|2O0zCa}0pO>4|$@?3gz~jdA?02C 4n_|001t xң|2O0cp S|29Уp '1}69c~f0Tj```tA2/c@ = |60=YX344O}~PFCziO8p 0@ @|`F4zgx=qS30xֳpG`8z0zM y< FtfcN= IY< A?Otq z8>< ԓ <':DO8#h21O= A~P"8p}61>Hc1O@?Oj1dzf_I!cf@?s0g4 zֳ A4`?3x胁4>O8u gX?5< sh(8e>t|CF >x}2|4<:0lɁcO>e/c `u A:|<zOz4g2<!f`4^D?p 'Ɛ>G`\\x}4GCCOg}!zCpI裁Ozҡ=POH|4 a10dv60pxO0 .= 9 a1?9! q|<|2?juS}0s'=|x҃>)"L0!}s#p |*x!8p `g= >g|}0G9Oq:6[?1YFc@Fpc}8`t>|>00|4S> Q4zg:t |$z>z>0~BC|91|:gC?s?'w@?D>h1^N=uza~j?4= g5|?S}zЩFO| = l`xgpi3z#>1'=Q8uti@H:hA>| A}8cfh<=gpx>d@>Џ'F}0SA3CO2>ҩ/p<XϢ{ >~bO``@023=CCxp΁1 y`g<чC#2}h00zS= p i QOciCz}> Pp'}CNC<`sj!=}s30 @N= gp>ѩC>= f'u1 }@>Ӄ>աCҳ |1oDOti 8Ρ@=k~H1;`>0h<СԳόZtcя Q?8  `:t Lg`O gdt <3}Ct Џ90g<iSG=A?;c#8e@dfxҏ|2j4d`1'!|4Cg`=~u i 90 @?-Y}<tipt-}>Dc`4i_C=kD;'[C0` 'h?+d 32F>zzu“>Ocf cf`O ||:?Jk~bN=~` S̀f`` hx:uACb^ H1;ߐp~b `>'T?!p i1OA1cgC\\x80G4~@?;G}?k=q  Qd `4)N}=@F:p 0cfd1:#~mz01zGtx@ pfpc \O>|z9`` ssy`xG==G8= h~`8>l= g<< =Y?1g1 P#`|03x1OtI?4x }4 ><~j#O5x0`x >@>~L Y~x0/G}"|6p <>чx1C!8dF8e ||g}1K3:02c !|:C><`@ϢDQ8ue Y3l0zzЌ1pc90zСOX]Hz>O @~vOѩC3s>ч8 h^c4pO 0g=p_Dz8pq};1>9E?A N=5 15:5PGf`t p|:өgp >'X!}>ѯ `@~N `P :էNc~hS0|6cǀz֩x4tOcf3$c3uI/h?7>u248g=' Z|рh$@>~L GF`t < |6҇l g:0:E809G=T?08eQ:p'0@N0zcg=kt@@_eFԏa3'>x `0!g> q@}  Q>>'t~xf?cOH>AL)!<>я D4:3L h q `:==A>Oi>=ԃq:[ 9c> zOi !|0>Ӏt yhta3FO8N}ko>^:у>=g@ԩCGxg=YFP?> < Ā ?3|C?0:ȀN 70@z~d? < QS32Os30`!:Gd @|<z a`G|d@:0 `Dofty!~NFpIfOxPO8pl3s@tQdO>uczg:u '' I S>dxpf0 | Ft#00 ><i3g= Y>9}|ѩG=Г>}>c>Od8sԳ>< >'zO!@:`_8G=j2c'=q`>'D 8fQ#c`2zGz0>O#}1OxЏD4GGh>g:54O~f|0sta  F|10Y?GCp`g\0}":E?1'Og0':C8>f2>|8ty>!}9!zO 3= >ԏ }60zև@~`` gԩ'> | |O A3i sQ:>ѳ>)<f>ӳ~ǸI Az!d htg! @XzN<\3eOC>փvC}'c~D 0s?3zԳFiO x9|>(L3`O0< 8'= FO5:<0 УFϢ'`` f~LOc 0>ч2O^z14b >9xг>yt1tփ>ψ ~@ gGh4Cd\= Y '!0@i184z>p 8f`4OpG,8<}&:#xv!N>zԈ|'>}GxС@FOCt AO:|S?sh 7B#PiO2c` |8xx8#fp ԣ~B?60P4dO Q 1` Sf~Z}<>'zFIO40`'dh:tq?LqN>IHGXO230|<`x9Ol#80A3&>@N1'='Ti3q ;}0:u 'x=y~Ryxg0g`|3Of@+BOz I:>>``g Ѓ5CC4zuh<0i?4c 3 z1pjD'}6|у`:5|3g=q~d@hC?||Sh< 8~koVfaz1p < `Op `9e@i #D|>|;s SN>N=|C4s?2k ,0>4 2c>gxx }1<Ya tc!Fc0I'}o1O2`#:~J}!A3g=D3| P?3G>p A30sQN}Sl >F2CQp s=I>':t `d`<C}0<'xGO cC1T< ^2 [4X?0S?:gСǁz Oh}6  `fO EC`ǀ~b  C}$1' Gx|$:0:$zz¡1>'zЃN}<<y`GcppOp`k`8' pi <x H8G)'`P>80z҃>~1'=1џ"tqg:?by~n2gwvpkNzf"V1H\7BWWGHC =Y`>1C Qt`SzN O 5z0G}4cX24s `= y ԟ5x8584c}2d?| i?Ї85:``~_ht qY0ЏOIO#9`Oi4TĀtg4ѓ~{|HOC8>>G$H?2t IOzN!z>ң> zЩ4tGsxF©'!<`@`C0>С|:F 'z Г `D?6{9st`Ot>ֳ>C :`@LOPpЩ<s` `x4|=` Fi4 @8Txgxz1`гEGd`>20IСS~zc2op=y~DlC! SO1Ot9φ@:8 Ih 8Ei4Ou=,:#`_ zG}>sh}CϚ|҃~l =q:tӣf=g>>A`?20qL2C#8c:u 8qxQzNЃ>0s3ǁ }>p }c >F::_zp O~Vi!z>#~K:< oWPN7pjh8>>ϊ>GL s9` C?00C:!)g=@85tq}S2'T3Nz>҃f 7d@?::c}:>O>!x't `zЩ10` >׃>׃>N= Q?0=A|0QAOz@0xq4~Jf?3Sg:t ~D|_d t |?3C>koS}O@~D88!`i0 Lt zҏ`d?C 48sj;S< CC8810tOz4z@4c?5= |2pQ~l `p?3O2p Ч8u}0ѣGb&N90|3ƀ< 4c `;'':ptVң@0A Сhtj< 9O<f>ѓ~T:cx'l>f xO >G:t>ҏ Á!@#Tc 3>''8>4Dg^ч}Oe9s  Q3 1xҏ'tS}2s3OC1 DSp3#t>01SA41?|8O|( 8`8tA A:4z'z @;/@d`O`'=Ok0IO< ht y~t?2'c E?:>Ǣ(CŐF0Paf}ѩ Ї:8O8`0tc4C NN='0c@>= at<~e;:sz `d>Щg>pC0z!8tԳN>x0|'|sOz@^s=5!ghF>1)z)8DuFz9s?10\tj4O5=Yc z/4D~t|4=i~`3 =~h@p3f fC>ѧ8841xG:FzgzOp y~l?$D?0ѡglF3?oXphd1OpQq~JuyD88d@|> ``x'~ROh 0O<`cH `DC}0zp]3|;2A?3HN=>=I:0zև~`a`Op 'dЏ C>1@4u8 `0zӀ#:S>z'}68 <ApfC?0O8 )8D<OOГ '`z=[a:fƀ< i|GC't QO< >0cp}Gz9|01hOLMmsԣ~L as=GP?@OT0Ou020|4Щs x|`4zhF1>'8dz }~l> c}$z9f#z78D?C8pas@52CO:0x>ׇz1c g!:s"x 0|G}C>өc??aO`@G4 4s `0#^0~nЈ02C!hpyOѓ~b  9<<Q3|00k'``1 7P== Ѓ~@>F?0~z#}6|20 '~D?%1?2?BO>ө~B30<`pSc~`0@=s3G==>2)<'@>>9©0x|O |s8 У~ba?0~dG ѳ>:f?3N> wvpkxzX"V1uBWWGHC fcf<C~Rd@@OC8`@8 }ӏ1OφN I0=u hszhtOgdzNN= `?5uh@s?30 azЃp 0A|G}(xu:pc>O#@h0<У5tOxo '_<c08cz `G} w>|:2W}׃!||>`> S|C I?"<Сg}xb G~Fdd?G'0al@>4H3|:z9g < q@?6'=IzF?7S85x С0s`g=0t |2S?2G}O0G}8g1OpI39À^8E p>֣Ft =is=}6e =0zc AzѧCxF~@thFz1+)>GcчTOx|'= Ij0 <x>O G4}6c|8ч| 0~bd!@<cNL?08:u `>ѳ0z0 Oz}03> '= GxСSa>gx8u Op G>z|3= A?5|2OgL}7/>p:pDO1`}8Щc> QF8PxӀ> 1?3|O5000:_z҃~h`O1}O>`t G}> ?CO!= 48`}` |0S#=< 8Щ~f 1LClt'5DO2=s}3ux 13я 9Sz3 =Y |0 чѡc# q@< I>>u~bx}6Gu1Ta210:^>=@= x `xGxe 30xe/8cHf# A0N}|O #ѡС'ѳ>=!8O'=Àt|'}pGG y>0SЇC}2s;_}8<'d `SOnph0 `@D8>O>5:>9ѓ>9pCP8sT><80zֳF3}>c> 8Gzj?`1h#8>0:ei>z>?Wk(=3:pjpi>= ds=~TX ~F?pjt >`9ғGs@ 4xFlpA:d= I0D?5cC `d0 nFoE `|>h>=i'DOp }6G0<|84=I~d:C:4|GA}4?@Si>я >1S3O4Cx 38 <a|2CGt>O'~ls |g=A?1|00 QNt СSOh4= q@8414cѡg=>Џ|8d`“>֡C!0e9O5zF>9C xyg}8cԡs @3uq@:p> iEA!zOYtG7':4| @5X?@0Y4{ tYxg@|-:GhFp 0:|G z3}03}2уF:~d`>!:dFptp34У12}2?`3^5|20D>!|0gt u: 4>C:4g}8:G>t10x45ߝo q-!|0}4}2N Y s=@|=C~``:u I 'C13S@z130'pj>>5Q?1@e?0Of@3004<c:!|1[}oȀ~F8t D?1u0t}|8C4O 'Sc`1!|}}<z~` z`?>ҟ5Ѓ8 `(2h0~f>Gx dcG8S13g}2tNHo6w=gL~fN=|:CCyDCS`8ui3c 8f|ѧ@ FOx>= 009s 9<0<xԣxԃ#zу4~F?0s ` |6pqCxԳ ap  `3H|0>ӌ` S<':<F>i@qv?5ѧ>g8~f>ֳO1ht `>!q0ggOdϺ2?`}6x== h8~w_ O1OtG='G=F'`@DC?;' xС9sx5|'ujp0 O|F? 0')}t|CF'1g!|?LO|1Op}<0i@FE?0?FwvpkJz"V1yBWWGHC }Oe9s  Q3 1xҏ'tS}2s3OC1 DSp3#t>01SA41?|8O|( 8`8tA A:4z'z @;/@d`O`'=Ok0IO< ht y~t?2'c E?:>Ǣ(CŐF0Paf}ѩ Ї:8O8`0tc4C NN='0c@>= at<~e;:sz `d>Щg>pC0z!8tԳN>x0|'|sOz@^s=5!ghF>1)z)8DuFz9s?10\tj4O5=Yc z/4D~t|4=i~`3 =~h@p3f fC>ѧ8841xG:FzgzOp y~l?$D?0ѡglF3?oXphd1OpQq~JuyD88d@|> ``x'~ROh 0O<`cH `DC}0zp]3|;2A?3HN=>=I:0zև~`a`Op 'dЏ C>1@4u8 `0zӀ#:S>z'}68 <ApfC?0O8 )8D<OOГ '`z=[a:fƀ< i|GC't QO< >0cp}Gz9|01hOLMmsԣ~L as=GP?@OT0Ou020|4Щs x|`4zhF1>'8dz }~l> c}$z9f#z78D?C8pas@52CO:0x>ׇz1c g!:s"x 0|G}C>өc??aO`@G4 4s `0#^0~nЈ02C!hpyOѓ~b  9<<Q3|00k'``1 7P== Ѓ~@>F?0~z#}6|20 '~D?%1?2?BO>ө~B30<`pSc~`0@=s3G==>2)<'@>>9©0x|O |s8 У~ba?0~dG ѳ>:f?3N> fcf<C~Rd@@OC8`@8 }ӏ1OφN I0=u hszhtOgdzNN= `?5uh@s?30 azЃp 0A|G}(xu:pc>O#@h0<У5tOxo '_<c08cz `G} w>|:2W}׃!||>`> S|C I?"<Сg}xb G~Fdd?G'0al@>4H3|:z9g < q@?6'=IzF?7S85x С0s`g=0t |2S?2G}O0G}8g1OpI39À^8E p>֣Ft =is=}6e =0zc AzѧCxF~@thFz1+)>GcчTOx|'= Ij0 <x>O G4}6c|8ч| 0~bd!@<cNL?08:u `>ѳ0z0 Oz}03> '= GxСSa>gx8u Op G>z|3= A?5|2OgL}7/>p:pDO1`}8Щc> QF8PxӀ> 1?3|O5000:_z҃~h`O1}O>`t G}> ?CO!= 48`}` |0S#=< 8Щ~f 1LClt'5DO2=s}3ux 13я 9Sz3 =Y |0 чѡc# q@< I>>u~bx}6Gu1Ta210:^>=@= x `xGxe 30xe/8cHf# A0N}|O #ѡС'ѳ>=!8O'=Àt|'}pGG y>0SЇC}2s;_}8<'d `SOnph0 `@D8>O>5:>9ѓ>9pCP8sT><80zֳF3}>c> 8Gzj?`1h#8>0:ei>z>?Wk(=3:pjpi>wvpk z:1BWWGHC -D>\2>G4O00yh}?/4:8O,cpGt$n'g0<0?=xУ~`@Ox= >O G=G}>= `{:@!@ Q l#~Fx}G:'=yN= yQ~P =G}:O#ap=hpI0=9G:n}0'}:cYtxO i xԡcS0 |'}(8t~pz900< >փ0p`~D@4fCx}< 94>Сg>A?6` `xGcf 8:z41_},<0|\L΁Q3sL idL `『|08|u1:L8< pqG=gp `d A  ~`OQ9O>Wf 8>1`l Yx=8e?4N= Y` 7 +}gw'85D4z~KfD gE |Ssx9@0_СNCxg ~z гd }:O8OGitxO >ЩG |  XN=q_ ~AA3`2O C>D'd< `:0 =0` `lAAyƀ >gg a'= < 8F?C34'Qf Y 3:@?S>~p ]O2s:>Sh57voz>NCx 0c},ak>'48CO88up3= 0scG@ 1s? p>'x a?z,x12|'=480̀>1q |04c|cO:=Gf0|2џsGz9|5O `Dfz !xGxx|:'=@zG=a 1v>wOd?6|0}8'pOz@8t:d >Q#1b W54O4/p~FFp84}2O`Hx2Yـ<1> 5w=k@ c@@c~`@1`|0Wt 8F:vkQI4:5thp'pd3g~|I?g\1 @<}6ci3h< ~J#:eG|c@1` ~tӓ~a_ D xGѳ~`x >ѧ2<9s~iA 9VO9!ѩ0s|8 `}:c x Gp>'`>^ CxGC0: Cx `== `| 20'0xғt 5zg𤧁@p4: wCxҧx|:1z~`~Nzx1 l c0:D29= `0C}2s Գ~V 8uN= :pYF a` <>ӓ>Gx9' ~bp/x9pt / kc>G}8?js>xO1}6}:10O~H|4CS= fOtp Sfs:DG=9gz}.:5z'zi` 1}=g}S?:>4Pti?0hpCd?4}8OF3@3G!LH<#`=3=c8<D?6}3}6S7|cC842O'8O8 T?Gwvpk z?:1R/VBWWGHC ;`:OP0C0g= 8~L`Ox~D`}SuA g=C~b?000'dhG82G~H  :exc h h0z=i@x ЩS=wxՓ>{ǁ8D3'zgzui`xgQ= `:яftIp|0g=`3c@4GNS>ѣ!Փf`:@ 18D>#=>̀|4'}&69OtQ?095Sp `8c?6Su|(x'}чz:sx>9O<>O 0=k4`Op~P 2w>q?6 `>= 3 ҡ809}c'ch< yC= 1x9G֣>סui1П0'}0OHxax9v/x~H?7O hDOСc4`O8<0ghFFDO@OX>Џl>gxzSdOO0$`I?1ap~axu `>5c|<!| }00c|GtpA?sfaf`?=>`>p i`“~@?'X ?!08Щ1?0ge 9td s h?I @xO G|<>'X>9P `pQ?10P?2f Y3 R~P ``L3Ct 1@3~T?)2 !0 >GuDzgCD hcd`L:4x9=j4:t'32=afdx|0:490c=A8u`` zOjt s>ң>< adX8F@xԧ}4c``xtQ}:< 7hz/ U4xУ 3>>f @ft G40>C=f胁11= `p0z֣>D>‡x>ӏA>>p|0hfA`ЩC>ӡpг> <|1#XD?#2}2ѣFc#S Gd~|_p0g}6?nSx }>N=  a8g=a©}<pЇzG 6ot#G0@~P id`@90гc >>ҧCx> xG8 a@/c~JC8sDQ#:4huA>2`= 1=`!֓O5| sg <yЏ#>1S >C>f?1G}2O| x1N}ԇi:@Y= '= >ƒf pp<Щ~X 3}<:t'}8p#fc=> A>'zЏ cC|̀` 8t 8es S}|0c>1c hpasnoI}<p i@ gp>էzЯ =A?40'>'DO|` 0:@z֣> Faԡ`x0̀>G0a>©Fz }:fzN=c`<0@ [|1~~Sxz}1xЏz^|2Oe#:5d~fO8k;sO:`> 0x FOb;ԃD At A?3g}!|6:t Џ}c'x҇:hC5``1c|G8>O 1O\0=}G18'`18C i f:d?1s< >~APETAGEXM Date2004Track02/10TitleSilenceGenreSilence ArtistpimanjzigAlbumQuod Libet Test DataReplaygain_Album_Gain+9.27 dBReplaygain_Track_Gain+9.27 dBReplaygain_Track_Peak0.229712820826Replaygain_Album_Peak0.229712820826APETAGEXM mutagen-1.22/tests/data/mac-390-hdr.ape0000644000175000017500000000020012146763412017710 0ustar lazkalazka00000000000000MAC <D, nd' RIFF*WAVEfmt Ddata*|L~c-d= YkRJnwmutagen-1.22/tests/data/has-tags.m4a0000644000175000017500000001176412146763412017530 0ustar lazkalazka00000000000000ftypmp42mp42isommdatlibfaac 1.24B 2G!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I##moovlmvhdELII_@iodsOttrak\tkhdELII@mdia mdhdELIID~!hdlrsounminfsmhd$dinfdref url stblgstsdWmp4aD3esds"@ b sttsstsz (stsc, stco I ctts #udta meta!hdlrmdirapplilst!toodataFAAC 1.24#ARTdataTest Artist----meancom.apple.iTunesnameiTunNORMjdata 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000covr_dataPNG  IHDRԚsIDATxc| XoIENDB`/data JFIFddC    "##! %*5-%'2( .?/279<<<$-BFA:F5;<9C  9& &99999999999999999999999999999999999999999999999999" ?jfreemutagen-1.22/tests/data/silence-3.wma0000644000175000017500000007644412146763412017714 0ustar lazkalazka000000000000000&ufbl3&ufbl4 testܫG Seh J-pM=\Wy$}gb@0 ^4^4_. Se:ӫ Se FC|K)9>A\.sk en-us]&EG_eRů[wHgDLz IsVBR4DeviceConformanceTemplateN1t E˖˥r2CiR[ZX0"?40)54I"@^PDWMFSDKVersion10.00.00.3646WMFSDKNeeded0.0.0.0000 IsVBR(ASFLeakyBucketPairsr]t0u ȯO 90W2 # @Bk\L 3@KL @Rц1HARц1H!Windows Media Audio 9.1 Lossless5VBR Quality 100, 44 kHz, 2 channel 16 bit 1-pass VBRcܷ Sez@iM[_\D+Pÿa $cD[?4!?4?4u{F`ɢ 6&ufblh J-pM=\Wy]?4 poY. /_~ /_~ /_~ /_~ /_~]C?4V /_~ /_~ /_~ /_~ /_@)54IJ^43I8 J-pM=\Wymutagen-1.22/tests/data/no-tags.m4a0000644000175000017500000000552212146763412017364 0ustar lazkalazka00000000000000ftypmp42mp42isommdatlibfaac 1.24B 2G!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#!I#mdatmoovlmvhdEL_@iodsOttrak\tkhdEE@mdia mdhdEED~!hdlrsounminfsmhd$dinfdref url stblgstsdWmp4aD3esds"@ b sttsstsz (stsc, stco I cttsudtaqfreehdlrmdirapplLilst!toodataFAAC 1.24#ARTdataTest Artistmutagen-1.22/tests/data/no-tags.flac0000644000175000017500000001112412146763412017603 0ustar lazkalazka00000000000000fLaC" BzAf鄚0 ˩OggS6aNF('/24Yk ?O?s?????????>||'O?C3y?<'??$?ϟsϟy??ϟ??2s?g9??? ??'?y?'? ϟ???'?'???????39??<>?'Oy?CYl ??93??>?'ϟ???O?sO?y???y??????O?~>O0O?g??3??ϟ?g?9?3??ϟ?|??Ye ?3??>|?gg?s??3??y?|?s|???>?'?? 3g???<9?s~??$??s???ɟ????>3ϟ?y?9g??s9O?|g?????3g~g?ϟ?3??>O???3ϓ9?L><??g??????~????'3??g? Yw ??gϟ?|3?~s?9?>~y??2g >|y?ϟ???O3?y??3'???N?ɟs?9???3?? ?s|??????>3????<ϟg?3??gs'??0?'g???~g???3g??O?ϟ??s}Yy ???s?9ϟϟ9|sg&|>~Ϝ?yg?3??g?<~O???~O?>?$??'?OggSaNF(u6.3>?3?????gs<?9?9|y'|??|@0O??g~|??>3s??&~3??9??ϓ'O??'??9 ~???'???ϙ??9?|y9?|'?N<??"Y~ ???O??'<?????~ϟϟ??O???9?33y|2??ϟ????????3?>s9O3O>??g'??>g?y??9y>???yyg?y<9O?Ϟ?9ɟϟ?3~?g?3??? Y T ?y??<>yIg9<39>3~~?ys>|~s?'???Bg???>sg?9?s~??$3???ɟ???3ϟ???y?9g??s<Y ] &gO??|ys?s~y<??O????????3??g???|&9I93?g9 >?|g|?O??~||>g?????????ɟ??'?~?9'' $9'?󟟟'|???>~?~'<~gg?'O??O??<s'O|9??Y O 33y?99ϟy?<3|?ə??????~??O?ɟ??>?9 ??'??'?y??p?>3|>|?????|?gsϟ'?|???????9>?y?OggSaNF(UmL+3-g???ϟ|??yϟOs??>?~?Oys3?3ϟf93?s>s3??>y?|??>?s?32g>????????ϓp~r'??s~ ??ϙs3ϟ??ϙ?9#YA ???|'???????<?9??s??????>|'O??>3y?̟'??$??s??ϟ??'<3@0?????~?|???3O?9ϟϟs???O???&s?|?yO9?9??O?~??<?|~|???~sO3@YF ?g9?<Ny||??|??g?9OggSVaNF(WU2302?O??|y?>~s|99>|?gg??>C??3??dϟy3?~O??s~yy?????g??O? ~?g?3?????~?3??? |?C?>?'???f?'???>sg09?s~??$3y???ɟ???y3ϟ?'3oY 'y3?~~gO?~?ϟ??y????s?s???|?3?g??y?3?<??????d?<????dOs$&y3??'??s?~?<?'3~y~~?<?3'??'?y??y>3|>|?????|?? gsϟ'????????9>3y?g???ϟ| ??yϟ?Os??<?Y ??ɟ??>?9?3>y???s?9ϟϟ9<?sgg3Ϝ?yg?3?g?<~O~O?>?O?O9??' ????>??????>||9>??s????9<>?Oϟs??ϟy? 93ϟ??ϙ?9??????~?|??3O?9ϟϟs???3???&s??yO99??~??<?Y  ?????9??93?'?3~gO?y9?3|?33??>????O?s?g?????93?OggSaNF( Mm\540???<?O?~>O?D|~|???~sO3?<<3????gyϟ?9???r??ß9?g'??|?ϙ?'?3?|3ϟ?|?Y ?9?~|????'ϟ??ϟC?y?9?9s$???|?>s3<9???3O???s?????>5Y# '~yy?????g??O?9gy??|y|???>y?|?9Oϟ???y|?<>?s??~'?<>?~????'??O@03ϟ?'33g??s?ɜy?~?9~y'<~g???'O?Y- ?s??????&g???????'~????'?????'3yy>?y??3'??ϟ??N????9?ϟ???ϟ?'|???93???>3??<O?fs 3?>? '???>p Y* ???ϟ?rgg??ϟ|'?Ϟ?39>Ny?<N??~?3??f93??>s?>9?ssyO y|~????|ϟ??>O9?s?|??O?3??9??~??>||9>??s????9<'?Oϟs ???'???s9?|ys?'?N<?????9>s~?9Ϝ?I?OggS.aNF( u.55???O3?g?'|?????9ßy~'????>???9??93<?'?3~gO?y9??3'?3??>?????O?s?g??????9?9???<|@0y?'??9?O>???~'?y?3?>O???3?y9?y|'s?<9=Y1 ??~>O????|??O?93|<3>I??>?9|9?~ß?yϟ'??>??s?'???O????N̟?3g?O>?>s3<9?s??3O???s???Y6 '??s???y?????g??O?9?3y??|ys>y?|?9O|???y|?<>?s~ß'?<>?~?????ɟ???y3ϟ?'33 g??s?ɜy|?|?ϟ?gOggSvaNF( ȕcJ+4,,3??>O]EY  ?Oy????d????g??'???????y???'?3y??N?C|?s?~s?9?>~y&>|?Os??|>g???'???@0?9??>L??93??9~????O|9'???9~|~'yO?g?~???O<293s>O̓?$??9~^%Y! 9???Oϟg's?????&g??????'~?<s'O>|9???'?'y<~>g?>3??39?fg????0?ssOɟɟ??>?9??<<3>y??9??9ϟϟ9<?|??sgg3|ϟ ?yg?3???f3<~O???|?g??'g????>O9?s?|??O?????3y?̟>?O????ϟy??ϟ??'<3??O>I??Ny||??C3'?????????????g|????s?|g?yg????|ϟ???'?'????9????sϟ>?'Oy?4uYl??93??>?ϟ?y3???O?y?????9?93???<<3O???<9ϓ???9?~O??g?~?ϞNO'Cɟϓsϟ9?g'??|?ϙ?'?3|?'?????yϟ?g?9?|?gg??>|~sy?|?Cs|??N̟?3g???>sg?L??3O?3y???>3ϟ?'33?~||g???O?|gg~gg?ϟ?g>sϓ9?L><??g??<~?<?'3??g?XYw??gϟ?|?s??y??2gg???????ɟ??s>|?9???3?? ?s|???93???????<O?fss??>?~g??'g???~g???3g?3??ϟ??s Yy???s?9ϟϟ9<?|???9??|>~Ϝ?yg?3???f3>??''???|?'????$??'>'?'9??9?9|y'|??|@O?<|??>3s??&~3???????<~r'??s~???'???s3ϟ??ϙ?9?|'?N<??Y~???O??'<?????O???9?????|''~?y???3>????s?????9??93?ϟ?y3???O?y????????<<3O???<9ϓ???9?~O??Cg?<3'?9gy?s?~'?y??9y>?>O???3?9?y|'s?<9O?Ϟ|9?~?g?3???zY T?y??<>yIg9<3??>|?gg??>|~s?'???f??'O>?>s3<?L??3O???s???ɟ???y<????y?9g??s<=Y ]&gO??|ys??g9?>~gO?~????|gg~gg??ϟ?>sϓ9?L><??g??<~?<?'&Y Z???y??r??gϟ?|?s??y??2g~?9~y?yO?g?'O<s's????O̓?$??9~?>???|||9??=Y O33y?99ϟy???3s<'?????~<'s???????g??ϟ????9?O?ɟ??>?9??'??'?y??y??9??39gsϟ'??|?ssO<3'??3???yϟd~?yϟ??3???~??O@1Y H?'?????>Ny?<N??~?Oys3?3ϟg???>s?>9?ssy|~???9?s'???~O9????'??|ϟ?|?II?D<<O?<|??>3s??&~3???9??<~r'??s~???'???s3ϟ??ϙ?9*YA???|'???3?sO?s???????9>??s????9<'??$?????ϟy??ϟ??'<3@?????~?|9>s~?9ϟϟs??????3?g?'?????9?????g>s|~|???~sO3kYF?g9?<???~g?|y?>3?'g?OO??3??ϓs?>g?y???'??|<3????gyϟ?|'Oɟϓsϟ9?g'??|?ϙ?'?3|?'???ϟsO'gL?Oy????ϟ???>yϟ?g?9?O??|y??<>yIg9<3??>|?gg??>|~s?>s3<9?s~??$3y???>3ϟ?'3Y'y3?~~gO?~????|?3ϓ?9??>L??93????'??s?C~?<?'3??g?ϟN???????f????g?????$s???y?~?93>y?ϟ????$???9?f~?????'~????'|?'3yy>??ϟ??><?3<'y<~>g?>3|>|?????|???|?fg???|?ssO?9???3????s?9ϟϟ9<?|???9??|>~?|ϟϟ~'?3??g?<~O???|?'????$??'???<<O?<|??>332g>????9??<~r'??s~???'???sY?|?II???|'???3?sO?s???????'O??>?s????9<'??$?????ϟy?93ϟ??ϙ?9?|'?N<???????3OI????O???3?g?'|?yO9ϟy~'?O?D|~|???~sO3?33O9>?s?9???r??s~rs?rs??'|?'?????|?Y?9?~||?>s$??????3>I????s~~~~s?|9?~?g?3??????s|???>?'???f??'O>?>s3<9?s~??$3y???>i Y#'~yy?????g??O?9??g?ϙ?y???>y?|?9O|?????????>?'?<>?~?ϟ????'??O@3ϟ?'33?~||g???O?|gsϓ9?L|Y$33??ɟy??????y??r??gϟ?|?s~s?9?>~y&>|?O??~|?y??~?'??????ɟ??~?9~y?yO?g??~???O?NY-?s??9?f~??y332?'????'|?'3yy>?y??3'??ϟ9?????9?ϟ???ϟ?'?ϟ?pY*???ϟ?rg&|??Or>??Oys3?3ϟg???>s?>9?ssyOϟ|?????|ϟ??>O9?s?|??O????9??~O39???gg3Y?9???ϟ????39???9?????<?9???????9>?3y?̟>?Oϟs???'???s3ϟ??ϙ?9?|'?N<???????3Oϟs??????3?g?'|?yO9ϟy~'?????3?>s9O̜??'?3~gO?y9?'?9??3~ϟ???O?s?g?sLy?????<|@y?'??9??y?y9?????39??<g?<3'?9gy?sg'??>gO?3??9?y|'s?<9SY1??~>O?9?~||?>s$??????3>I??C>?9ɟϟ??y?yϟ'??>?~?3????|0|??N̟?3g???>sg?L??3O???s???Y6'??s??|ϟg~gg?ϟ?g3??>OY ?Oy????d3??ɟy??????y??r??gϟ????|3?~s?9?>~y&>|?O??~|?y??~?'???@?9??>L??93?~?9~y?<~g???~???O????|||9???'??'?y??y??9?|>|?????|???|?fg????????9>3'??3???yY"?9???3????sϟ??'|sgg3|ϟϟ~'?3??g?<~O???|?Bg??'g???~g???3g?3??ϟ??s???????????????ϟ?|ɟ??<?y?y#jUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUPTUU*UUUtTUWҪUU]UUURUUU]UU]RꪪuҪWUUUUU]UUUUWUUUUUUwUUtUUjUUUUtUUrҪuUUUUUUURUUUT]UU\uUʾUWʪUUUUrꪪUU]U)URRUUUUU%UTUUUUUWUUUUU*.UUUUVUU]UjUUUU]U*UU*UW*uUU]URUW]UTUUUJUU]jU*U.ꪪUU]UUUUPmutagen-1.22/tests/data/106-invalid-streaminfo.flac0000644000175000017500000001112412157371172022332 0ustar lazkalazka00000000000000fLaC Bz<YkYlYeeYb)YwQYpZ$YycY~YS+9Y TLY ]Y ZY OY HaYAX YFxYgYYY^vY7Y-BY YY#\_Y$*Y-Y*eY?Y8Y1/kY6Y FY!i3Y"Py#XEmutagen-1.22/tests/data/bad-POPM-frame.mp30000644000175000017500000000372012146763412020417 0ustar lazkalazka00000000000000ID3 TENCWXXXTCOPTIT2Emit and exudeTRCK4TDRC2004TCON12TALBemit and exudePOPM#Windows Media Player 9 Series{eTCOM pjat lainTOPETPE1sheCOMM  häst Xing>. !$&(,.0268:=@BEGJMOQTWY[_aceikmpsuxz}7LAME3.92 $E.0+v ?@  mutagen-1.22/tests/data/id3v22-test.mp30000644000175000017500000001200012146763412020005 0ustar lazkalazka00000000000000ID3'TT2cosmic americanTP1Anais MitchellTALHymns for the ExiledTRK3/11TYE2004COM-engWaterbug Records, www.anaismitchell.comTEN iTunes v4.6COMhengiTunNORM 0000044E 00000061 00009B67 000044C3 00022478 00022182 00007FCC 00007E5C 0002245E 0002214ECOMiengiTunes_CDDB_19D09130B+174405+11+150+14097+27391+43983+65786+84877+99399+113226+132452+146426+163829COMengiTunes_CDDB_TrackNumber3@7`  5l.oGF8DdEQ5S0ѸN;,ܙqӜu6&Ǟj:S{u繄Ѓ477<-٦Q,tabX; v$zs|K"%:G(!Gqʊ;W q -ഌ\M0$: 2]sstIe̾xEI-$tLE'ENM:jCFV/֍VGBhfKsstRrꖊrP9~0dÒC1@J9LAF26$LK2/&dlbdH`7>@SCȁ,BR+ WV0h`ڀT@& $ [ v3R`*# [‚p9G#t9K, I&kLu Vb6y8߳A@t$AB,?cIJa;wcɘcwbjx҆=LLxW}Qhn"/4Ӕ|,D,<$ 8ѨӂCèa3c~;8Y>I0G%^nfH.!J~h[𝅘𝅥𝅱~9&(f14H`N?|,!*1ԁˌK 3q86I?ZfSS7R9pPR,1&"0ot vt|tLCK),81Nbx20<\(8"\ 4rI$RɧBYeH"BR HMZI$H@@JH ADR)IPD*LԤ&$HJQRȅB`ڀmo_mom:mUՅ]]]]XUՅ]]]]\ֵtNP`Կm~_omoUՅ]]]]XUՅ]]]]\ֵtNm[_mm@/mmmmUՅ]]]]XUՅ]]]]\ֵtNfη]_mm2/m|Ӄ'UՅ]]]]XUՅ]]]]\ֵtN'mq_mm@/mm٠mmUՅ]]]]XUՅ]]]]\ֵtNmX+_m[m/mm@mmUՅ]]]]XUՅ]]]]\ֵtNbm~_mm/mmҗ[mmUՅ]]]]XUՅ]]]]\ֵtNm[_mm/mmmmUՅ]]]]XUՅ]]]]\ֵtN mZ_F/mm`mmUՅ]]]]XUՅ]]]]\ֵtNT,v_@/mm܀mmUՅ]]]]XUՅ]]]]\ֵtNtFO_mmmmUՅ]]]]XUՅ]]]]\ֵtN8οmp_ummUՅ]]]]XUՅ]]]]\ֵtN&_mm/mmmmUՅ]]]]XUՅ]]]]\ֵtNmOO_mm@/mmmmUՅ]]]]XUՅ]]]]\ֵtN3mՐ3_m/mm`mmUՅ]]]]XUՅ]]]]\ֵtNlmJ_m[m/mmՏUՅ]]]]XUՅ]]]]\ֵtNQm}_mm/mm[mmUՅ]]]]XUՅ]]]]\ֵtNmo_mm`mmUՅ]]]]XUՅ]]]]\ֵtNJTӶS_ml @/mڀmmUՅ]]]]XUՅ]]]]\ֵtN~mw_mmMmmmUՅ]]]]XUՅ]]]]\ֵtN2UI_mm/mm,[m[mUՅ]]]]XUՅ]]]]\ֵOggSO'r&:-]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]tN mp#_mo/mm׳mmUՅ]]]]XUՅ]]]]\ֵtNBmv_mm/}WmmUՅ]]]]XUՅ]]]]\ֵtN'Կmp_m/mm۠mmUՅ]]]]XUՅ]]]]\ֵtNpm~Vmm/mm@mmUՅ]]]]XUՅ]]]]\ֵtNPmn_mmCdmmmUՅ]]]]XUՅ]]]]\ֵtN7mq_mmomխmmUՅ]]]]XUՅ]]]]\ֵtN:Կ{_mmѠmmUՅ]]]]XUՅ]]]]\ֵtNP\moO_m3UՅ]]]]XUՅ]]]]\ֵtNvF؀_mm/kMmmUՅ]]]]XUՅ]]]]\ֵtN=Xm_mmݠmmUՅ]]]]XUՅ]]]]\ֵtN$mi_mm/mmѠomUՅ]]]]XUՅ]]]]\ֵtN/ _mmGm mmUՅ]]]]XUՅ]]]]\ֵtNvmh_mmomUՅ]]]]XUՅ]]]]\ֵtN*mz_o/mmmmUՅ]]]]XUՅ]]]]\ֵtNhmx?_m/mm mmUՅ]]]]XUՅ]]]]\ֵtNcmJ_m[mg/ZmmUՅ]]]]XUՅ]]]]\ֵtNNmq_/UՅ]]]]XUՅ]]]]\ֵtNqmd_mm@/mm mmUՅ]]]]XUՅ]]]]\ֵtNukR_MmmUՅ]]]]XUՅ]]]]\ֵtNkms_o/mm mmUՅ]]]]XUՅ]]]]\ֵtN{Կ_mm0/m[ mPmmUՅ]]]]XUՅ]]]]\ֵtN1mn_mm/mmї[mUՅ]]]]XUՅ]]]]\ֵtNFm[_/mlmUՅ]]]]XUՅ]]]]\ֵtN'my_o@/mmPUՅ]]]]XUՅ]]]]\ֵtNrFKX_mm/mmm5UՅ]]]]XUՅ]]]]\ֵtNmQ_mmu/m;ͿmۅUՅ]]]]XUՅ]]]]\ֵtNԿmN_mm/mmݠmmUՅ]]]]XUՅ]]]]\ֵtN mE_lo/mm׈mmUՅ]]]]XUՅ]]]]\ֵtNymi_?/ym֐oUUՅ]]]]XUՅ]]]]\ֵtNƿӚ_mmٯNmUՅ]]]]XUՅ]]]]\ֵtN8 dg_mmm٠mmUՅ]]]]XUՅ]]]]\ֵtNP9ԿO_mmj[mUՅ]]]]XUՅ]]]]\ֵtNyƿ|_mm@/mm@mUՅ]]]]XUՅ]]]]\ֵtNNً_mm/mmڨo4mUՅ]]]]XUՅ]]]]\ֵtNfJE_mmkm۠mmUՅ]]]]XUՅ]]]]\ֵtNRm~_2[mٯmkm@mmUՅ]]]]XUՅ]]]]\ֵtN@mu_mm/mm_mڵUՅ]]]]XUՅ]]]]\ֵtNu>ˀ_mm/mmmmUՅ]]]]XUՅ]]]]\ֵtNm@_mmͯmdmmUՅ]]]]XUՅ]]]]\ֵtNCmj"_m/mm mmUՅ]]]]XUՅ]]]]\ֵtNlmzH_mmmmUՅ]]]]XUՅ]]]]\ֵtNGm\#_m/mmmmUՅ]]]]XUՅ]]]]\ֵtN&οm_mm/mmmmUՅ]]]]XUՅ]]]]\ֵtN9mkN~_mmUՅ]]]]XUՅ]]]]\ֵtNsmn_omڗ/RmmUՅ]]]]XUՅ]]]]\ֵOggS'ț-]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]tN=mr_mmm+sUՅ]]]]XUՅ]]]]\ֵtN/mm_mm/mmmڵUՅ]]]]XUՅ]]]]\ֵtN_mm/mmzmmUՅ]]]]XUՅ]]]]\ֵtN)me*_ڴmM mmUՅ]]]]XUՅ]]]]\ֵtN#mg_mm@/mm7mUՅ]]]]XUՅ]]]]\ֵtN2Կm_oڗm@/mm]UՅ]]]]XUՅ]]]]\ֵtNԿmQ_m)m`mmUՅ]]]]XUՅ]]]]\ֵtN=d_mm/mmo[mUՅ]]]]XUՅ]]]]\ֵtN"mTc_mmQ@mmUՅ]]]]XUՅ]]]]\ֵtNbmx_m_G/m`mmUՅ]]]]XUՅ]]]]\ֵtNJms_mmHmm!տUՅ]]]]XUՅ]]]]\ֵtNm__mm/mmoڕUՅ]]]]XUՅ]]]]\ֵtNF][_m/mmvmUՅ]]]]XUՅ]]]]\ֵtNPmoj_m@/mmHUՅ]]]]XUՅ]]]]\ֵtNI o#_/mmCUՅ]]]]XUՅ]]]]\ֵtNzԿ^_/UՅ]]]]XUՅ]]]]\ֵtNmX_mm/mmߠmmUՅ]]]]XUՅ]]]]\ֵtN.mI_vm@/mm׿o7mUՅ]]]]XUՅ]]]]\ֵtN Y_mm@/mmޗ?UՅ]]]]XUՅ]]]]\ֵtNƿmH_mm}/mmmmUՅ]]]]XUՅ]]]]\ֵtN mx,_oo:/mӠmmUՅ]]]]XUՅ]]]]\ֵtNCmh_mmCZHmmUՅ]]]]XUՅ]]]]\ֵtN5ֶm_6m!/Ƿm͝UՅ]]]]XUՅ]]]]\ֵtN9T_mm@/mm mmUՅ]]]]XUՅ]]]]\ֵtNom@\%@/mm#oUՅ]]]]XUՅ]]]]\ֵtNjmn_mmׯmmѠmmUՅ]]]]XUՅ]]]]\ֵtN5m}_mQm mmUՅ]]]]XUՅ]]]]\ֵtN`_mm/mmmmUՅ]]]]XUՅ]]]]\ֵtNxI_mm!/momUՅ]]]]XUՅ]]]]\ֵtNW_mmٯmmmmUՅ]]]]XUՅ]]]]\ֵtNBԿO_o5@/mm֠mmUՅ]]]]XUՅ]]]]\ֵtNu6V_mm@/mm'mUՅ]]]]XUՅ]]]]\ֵtN#㏀_mm/mmmmUՅ]]]]XUՅ]]]]\ֵtNimR_mm:@mmUՅ]]]]XUՅ]]]]\ֵtN:VB_mۯp WmUՅ]]]]XUՅ]]]]\ֵtN@ֿ_m/mm,mUՅ]]]]XUՅ]]]]\ֵtN&6_Vm/mmܐUՅ]]]]XUՅ]]]]\ֵtNPXm\_j呯hUՅ]]]]XUՅ]]]]\ֵtNdοV_mm/mm`mmUՅ]]]]XUՅ]]]]\ֵtN_m H_/mm݀mmUՅ]]]]XUՅ]]]]\ֵtN|Os m|S_6m@/mmڗmmUՅ]]]]XUՅ]]]]\ֵtNPml_Q[mK`[mUՅ]]]]XUՅ]]]]\ֵtNm\_mm/mmmmUՅ]]]]XUՅ]]]]\ֵtNwmOʀ_mm/mm`mmUՅ]]]]XUՅ]]]]\ֵtNsmE_mm@/mmmUՅ]]]]XUՅ]]]]\ֵOggS0'm,-]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]tNmh_mm/ҶmmUՅ]]]]XUՅ]]]]\ֵtN>mz~_o@/mmUՅ]]]]XUՅ]]]]\ֵtND,_m/mlmmUՅ]]]]XUՅ]]]]\ֵtN%F#_/mmԀmmUՅ]]]]XUՅ]]]]\ֵtNSmI_/mmmUՅ]]]]XUՅ]]]]\ֵtNLm{_mmkm-@ mmUՅ]]]]XUՅ]]]]\ֵtNma_mm@/mmҗ[mmUՅ]]]]XUՅ]]]]\ֵtN,~܀_mm@/mmmmUՅ]]]]XUՅ]]]]\ֵtNcoƀ_mm/mmzomUՅ]]]]XUՅ]]]]\ֵtNmma_mm/mUՅ]]]]XUՅ]]]]\ֵtN8mp_mm/mmހmmUՅ]]]]XUՅ]]]]\ֵtN|mY#_mڵ1mmUՅ]]]]XUՅ]]]]\ֵtNgfZ_jbگmmUՅ]]]]XUՅ]]]]\ֵtNm~,_/mmmmUՅ]]]]XUՅ]]]]\ֵtNgS_mo*ȯmmUՅ]]]]XUՅ]]]]\ֵtNEmx_mmQ/խa?mUՅ]]]]XUՅ]]]]\ֵtN'mi_mm/mmܗmmUՅ]]]]XUՅ]]]]\ֵtNmO_mm/mm@mmUՅ]]]]XUՅ]]]]\ֵtNmO,_om/ZUՅ]]]]XUՅ]]]]\ֵtNXmu_mmmHooUՅ]]]]XUՅ]]]]\ֵtNO_m`mmUՅ]]]]XUՅ]]]]\ֵtNgO[_joA+mmUՅ]]]]XUՅ]]]]\ֵtN(ԿmH_mm@/mmmmUՅ]]]]XUՅ]]]]\ֵtN mT_멿m`mmUՅ]]]]XUՅ]]]]\ֵtNBm_mm@/mmmmUՅ]]]]XUՅ]]]]\ֵtN}GVmm/mmڨmvmUՅ]]]]XUՅ]]]]\ֵtNjmU_mmm`mmUՅ]]]]XUՅ]]]]\ֵtN"mh_mm/mmW[omUՅ]]]]XUՅ]]]]\ֵtNFm _mm/VmmUՅ]]]]XUՅ]]]]\ֵtNMX_mm/mmmmUՅ]]]]XUՅ]]]]\ֵtNrmu_mڵ/mmUՅ]]]]XUՅ]]]]\ֵtN)mx_mm/mmUՅ]]]]XUՅ]]]]\ֵtNmb_mm/Ӣm%UՅ]]]]XUՅ]]]]\ֵtNƿVƣ_/mmڀmmUՅ]]]]XUՅ]]]]\ֵtN|mc_om/moUՅ]]]]XUՅ]]]]\ֵtNJm_mmѯmmmUՅ]]]]XUՅ]]]]\ֵtNCƿcE_mmζommUՅ]]]]XUՅ]]]]\ֵtN.ms_m/mmڠmmUՅ]]]]XUՅ]]]]\ֵtNaܿ γ_oo5/mmԀmmUՅ]]]]XUՅ]]]]\ֵtNIm_mm/mm?mmUՅ]]]]XUՅ]]]]\ֵtN<m՞_mm)m mڏmUՅ]]]]XUՅ]]]]\ֵtNNX`S_mڴٯ-ٿ@mmUՅ]]]]XUՅ]]]]\ֵtNW_mQp mmUՅ]]]]XUՅ]]]]\ֵtNdF_mmf/ ڵUՅ]]]]XUՅ]]]]\ֵtN~mb_n/mm׀mmUՅ]]]]XUՅ]]]]\ֵOggSz'F]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]tNZmH_mMmkmmUՅ]]]]XUՅ]]]]\ֵtNucG[m/m܀mmUՅ]]]]XUՅ]]]]\ֵtNP6NӶmK_mmqf[jVڵUՅ]]]]XUՅ]]]]\ֵtNmf_mm/mm@mmUՅ]]]]XUՅ]]]]\ֵtNImw_mm/mm`mmUՅ]]]]XUՅ]]]]\ֵtNRm}_mm@/mmԠmmUՅ]]]]XUՅ]]]]\ֵtN6?F_mmmusicbrainz_albumartistid=e5c7b94f-e264-473c-bb0f-37c85d4d5c70date=2010-10-11tracknumber=4/118musicbrainz_trackid=e65fb332-0c1e-4172-85e0-59cd37e5669e*album=Belle and Sebastian Write About Lovereplaygain_album_gain=-8.14 dBlabelid=RTRADLP480title=I Want the World to Stopartist=Belle and Sebastian8musicbrainz_albumid=359a91e9-3bb3-4b60-a823-8aaa4bad1e36artistsort=Belle and Sebastianreplaygain_track_gain=-8.08 dBreplaygain_album_peak=1.0000aiffFORMAIFFaiffCOMM"@DaiffFLLRaiffSSNDɨ@İ %>Aлq)LK/J,MЂ"~-e mm)2i)J~" R"hDmutagen-1.22/tests/data/no-tags.3g20000644000175000017500000020535712146763412017306 0ustar lazkalazka00000000000000ftypkddikddi3g2a$uuidmvmlԁ'w5.z"AX*;!Q1Յ((xu9C AzFO6KF-T6Q|#g}V/99u}4~A 13ZzdmqN+ay mݠpoMDopB6HXRdbqu?zAs}:roL'0F~8u˭F޺fr D_&~%~VJ{z"O >),53 0Ā߯SgǟmR^Udy'Hۡ䈾ˋ(Kc-ǔhYx)+J~7b{:Y JYUDAXWM{Nx{zǶܰ.xQ{@sۨ }(kT=.1LZQ-kW) uE$@a &*DW,8 p* PX(6A0E/?iqݯxqgR٥iy鿒% Gm}jW4t+@ ӟ " Ӈ@z\ H 9P8quB{UQU/d$!1X.jg|X8L0D(g`JLF0Y)213SVm;i(&DM>.1`wC7:"Nf;vy=k~O.r5w+h K5SqrSbBޠi=u*π]6[/R(\#6H9RH4 `Q8/ ՇdvX-͖XBZ TLMi*]k Sokb1n)zߧ=XCُ~(xn4Bы=tr=k,wc&P^O,|y-^q'#cBDX_GR ><=B +#ʻ M.$pL HL' @$ BApPP`L"#ߎ+Rz++cj's^߱.~Ka<\'GW-V O>׺; {P)L-c=q@Ƅ+>'U~\{jWz4"~p~q(TD `H,P,$pH(`P0 B0UМSU7WzVdϝSoC-_$ ߷xQŗn|r‘tohgoןK|ڟku"KC: >aeapB4bhiCFWfAiV{bE@ "Hl( @, A@\$ ƒ0\$b;g?|Wu\[Ԛ}w:&.{ִ ?]ϕWk~/?g͵IEj^ҽ<|~K'o7*ft! i\IZ=q vS <& ,P@THL @T, @HL4 R^<ǝoWWƿ2og/z7yx# hI2"fPl>[cCj|q#~&ܰX2ݗ/idr7 4ey?w[PyurB$9 /@ @&p@8H$ AT( PT$ AA(PJ w6SP=\?C>3;/02@]"D+*kɁLw~Щ oj7CU5[%=18 a LH(% `H, @\, pP& D&C[^/on8<ޚ'k6.!F1i܇;gϛ#‡+ˆw ht4R<-4\rr6-!-ljFt`BKg ?S3-n;p^2$MO' 曤zPP (HWH&BA X0 `* X|'w3[|o7RO?^J,r9p;w>93i>_*ϡİauP|g=d ~x3 cNjl!% $k|oԀB2fTvB'ۥm]hH.:)|@D@& 0" @&H`Ba/Syu7hXN6収qٕ7u" UކP1"%F~]?gS/ڍ60^%yO.Kz%z`qRh,r]`"vz{%cZM>n=&I[:=/:ҾN=ԥ`b*`{(zƝrm:@t .T`Xk, &J,^+*RW!jp%K< /2yφѩ8{qnr-M(TIuMMa;﹛ Qny@,a|N,y~fWPPQ{">}@ @PT5\}P/PF UN ,`Rhbuysq&8M=RTc;@ ojVT.ߚ$R9FU 65 s)@F A@P, @P, pX$ Zvxu.gZIxs?Y}ʫƮH}XMYO~-}x/fRҹ(Č"*& P&pL(8  PJ  XH& @P";S־׵|=-f G|G')4T{yU\]zL~wPx>xYz+ 8ko|]ƽy#Fh$׌_ rM^iwUA,,&@(\pJ$ B(,$ ¢A0Pl$  Ux~kKZznU:ry.nfZOyyxJ<`rcm_jٌd0ӱݾ:`!lf)(c*%cpۆ`W;F *~3@P`@@JH&BP. PX00"xeם^3φ_̕k#s`@,%oWܭO@JԒW"l": fu>(v9);r/AmzaaGXX*)W럜ڭSxG gf~mK1b|B @& @ H$BA@TH&0. p\(koo8q_>2.Z?ӅCc[7܎<8.PkT^Q7uh7./gz!7]]Uh$b{k1I`M6<\,``J$ BPT$ H&@P* -}kW|=w^{:u^fk&[C.. o|. _͋؃⿽; ]#,KTqN{>J2 @w ~S^p*)ؖ ~/j"H@pH$ !(H(QT. L.)y߫\۽s57_9I]~npql< ˷t&U/;:Ew'l JY۫{9΀Ҳn41!|U>`K΀7 @@ @ 8JW$ "(P,2 B( ZS|oֹo̯ s8I5'׊H=SAxqѲFmUD:O +uUnCY=nwyfL? l-s`WZ4b;7&*@&*LDHnPR$MeD_N4n0VLT*/Fҿ:eRRDX'}῍+!}6tFxouvzT5qx|I8A- |x^N?p[%v?椑 k|pʕZ`XF4*Xw3ik$Ii_~#["kS%0եORL-̷- 6ȓaM|',ȽȺٺ~^uAD! ("b/-sٓ[~Ogĕ:#jp X4@6l6F PB&0Q1Rk5788 ܳP+Wȫz #_YJz9!5khR[ f.{?x ԉSi~Y{BohEOWMUڝ3N/9tvfkՅs@Ljg,O^^ÿInիW! ዠ$k`Xx?`[v?Hzc&h!#1C~ O_;%G: dP(`PH$D B0\$ A8H" BAEۺMwQonuw5^j&꣆l2+E9=-F߃Go#X&)sGb?n3"bZvp.n. :S\T/0(`Au<("dRx_P 0 J`G؅Nctl0זfd58e #@ሹz.3{(|ntOl!QEH!/!HT"$ȩ7(0)LP06qȀ,pNK',T81lַ?ŋYwON5 i{,8Z557O{w'@P%uήq܎DFkoTDmXK qv_UvpqDE D DpP '(ԑWu]k$#@tqO^sNZ5M3YV5sjy_@zR&}ɡ7ۧvN˱w,nӬj=|`I=Ԋ*H_{LrB@D@Jh20_w(]{,yy_VR]qYp)1j _8H':Ul7lo/ A eĘ/?kG.!5!*04~gP_/4z_Ƨ)RZW !Sx骶77aɯoI t(svI]%}9lX@lg, \TpN*  HL%  T( B ( XYk:{eqUS8ͯWOlJG|<2 /.e!MH;ZN @ X(p$.PJ  PBBPP, Q z^ksk_9㊯ЅV~]ڻlNeyV!x:>e7=@Ctg_iQQUƳ}3<Ē*-{C;SRzk]TޥB1 Q @XBp. FH BBPT,4 aP$ Pug{ԾKN9xoݎ65ȹx[<./o/زBQX.ʟ>wOTOmͤ>kJ@vw*]&;A%Z$ @ˀH$ a@PnBa!X( B@Eo^q8*}je?y:iɶ<(|yCʕ=Pfnя]ByO9驡ء̙+^*՟"9!{>y/f@.( * JWj*@P, PT,3 Z9ճ[MSuU xY|_rq] duÒ*)3E1i߉ :C-?Nw6oeoFBy.]BTiRgMN㋚̬?{:p PXJpTU㎵:Ǵ' >R5"*BR52nty(-fe>ziګcY 2@\0&<.}(Éwz0\:.f HSCH=ҧi"L$ 00HXT,#0.@T(E⒞\㙓zvyί*k/Qz ]~k%!oN~~!7hM/?>I"?{J ?!M~tira")IAK:`0@ @XHR( H" P( BA.w7p9)8$g{Al_ +}+׸v~9H!M%M L"XP8J( H(a T$$ 0X(%02otw;v-uq:]p9;Q4s.mYj]j!"RlenA|v}?QٸsFyKT3WI@iZ6Vw@` &NW(% PH( CPH, ba(H"U}yVKS1Ԟ} %ڼwa=- j?ROJ6q2ڇXyC#%}ܚ/ d&Jo=^|reքAu/J%qA)`L`L"D,T*qƅk3q:l ǺRp#]^>M^t[EmahaU%W;]Tk*Y%!!*b75JR;^uFv0-QCHจ LBPm6kJii'hjpE~d@ Z \l"@L DNPEB` H6pҒ Y=*kWۖмyb9BDQ)SH[7\`N)cu>&|u{di;;U1ڄDbi T B6#3U@lHBl H 4,PBdkYZ=q?, lԝ-ps׷]u2T_M5m]VBts/-gǗɱy+mg{βZ4߫ 4V08  `H(% A@H("  P$ DaPH( a5knx槏ߋ\N'y7澝WK}e.޳z -'(qTF'wgV96Yx5G\U>%j0zK3 XDX. J$  Pj QHH bPS]\qʪu%ɪv pǻGl z;,>ib.<6JCٍxoM{̭ s9%IJu_@K>1L  X J(r a\( DB@D$!Da/dOn>ٝn|ܮ1i5.ct6s{osףUwo_t#^ZHS?G[}lMa4/;r$$`B@ H$ B@P&!"B#0^߳zg_m$}'7/B}zy[k_6_]@^ 6ÄS˄v.(qݠ2(f" 3QfP@&LDFWL A$# !1L$_:kN>x?j93nqս {ӻݜ(ɢj28XMy8~{by hz;8_ ym6∈H4"&(F` H(5 A(H. PP" BAEr羉~^{wlԪR9;~ kyoWtƤ;u6}PqV*iq|eEXox.'d[mˁt..\@H$ PH* `X$ P$!Xssߍ5}_{"^_ưK3<:ȓAgB/E?kV(Z.:9Wq rCƸ)v }Bz>6n\< .`TpH(3 !(X PPD `N=s3~kuʃCׯ{j)Ucy9Q}z X$n=38jOƲj:9} N)|Tփÿw@?P" D@J$ APP,$@P*!H"u5Yǎx}q*E_]}xH9 ~|NϭqWʟo2֯}Jk.㶛hפgAXa ޭ!&}u>ZSuE^[HJ  . D8LWF !( B!(H""3*uCmnD]nZ$r>~-49uYx~kмZ|{m9yMP Z߅n&-L)CL4B eT. 6P J$*P4-S?,'E$q3"Zr"h.ܷPVܬ(9pp/xR/5)9SuLn?\SsiGJ{&D!J pB$jM eku`sTj %k8k "Di߳h> 'I8v#عa-tn$h3;M5*Q[5xgC=ajPi櫺;;}S.[n.s6̓=L"s08J )0TRȼǟYрĹnֲ5 MV݂5*$T~:yaLc^[8qc϶i9Ez~vUMպ ?_]LuZ Lrx8]$ ݹ<\A ( T@t 8P(XBhbUN9qMp?RkK}D| MqZC /5rٗ=Z5x[dMRC:%Bb/ŧNНCO(\{V3R7ǔ_YW^4cu5m=%4~/%aO;hXuT(F B0`(" A0P$ -f+>v^u%]T;|~ѾNyC47 W5rrct; `wXe=5-K| K[MGw{bM (@T0 8F( aA(J q H* B L"Ǭ*dd֕%q,99l}>iݯ-1gJ*K~|5MnɇǟfYX=<hzY˛ikNdnM)_ YI PN@ @*5" H( AX(&QH* Z羻UlN/*q/oʼ'|Ogr%2vYW)٠ǡC|65B>[xȮ*W=LV5h|_1:.LW/ J`*PTD FWh P`( P&*gZdUVW8M·i8}G^9__xn 6~JG|VzJ&vy8sU(O R٥;&A;D &\DX@.XFNYUj?ð4>ݘk[o }qm0Dl` ڙ9d΋:zR0jsXAjQ zǞu) G#ֻSʐ#D' ihhfd7y X|j65e $~6X^VpJM21*|qz29bcWtUo.YSquF&4DlOGTTq eeSYxZ ߇K mXis/}QW%wآWIO7g.XuMP]奮v 8@ż*.CVjBpJ D,QT^oZή[31ng,я |{5MzvgբK*yti+t<$5bf_hϒ>Ӿr 킡,~ >+ x@b3T9H$dwpqLRkJ( (F `H* DA0ۭn_9ur'N?=f6tnϚ uoIG,]KJWaEea̻bSSKN q[=xVQIAǩŻ~)`ZlcR߾WpQqw(M_xZ I:g: ZL=_. pg׎ }8 6:Bq qsa " + @ (J( a X0@X. P(*{~xS8>׎ώ;nqs^&'u,ǿ|U}'B'=T}23" i_=blwi)(Fgy7}0%N#3d ( Vani-\=[> A2 7xNG]YSi N:`=o !!Q2w`w)UiѤ?5J+@(J( `$& `P,1ZVQw. RS=}z|MHП5t#W=*_5 ѓ ;]]EՒeru"Cܘ%`@p  @`FW( `, @Pl -qǷ|UL5ǫdrknvܷw}σ ٚp^eiF}y?,[Uԛit ^G&v#zP@4S'O}b*^R7YW)\`h pP 0D(,ЩE:޸޸4Ȉ@ai o.agX]*}<.=X}+jԺ텁5g>5ҡMFUX\Md t^Zu]7q_i[>؉y.e[3> E[B 9dLrL (- k*{#CTԸ zw{b6:8 ԝMHfb)P2.{{Hә}\ (\,0eǬ@!AaHF DHXPBpP( ( BQ . Uλ뛿/q&֪u KA;vz" " 0@  8B,T B`Pj BP,'  D& TTˬ雵E#W&4r}{4ImST8?[o¹+~*`Sy،>LIa7K="X+5D,@Pl A0PL Ca $ 0L"U.sz8njU_h AZ&3q\Jt{ (Rugc+=][?ԟD6;i56gwױi]:}7{O:9in`LD , " @ D**`PL$ A@L" A\]~rssz_j No&|{PVlp5;s]u{;"!Z>l;+c6ބ3B$!:d;٦票_/b{~@@Jh `(( Ap& B._2):zs3S/|,ָR49=(M:Fy(sqptφ%l3a _m,CMku𰭞ȲobxL^; h#57@`@@ "`D(Q0`* !AT(& C1W\ۻ<]V嬖9."~w$t9'B5./Ƃi9p=WpYUi{|)S{muhD7NdaQ6+l=j\aIP\ pF("BBHn 0P$ A:zoߦ_5 ,xE=~BԛC1Gm*ҍD_YumGn[V,D>A#J !j-$TMhP` FWL Bd(BQ "3˪~껻L+W>>>CϢ/=y^a&=.r}K?v]3!sAl䅫 ԤJ&|*Tjuٷus.F && ܘ&8 (,\2R-ud"}b7.*3~^zSOCUx??~"].͚:d <ѧD"j#+_En-z4]n,Gјgfxh`|Jt@w ͛& HD,BhbEԹ5)ou<\K*&葪;܌.[~q:$Q?MǐyNGz4X7ߤ=G: F ,ɺxsR궟{LY SQK+G'4 nsM87 F3 &^s X\ '1B(PX( @`$ @(jz^^yȥw^k^¥q?;>'%UvPpsd9Q<>Fuy|l]Rn UA&B-IלZUsGzNƋplkIw "@XXLB$ PP 0H Ba8\}[zq~#uf{ =A @hL` *B( bT( @(  P*X;Uonֿ%]p<\ ﷕&co ^Eɭ={ ߙ<~:T?/q-x5/pMv\9}|" ټT'qBγ 8\` Pb@@W(&a@$ A0P~U^uY*˸NYgߺ?:QvSf;a#a 6gD}n߉{Ө:. @@(86FJU׷gUUCS'p?05Z4,4 kͫtTktVY.3<|UJ蜫|SiL;H )SN"Z}2qgSj^7_cok7^kYPOXjTl`]pP$E1J$$ `T( a(2 ,p&^޷S&vݟbo?EӇ=|(wofa8{KHuʹ΍Gםw^M~s۫:Kl&_7/<$?# <( :3,+P. @pDHF"AP,$ BBnߏ髟]{2U.Mf58=?6u;?6-X|hE}<>U`egq9{ Lw7ʉt?^yvj)aɿ<2UTzO(W: dQ9Eo@@ @ ZFW$  aL"׶I^V[\{C6| ?-ɮ.?+[~=ԠykC#Zݫ? kFŪΨrj$!7밗CnF?*@6 (,*FFPQ 5"r؈8OL,TUpShm{D$0"Epű=1Η62Q^4-tz퉂A'$rk~ .,!:֮0wp?Al9C){(u?~;*@u7:@o.T+!P,8 B,$ 0P, AE-yn\ɾƷuWުkN+p6GK>w_w]Õi<k%_v _xT;tuÃ*1e{e]!wޫ~] EI!&uȖj,c!.gB1a $D F( `jA0J$B09rZe:]\ˁǫw~u|iMGo+.kyt^jnCHa^6#[nb.90OZaǜAg;ZΆ7/w@/9C"t "-0`H NW,T`H a ,X5[tboYi.y&G/;WU};xBkJ ]FdzHwT9[~S/Qm $]7Uմ{69W#9!X^ L`@ pD&0X j|yk 0;_7eU]LBuSV3t4A5%VX97y3 txK]쏾#dy f#96"D,8N,T`T( AP$ *g=_wW³ZLvk&8,v%Ҿg~'_&X t/PS>jܯ9rޞ 6标t.\H8#&Mw`oe}[\sW}'==wzg8ofyDȄXXB\D&" ဠX$ X. @Tb|׷vZJқ'%9_.:L@Ѫ<Ҫ_? F_57k~Ͻ̾=ʠ~e{uE9P/D|CSH~s1;M-YqFcgɚZIIX:l@L*J& @P,$ PJ PP"îe\o.LUy}-|8KB?&>,}3B؍:b=HwVSƠ9s*zÔ"\ki%tst(}z# Jl.0QKH*@ RW(8 @( B#~YMֲd/Iw:o'5G_r۫~fX(?}i&38z*꣺K ˻.:ĴK,^ĿxAȐs UZ&rŀ (H &L*_t> ˧m"t2h5GR>\)V M +|G)}g9$ml&A1~b^Ift$TRr`"S$pJJ}t?̢/:vIif Ga-97;gR<ʅ _eE9ZFjUW` *T8DLT`,PP,,~WLu[3dq֐r>v?]Wvr?_| 6׿k2J.W~wWз}D~\ѫ/0W֝lo-T&N1*X| A8@` J& `X( AA@X( ! ׿v煮MfurKhv}wos.onLJ,2ķ7^ /B IRmY?6=A*?c}VH]Y(%֖w~ `J* `0 `H B0H"Ǎz]Ĩ߮㛫ky7U| ^6;Ϻ[Bąf%IW; zxv>>r庯4Tga%vHIDMƹh$m8ɍAc(;N `Vv\@ \FL B X0 APL3BAsw_⯞VUwz|x)G3{ݗ砏t:H=}T-$rUZ,`.?KҾč|cXgqܯrw:p]G}s)zn@вTp\`T\@@FJ`XP$ Bp(% @EK2ٹnGY>_%LbKùsYv#;E׺9jy ߣ7.7z [%5v?ͱ*tA 0Iԓ>[MPL&.1H( PX0 a X0  [d+R]~rxGMZ96h `{P+DϺz7TNzSS#wyz˰ͪBs#/-qAc5N@OB vpmw"3NI: tX"P\ pJW(AX("`(6 A@Eێ7xeIULy^8_' Oϟ|q0(y&>dGup5 LuK,%Ety:3+?M eL !8#*>;6_X`]1|ug@BZ'Oo =}}O0k:*sr'])q Rҽg P6nKh)Wݬd-pȀP%$,L,' @X( q0PN B-gmnqj8SI8J#O ^oKAϮ/}%_oklcĺO\-{)h0`IY}S_8 ;cB6^3Q9]` !  0&J, `. @X( `"{jS/Rj{*49/~=i.K~?-C2uq~[m E$aT?:#kRu]Bem˥;4(q \#5ͯ?h.ÁRz‹MLuXH(pT, aT,)Fa;굾*enf&4w*T9{?z9=FDzq?|Y-=c$%κO8xE^?#N_8p#U2Oː_/$K7(\ `D@8HL `P* a(P, @(1knT;2Z՚.uQ%Ge?;g?u>~!=ڸ8~GC:8=>E>(bjeb^ew#AZrQпs^9-'suV"GT!@(*&$8JWJ1€P, `*_:ޓ)rdZEΧ>IY~ΦܨI G6|\}h4Fz>7WIl $Iz"e_ 棇_6Jp/:6XQnH\rdjLOШ+% @€.P@*0Y20"7|=W['>}=zucJTBxfIKKSlQJa/LyCoͶ(ƹC$ՅWLCšk {0Ez19@X[c.ͼözƅq. h D#" X `kJH `* b!$ VoD2Iͭ#8'_oողaw:kɼy1I=}GM4g> ѐ/8'q?_7_ۚs! ?fݙ45)}d 7\aNa $ @EPD$ Be X( B@P$=|R \2uǷ=ko]g|rWDײuK0 1Ӎؾmirr#;%|nx&C Iif52вd-$sY?߅(P0 ( H(& ),$* Au5TѻArO>T;n_LzWFj}x%dԻ OZF5\c Hfݡ$w?w'XW-sݰ1e1]\MŸߢ 1B@.@,*H$  PD9 APT"k[ըELFk]eg$o,9??r ͞RW_"upRTmI"Iœjf9K/^G7{\qqm c0C㥨9N[;Xbš1Π(`@+D&F&U\dbː< x`.4$+E'n(0 zpF1`^L!F=Ca:(*JCQs[k 'M2'~xpQuQf^BjCDZ:`^yC[׶Ycwe.P~m.P(9J( AH(&2`X( EN>SX˙V_SU4;\.߮r_#YTٿ=DGǃyKZ{7 [*/;pm B*5'!J땏aZskǨY6/>Z2V_ȴ, \U..J$ `(&$`h a5[Y^wʓrK~|7Rj;#[\9o0_Yv`'./SJ9I2Sev<[pgoo*phÈ4QW M5kST5^F5qY2_uhv|<;[o'o`3O0%²p׌Bk'I(j^ȉHtz Q)"k:Dz^b JM!I|!PAp &@8H&.^ N:`,iSu$ V(Wܚ3=Ad Lu0<SEQ \sh롢+:7dƌCT2 ~yH1`t@'o^\7=::A[l|T ',7ײREapDMKޥϯXj `Zr4.evGX&5 b+1d;GŪ\%*fq>Qlk9nbB% rSxIp#d5Et`CEgD/i9f٘pH.@P\IW.$ ~$YX\KN0㚖zj1ЩsqMVX{HÚ :lwN{KsT@T.@XJJ$BA1(& BBPH~&Z*M/9Z׷_ZG_:u}㱩+g.5z'oop2[㬺N7gl16Ӛ"hv>/ ǘ50(x@ <5 C@L0@@@p.JN&`X. @H( Ba=sqw5Zוkv:)\V#mV`GMqc;vKj[Mwvzx-||Gt%cw:>v]=N+xgx֨J~c{ -nh8?x2配p;&Zs1V^/dAV_} 0 pPpJL4@Xp Q T." #0\MV㛽M~?Y#NcϟT~ Flz>N|Q|CPOzh*)z)5]OlI޾o׊/m46q"!GP\11y_]jUe ȭh P@PW( `X.AP$ ,q}ޭ5ujq6 -7oV5:AF®}RHU :Jnn\C'vO>Z#?sĎ'A:N(QQ2(nT/f"(@@`B+((TK} SCmLtFכ %ӼR A9Dҩ&EFZ&)'a.H"D=]lOH[HGjaĄj//VZFҭkc@F+t#4L#Iv0zLGtQE N`^ s\F(&"ph' B@H"[Uy5W2Juq?nԾjnCmh[@%NW'rl[NG:Z7HN(?C}W fm.hU2gl΢Ywv٭zkܪ {Un*DʋHn *.*8J$ AP,% PL$AB\~uny{U)'Vrx>:y'?~)ۿCkF⟲0CU}T0sSQӆ1OEn2?B77cDV^7jq6&El,U’M 0@ H( XP a, aPHHU_o7t8J~1(>ryp4NiDi!of &O,cj!C|/#Jn"Njo qE`׌)h;i 7{ L{-LQ2t@* H,‚a@Pl `*!A733k$ܒqN|G>|Ρ6sL D;E~Ch;yvG՝5_j XxZ)ϙ,uY-,ֶbE|‭fH,T  LLUP*‚ T(" ZY2(j.R:NKuۣ~u59ԁMjxzy,M~r%xՐx>l7?@*U!<@2VD ,$ F@!`FWL4+B0,$! A:UƫƵ"U䚑:{O>+^[ﵢRv׶^gjV K~uN?|kOJx+}oʰ4;yt cG$6D^WqI!0*\*p@021SYs^WHI";>/WaW{/]똲az@%qMxՆ@KXB(dW$O^=J[ {)[A;ٓz.RLQ58ck@~R[b&V>h7sHxh "8R,D ဠH& a . APT"=ⳎWZ%ξԑk}ٻv?jnM$w;W:CȜ]/s!q.%PTDTfsf+eC7 B1y}XQ&.@ H J,c pX* Q $ X=)WΫx<^6>mwbKCEz"/y>M"a|{!#* SqJn=fb ȌWNG9s9rJCiRuZz`@ TTHL ¡APF$ a X*P"ymWzU=o\Wipr;7LG-q߻K)5;6sY>bNUvϔ>2yLW {a0nBpO}%Q/ξ] }@ !0 *@@pJW$ `XH ¡A8P,$#b7»󕗾'N?erw.Vݿ'vwFߎonUJu`f.kt7?qU@udԚhYc P XTP B60XrDKcw?nojf/0 R!i1я ׏F|}%n4b\4KcJ8KՅ-R%<cFckudٜcOAmI *QZPP6D\fev8D( P\0 A,PE}RuRw/}U]Ln?Qr4= ko;>ѓ_yw^޶rm[t 7(-oF[q>> 0!˸U%VPv+/ N'3VMt3ʑX)YSV."@TH BW( aA`(APd B-kyRofu^~*Dյ)'wN~BV|?/O/o&u!\k]}qzu3٢s5)cYKxԟǖ2ol‽T#Mn@g TP.P`8@PXRdaWZo^a?/vavZÖ}$GE-)1YG+ ܨE_ۮxdMp(lzGmB?s4t"}Vp?tʯWКt_>cۉah'V*Ĥ"C@ 028T(Aa!X( `P$ EMnx;Ƕoxԫ3j#ߍsE `_wlO37/`ܬM<n)@MttJVOk_iޢHsuѾUΒ| -: _:@r@`TXpJ(6 @ . DPH( En\uYu1Jn<ux?)ĵLbBbv{kU+芎!CbY͖gխC'wVAo،d2k$I(T@ F(pPJ@P, PL"׷׫y^MLLuur^7 Ά;T_ _?}"hmsw _˽wH4-%`# 7Ո :$xRœs" @  D.JLD pPl A0P*) XuNnfqUjyV-tYt\e 8^iwW-+ժeNwW+k(tt]B;ѻ֠DpDzx=BQȠ.<`pPT*&8J* a `*`H(% aEKYRk$*)牕;_~*ͻS֏$/HKy ~:-*U̎~y+?+f 2rKD;;DmrV׿+!:^n,JpkB pLX("  JL B0X0 a X(& A@yvTʶqA-[; :GiM z+^uӿ?._w[_Dߜz:0/5ۙ]gqaO\-”3>,+f[a"  * \8L()a@T0`XHB @E|] jw}骮'q^v[_?ge+RS*woKWclVWcӰEv'G suid pNnTTz-Ǝ2FsP ,p\@PJL `T(s  P*\|N5f'ڪW_Mǵ] m>>kh5qYH:φua;;ѩgZ^#u' Y'~vxVbџħB־"&* JLd0,)B50*dSԫj.jk-$XyFv۵_QƮg껳$O?%.9W:ÛA'ȧH= l?")561SW@*{ɽWOBcoZ#!h @ P.pP (,ܩ"55ֽt|W~QNrz̳LA~Ʊ@:~aEJ2o.sʍ#3Vߍ6FJȭXKߛ%n mHqo+D8⯢40@L˙0M}@>O8L(-21j^N tY('Y?752c`!Y cWsYY+fv<`vNtlCܪ-L+@" Z UJ&0P p, !1 ^k4w$k&q2UqF&Ǘrd0G}k3y< %wpl}rN=VW7)4ScW3wKrqPQ((  FLaAP* P`* `ϖqܒe+]*U^7=>a^uo8Na=_2X4F>%lڲ<D!MZLd2Kj^BuˀaD`J* A8P, B`\(^nN7y㎿Lbw'^x'Tj0_NH? v>K;6M7van hlMeg׉ڶwHFu5&'?rC@I&0p 8H** L(&APT$1 X5^JfZy~%ڦbjo?мV@oվM+ o a&tdGEyrAo9Ya >(JԬ:<:E΂TjJHHL$ B(PN AT$3!׿k9MŅirI49'UEO{Ki@*u\Jm54At"9Toɹr+bU*x|aoVSZ~ZjlBgqw鸡Ѭzr_ @@HH",X`H, AH* C@ "־sk)5Ԃ8hF>}yoi¸+$~5˹/.;pDO]I Jzq\]q^郮/{(pPޙ݌ \W Hc NmYBk t19q&oki `` X J&0TI_+o}BcތȰl\0Be$ܚ(x:)IGO+'Fxܣ95{dhM˓|kĠ 9\VV{l;ܷ]7} ki_f_Fyh,h @!" JD,&a8}; er{ODyY/u^ꚪU IOG~ qjze\hHo9@J%M4'Mߛ\ZײZGAѲ A F$`hJ( aA(P* A\, QP$k̽s=p9}˩WMoWwH'G7.8p}?DK :Ok76F6_]K.TzatΣޝ豇&``@hk.BҴ*"T (`@@8JH& A@PnPP,! ZfLVzkU%8X4984i\ \ja5'(e';;7칋t9w>"'IsF"xҵt&/詐Ks^o2+@8B@` @J* A0P`J ~׬9ƒIӧ|{ Ew7{nhVmn܎G#"U``y}${f8Iлɼ-j ~u )a84S"4@( p @H(& B`( @Pl AE|s j$TXy{`ֻV6✏_9 m}~gH惊m%3/ʖ;l~ѝpkz۾N*;~;6pm5-L{,8.AQ0" P *H&h 84} 4 g-2ٓ>7e] Pn SiYHjdox-Np~^)[zhLȯ ܼo#rkQ^y5\6so35һhľC&TPH sj ;&DL( BAX0 @T( B@D&kzyۙ9VMe]>KhrxWC/}zx,jmNq; 79>~ >{g~>IR}+[S })1|6{->/5Ң.DbLpP(8NW, a(XH2 H&ͥ<"JK*k'n.\mJ{or'7,u?gz}qxY}\.,BO!zA5}q{~S-z/Dc$STN*p@HN -TT!N*UUswX j*!YO{j8.Բ xMt2| `". R + #љ h%MF{i^Qj@YR$K盫t} < Le$LE5+&ݔ⑹+TrF$zs෱WQ xRׂ t^|?.(-Pg½@r*%yKg%G3 @7Ⱦڝ*1  6 DN`L(*!a T,$ HB5XYuZL9-Hī7rrzzͳVӟIrOퟺU~Nz+^%|)}#ӳʿ!{^=S--<}'}"^%l$ 60S4g e@J( @X(6aTH2 \dֲR]Quc;W/8?%[R|lAuڕю}kw΄T|8<|:h1T|d8CMSm)[e  *B`J(BA0* A`XH @T$s]~-.rj?ZO)௃D_}zڃN>'촶1nݑ YOrZ.>7{]E{gwt$a*aP@@8J$ @Pl p,"A7J_|4RUҪ49>?{xNOo Ah[.תm|7*t1ټ]0';ݻZ&ؙ)$&  P"@T.pH(' PL* PX(B BAʍw2 w<7W%WDjlN<=zWѠ7ӛv3O9=+/'<26t˟F }j{6'ϩ HI.?0 P@J(6 bXH6 Hj T헾z_UIyJ$'ݺ57 .:t#ؼS/ۂ(Mo: +su3 y{:=_CEeEsA7;EHFi[{Qpp @LJ, aC0P* P"3]wK-YV^~t}Tݰ=Ӆu~_/|qnkG#e?;{}UJ%WxJg!WuPw;o"M8j0j=%p bzǸ Q`3TP 8J( a XH& `X(  H( X抺s%]\o\i"䫩w{yW!פÈ a i|b%B%(sK[Emw>Ptp65w=7ٍ.JD)3F#r l.LW(@L$`(  w;]KQvu0UI}u_NCWi>߆s-M eߎMu0Hfd޶q @=vE?`o2"u&j]KAӑ?W@.\ @hLTf.Z_]1f]iOzky%]LotI}^|ö6ӺZwكR W=afW7C E[3ts).|iҮ7;zsi( F`\ zH$ `X0 a@T,A@Ԕ]qTOn?Ӫ76iO;#~%^xĶb?nȤ9A>vev?wIHx3~W>:K^I{\.be h, P@D@DpP(& 0 9C3u4/ZsgIW_eXOxqCv"Z&" u1?b`}Joh'gm wo}eSh #5G:Vz7GM ~u |@,(uŀF( 0Pl( A0Pd ,zVy%$tSo9? o;5@MtYT~1*3y _&H{@XHH\H(P,T ET"2" kԒqٗ/nu_<$C^VZH#$MmoImUF:Ro:aܵ72|7U0#}D K@LoSxr " `*(JL( AT( B (%[s.TSy@/ےIcoK.];Cc:H!DSq5sQ"H//WgBl8b*qkut P D .LL B0PL! C7Yy&eJV8/_nN}5m=߲?ſxZ"/G/e_f>N3m̓7u7zȽY B᪗|K?m2t}BOMK]>5U-֩;wќN{ƪߗ}ʨ> ȡGF%0e->oGhW -ϔ"D0 .L**8P(M iQyk] Jr-.A(WF)f+/,zjhEnD@7c);ϑew$!KυZg{W .^3G%rC 'p8paحF'CV-D D@u .5. HLDB0T( @T0 AA _;}m*wySoZ_wSBھ'aOE{gá?Mf;Hzʜ\wP#tv[9ߐZ)|q` `Oy7fOݔE^h뚐Ԍ` l-T@J,$ @X( AX(% BAH"{S2U[%"^}~*Vq~Q~QcN@S;8M꿀=5-{K2AƳB~ЄL՜gҲm'٫Z͞kFX##RAPH@@T0J&)`( P(BEFֽ޶RK{}\.Ҿ:]COvߡuxaj֜g_fCEf<>>AD`d_#UBK%jX+jWGǶZE5L2Fkh=ȀJ(*aX(& Q L. -z/ M"/+U5}{ RAɰuާ˟x?O?_ӏt.݅w_fϥp#;]&Qώ'=D79wDWl&w4;%ϕLbsI5d4,~  (p@pJWL3 @P,3 P(".̺Le{FCKsӏeF3|k?2Rw}k1jo#_,5$oPa/\kĐgg(C*ETvHlܛG:5 4(ޖc(,< *$\ J(,21j^ۋzO*=K]cJq_z[HYR zD9Z)6{ gի]BB>Ԏ#HuPAQy]]P%;+*7V.d5떓pӬC;Fk@8 +  +@jPpT,c`T(& (# X<1k7jKz\K?^IϞ~t SqVG/3(NOp4Wڽ ]яn ebfB׏UxqWEֵa[ٿIHםSr(iYe&.L`PpH,$ @n PH* ¡A(L"I:ܣwl\ǟt:{ y oQuߡq3szԩ?%^#k?@F~Z]7U!BGx~V%c,PZVȿ7l-{w#`i%0* \@J,H `T(7 ‚ X$ վ<5"|JJVo ~Kh8.yAE5\aӀ^&Eń*ߤ靹tg`[ZϫMnR8PH@(.*8J,e @\(" AB(L$1!PT̽Y(T:y~.rsMktzB8>b1s>i);$ml 'x-xnwQ̃\rQ&u'JfW;&eLP<@%@B,L, pX(`P,A@EtKJ]e&jeC?O7ukE;/=b[JB[huS@zKEڝ aИߧ8cE<&z6=Xyp{^1トђejߡ [.mryiqPsD \"@H( @,4 `X(rVs^&^]^Tip9?9~u=~N}՞A|-MLz/OWE\ڐ Az.p[?9'yH'.ъʺFn\ V0P|@@*p@0H,4 A0* `X*A=LqT󗺙kܼT˥ߟ?QcU7y·+ӧ{r>^ ;=gOWn$o#<x͠_2p7ʢ ߩq&{ \!*@-- ! P 08J,d @P4 An|nkRRKyc9uM[tt[}Y`#lSV.X%GBCp(wvFFsw:ߧ*E-uw,LRxïy7V E`  `J,H @PL AP׭~x<$*&o7ԯKdߟ_sR?SMQK%$ +f[< ͣogj.WY/n:l6M͋0L,wD`(pJ, a!T,JAH"ϟZngU]dk+Ƿ"%+z_n< kÁ o79/z$c{0nbm! ݂)ɘɾu-m=5g@ŧ|fZf`QH @p`(pLW, PH6 QPBljwzLdW e_DYk'~4~i՟"֎|M?7z u{e{/ZR0rOO߷XK=%FQ PknM =oҧxehċzAP@b@T L0XdHĔzkemI-9 Aj?W|gtbeWe=h+8wx,3X{uyQVԢjz@TDgZOvPPBႲf/=#׎W<1`PaN;ތ um XP-";d0A9羀zD^R,A0P, APT(&  HBǻx pSTZM,~ߪ|uԿetOA_ס+3}{czx]񑝊`sp|aRL둠7EJ<~6++r1>B)G0I6@X@pH&@l `h AEO5lj*/d"Vjh,}C'sEB_([?_kw?̸O➏@A  ;k܄A6O)7~|aqO'k6yz5nX!I(6^|  .P8D,G †b (& Q L(V龳[ޗSTKj8 ʥٛW4m}}v'eh~]+dۯ}'%ҼkTf>`ξ/ gֵ׍!P>W][ž9W隹W|yVJfc7S h1/@"P@J&*aa ب `*kN*$RUqTZ'noqwO:ѿ Ǖ~7wOWĊ-DqrϯoAiGĚX{dH v7RD6Ƶ/lP- b`  XH\J,( aPP, B`Pl8*{沦k]‹UI(,r[ҿ$ ]FN<'FLEP豒ՇsQD릀Oc~}!~{c` Z|N^CV!'}4|nt^JY뤺X+% L*PZ`@J,( @( `H aA&P\o|MɐWyQӏ.eKG7 >hO'C~Iѧ_cP7z?G~ߺ3HӋewse -~nQ>_nt|-m2Uv \b `J& `P, `X(5 U8Zqۍ*[5,rLhsd[?fqҚ5p/wYӷ!c쾰 WdLO>5*m%tDځ ~) iZdN>+@$` $ pH&* `H(Ba>[}R)tk3Y/_Xy4pr_f{qK PY~W *7T/v.0$pL,T ,% P$~:qܺ^$UY5<J1>%:W*i*\wJ%:{f{J8etMw)zUϡ̉Iެw&jץH7qIw ͍WY-Q}qHL\D &F( @P,% Q0P,% !BaE/Sȩ"J=ʪVCGgi\II<'⾭jo8[î?Xܾٙ(6[;]6Vr_ñ;#4`܎Z#/vf1ش/4Em+tUM/ZP٩PJ3`P\PPLT P,AA0PD![UMomy2ZB矯#5>>>Kܟt|_҃5eQyp xY _Չ1d$>CsȏyȀH )fB){pFɔ-]đw}e;oH.T@ HLJ,( BBa 1C@Eg&]K⥸~@I+[qt>;eM会!q{BQ݃z|[!H{iPEkPֆ(҆EÓ \jNj[)2LU P&8T,t @Pl!rμ]uS#YRҪH⽾?uC컧׫vw[HzM(._9_@5K9˿c7QIm R3+2]mI󄮱t"CYot䈾_  @ *B@R,$ PXJ pPj ! L"qjS[. V]|6u79y1gR>Qz`kc\myW6!v$ n%fVn \o9`7ǁh7%.MM^uۂ* J,(  @JAAE~=}Wkq-S{Ԙ>v|<_wi:xn[ ^V~'Q_l[j_cPP3J [wj_S~vA|ۯ@D,#P*@PL$J,( p`, ‚`H2 L"=w|GrsZLt?慬v7ӓӥ5/X]bosd]ôy 8FX^gZ ־[Fnz65#Gx>J ?랼\ؤ4-sO9@ Bp L@J& @PL DP$c^Vqs{ڬe\]LJOEǏu.i~n=Wf?c?]L?Qn>Řrv> Ρ#Ou˟q~^7v=2Y\֠tlHܺzБ$/ED΀ *@ HW, `Xh H(% !0gqzʄTzw|XH[>4{Af.Ţ*_'Doo.n'~p,h-J^N?O `fwxs$%~~u1cQP)@ @`(L&Hhؑ21\ I~D/2>$^x Er5v`8|2κ?wj.$¾LO؞UU*x}7? qfdPkU]0`d"HlGO2fw dif( 7o8L (La"21%zƸھRYNǶ=o|Ʊy{_s\(#[wg{|X"ԚJ|kx|駺CU1VRZ6C3pSNW K, $=JK^Z]+RearAEۗh5uP\ dH`@  pL, B0l `\* ,sxX\-.uJc')ҾsCUk~g#_dթGߺ7-WC1=4,0Zh#|!1o 珜|f ,P J( BcX(" p\*2 HaW<_4\n]$^.XS75;鲴mN q=?&V_g6yd_I&nŜuk;c:JD @\DJ,D  ( @T(1)N}x^M^4g?~[s6--H~O/op+!U<q?5t^_JWd;Քe1y6*:CgbYa}Rnr[:mW4yJUX]=-35e:`0 ,L J&c h  H&S=rUFy*eHyoo/jfy|tm_1rUz!Wk mGY(mlbF=5ߵȳ6m::][./< 'Ük.r%fv}곭='@3}0zCb}Dj<ҋT8(0]ֲjZSs?6 hH́$ @o^J& B0l @X* ,zf]q1)y*ƴI/CMg+9{1N ψŨyc`]\Q:8$)܋R̾1w>81}.,[;~v_zGr;'oe?X%I."@`J(*a ( @PJ BC@W[㚹Zqsp9;y_n٦t/qgwA*c KqCz]iWKrfnPV=FT{7@?tN[>第׶l 0@ DP 8J,E A0P,@T( Fa@P"m9q Z[$?z\CvW?/?׵q~i~ίx~'uԌ {j]/ 8^ܝ{vPԱbfhɏvB$  *0@@ J,$ @,8 pX(!r[ʚ%%]͗Vӯ]{~U]/k^i^I4aKYH MZ-͇}y>.hSS1ұ):FJ[dS3u)U>mZ[k\V&".08H& a \, b`X( D&S5Ǎ1-fKqjI.O)ydO\J:6n#~|wxg-ZL/$ï R]Ѡ1^8 X Y~SQFk2ؚ |V:(@.PP 0 R @LW,T A1LDPL"w(kTEBL2^ \#{(/==+y'n_+kZ9$$F~ m¯w"Ô~}&s9EpL( `XH& H( AA">f5)u&.Uͯ_ϞZF'[=^h]{qۣ7+|< [Պnm؉W_S|&9n: W,vߥM>,BN.G"Icp`T @JH`H*  P$ BCP\2Baޓw_&oy̎yq޿u uN6Λ}v$c:psW.LGw˓oeWFg-C3M|tjZ'|l*::,]-R`A1  f!@T(HLBa (& F@,%D)k5L&ƿo|7ݷ3;Gg,v| yB_GFGX>mQd ު8oٻo)?жbE;M5RX@PP JW$ Bb8X( APD *7S֤U^yμ>櫥jO Ɨ޳J^=OPƽvD/k_{йnm0vxx+v|UJ P"PmoofmfhdtraftfhdtrunmdatfreemdatFh\T13U8ֺ~%=<\Mi!|Ǻp:HȈg" CY_XؕYmQ N̅ ,${ޥeXˋz'qҨQ .aʤ|&G<6P׬7 sX5jL$,Q:2cHk|g39cbFi]Y)yeU@TS.:gD"4Ѧxz~duC|6;+֮j o_)Fմ7f]%0еYOV@D&XHL$C@XJ DPTNBaD&C}]޷9u^5-Ypv> \~6՞ʳnELn_l-wHܯʥ,Xdd߭QVBPVmqX./a.qϧ]q0  `L,( AA* X(' BP(:7s}s$ݳzq>3cɍû峣 m>CIhIFQA<,>OHTa'oj:~ {ۂmvK=F yz Ffmk rp@RHn`H& A(. #0=DsU^nnj_9Uܿ_qϧ gxzଜ!IˠWi /~>ʱ _ 'OgZќoQ:Rt.;`_biԷ^sL>EUp("@.$pHLD A(X(3 @\*0"UTwsסe_5ۛX<=<ǛXf:O_3~=P\>>5.`g LoTuՆe ~_ZRqUL]>@m ]ҡ  &L JLD#B`PL @ !1 Y.wml&I|y%԰t[[hˣmcwWygWGy˭YBHfi?A`Q<r3?ӡra[Nqq`h (`& H(H& PT, pT& P{}k|ߟ޳5Zjrx썼Shظ YaAPu=Op;Vxn/#6E,א=e^¬$5d',o˱{ՅG ~rXHوJAc@  H@J$  XD A, q(bxYd㙙uůr +C_QοoH/v=B}׹>뚇1bĝGe8=|e?/vlC"~+,L-s?S?xU~>RZ&}M(éW)*8w=} M,wQj_pp@XL$ !H( 0. BAP& B1 ~*~}7:殼ߝzGxMӌR m힐ړg_G0Fg=Op Iߤ~`\|j| ?SnSJGנz $*@*@\J( CQX* D \$ d$ BbP|㊞5g|Zm~?a~qǴ|g_O4r-#'G^LU#/'[ ˥f{QI[W! ->-oh+LgQws|` LWL @d  T$PBڼߏ|ݒǼsܹKyq׃kѢ穚G]em}IԪ{}{8!P [v͘8ߓс*[@%/p ZI/8L TL(Y)bԗY?z ,0CldT HFJtR +_:_}3x'y ԊʥxR̽L j7b'!oaPVi\:'՛tNkU$gnV5Ώ1g $ M0\*Z}pF$ \%߷.Ht!1;]radTHk;f:oAs+mkԿ߇Ϻ<;kzn]Dv㍫)riۻ%UGUJAL, j8LI jz#WH_OzS '㸧KCaV%nj ){oW^/YFqS7&O&|vrY|Z*dzȂWa/$ cSP!N ,`R3RM|ނXuÅ5_?BۯGY\5jNo|{aʊZ9h/~mK"`X*"`M1eIӳ,7SJ=`^ʃXH.RL.,.J ,Ъd`wu|X߈W1x_^rU'iW׏^{tq.6񷍿K~!AR՘ )NF* 9ux%Ct\%9.KgQɋ*a;,X <* *L(AP$ AP\* BaEs#YǞ7n$_kCtmW}0WmQV^ʈ>I~ mƽ9%ݎ ӜEo5H&"@H(&!`P$4 p*" Da337ϙ>$uJOO/>VП>[eN(fb_p~wdrqk?`uPqGs/qcH8 p@HH$a!@h q &% DaEm\Իy]|SYu/%\#[;G .tLWsWZ|a\.^^J*b"NWǻ*ilL@$ @mutagen-1.22/tests/data/sv5_header.mpc0000644000175000017500000000020012146763412020123 0ustar lazkalazka00000000000000/ vư@RJ)mj)fc,1B,Fc./y͎pu1D{-eRa7tfTb)JT*vdkB@0=`PTdZOz SGm=F*=)2gK̘Y +ci7bpynd dA@ `00aU^!62z)}H:]X7NqUlλcjk)h{ech辅*kTd(jA@ 08 {T:TVuw0QlKJ48'ދ}D9<!cW=vEFbJd/z 09 \E7i*of81CQIxD1,Q,<_/m u,V)JȆݕZkU d6SB@ 07 o*ikP)U1u(1wR+TQԠt뻋<޻ j29N؄[Jd>lB`` 5  صĨ3`:4\ȴ"bY Ӟ"0-d/uҥmguΔB(tndEyA 0#4 HG*P̿b7xxrmCMMm*̺t A"(BT.?8EdK`@ : Hm~˩2 NPrYЪ1E叼XRZa饔ƠmV߲-C8غdQz @0P@8 In͠*Y#V$.9sgnF̛?ԺbR\Vko*U(VIi{dXXA@ `0/ ,`rј MvX*<>YLzJ1ץ/;  ]&up+C dbz@@t: :D\=XYzSsWN}x^w]!F U̐FY׭$FЁte Q;1F!-UPYoLdgtA 4 j6VӔ*{۟8^(rm&^HXRFKA"Դð"j#:* dmv 02`̯ar-Xʱ+84ΕT1XqǸQs醘–ݩKe+ ]S*cRduuA@ "- |AuOހom61WϨp9 Xѵ9}$KS<Ӹ啇Bd|`@ 05 iud5'2haµ2$!/&p4  vZc9frdf@ ;`@M93bRqό]͹(u2"*+bխǘ?C\(tsKըV6']9[h;s=PdqA  .`B2ELv6dgA  #&< (krqt))G yN Jlz C)Ňʹ3*79xdrA@3 h(Uƚ,r ET7_۰}L)!!I;Ifw=p^S _do0-amWIn qѭ =gy+0V̪'L9 (ֵHsoZkklh0,T>d@#A9`Y̎\fmy$'@WHbFbVyၢڌqel) g jdcB(G`@z'8jF@, ZY&&BrVI킡g?]}vwo:-[8VJd A`? @1i2iy/9q]UB Lː8bF@SX2r˫w~xyd C`wlx:iL)_d(fcSIKi&=A׳mR_|(ed@B<@?`@t ;L&jVy#3|2mbb6STÙ vm <e(d6>*Ld B\@Hx('p( i^ZÝNF 95 ^A)xf.pnJk*X|ukh}&dj  #P`H`@?l5=F93/`tIi&4͇aQr__{Pd  #8A`4`,1{o:+ile.Yn B)VdV2R.y˸^X-sgʿQd`A:`krFRN;vuHe8C-6*IlxH<+lqL|貔p9rKd@7#$ E`@;}LPF͇ cLcZFVS:Vޛ :4*&⬏m@d@` C`.]P iUM2sT0q  i00+&ctdC4dYhQ" d@B G`Feڻ̿UU |[,aRmj"l 2<7H[2w2Ċ<uJd@@ #8I @N*Ne<}^qG_Q./My};(Bs.&Rd~@ 8N c]Zx)jm6Dle(At: w)SEt4\95h{uOAQ !dmA #8hH @ :IgނYY;* }ܶlIr.;Ww{F=/ =MCdA  #$ I @eG&/j7 (ʚ_](y9 b(.hRS⡋ k>,FXjqPldA 0TJ @&Tִw&igE2l]Hkuu%ԉ45Լx,Yd` "F @NJ(ʞPmUF@*vJC7wl;x`v'.X"eg81d@"8D @.o&Sdɭ"+]"'סyh ...lP)C>AXSjHtXdA#&B$F`@!aκ^Y%ϵC([uB7kCb]Ej)Zy iA&=*+isOd~ DI @R<6N-/ۀ;,?'jYMʃBBŦ^OJViMdA0\J qnat9B QzU .*4Ir9X(Y_ed`  @E s-^8ysK!qVd:@(BR)`7,s8Caf%&\M ҙd@cB$<`R[O*Y+T9<v)iWj+nyuk1朗k8;,kv}?9_ldA $E r>Ol]ڹ~ *zXA׵.JE񖔧LBjԊm,L> G{zM37d}@ p`M`"o㯘wș6sz}ۈ Kf( SIsݼ`vlYA&P]UE.d`#&@ mF m|MmQ7R;A-EkDEڻ5[tjwShCbI8_BdA B @D`@s+;)3gӭD o\kxFaeE`PjtikFiJr hqqZUdAP`J @\t-{onAŚ[Zzw])SL"0_:y?RƤpo%owxRid (@@ ;o-)dy2U zBfQ ԲH׹'Զ0,fG6R̙TTR7d@ BDH @9 0vPW6ANufm( r-x5ZuY#nʽ̎b@dA@ `8L`@၃dbD-|r$$XJP-Yȓ29>IT7bkȏ)J:ud@@$I jA/._Z0XY\J5*ck qΕk73}MUߺǨQ X duA A@H`@=k!g\/d,fg19ԭ ׿td"B @H @/Mc2G~M Pj]4iu`pAPB]1/75AR%Qހ<xUdpA @p@L` pv8b*ӲyÈ NCOϣRc;ooAԤ}8sg^Xj_dA #`?`@#i2Vt2KڅYas$Aık9ʠ)g/0wG_5ta c6d@#B$`F`@U!.=j]| w8[e)l^lIDpUUrӣ+Uѭ35e)r76<d "E @yOm?۟wMwYP*w\Bv1BRPT pe\(ME{AZϩd*d}@B<H@hO05ZT*koSu(9E o˭&{aً9MOoȻed HL @޻?EP r/ hSc' :v)S6ruo ȊВP@FF]ιH@5d@ (F 6*1X)P `:cj 4yuB ,6dOPTmOMd"B E RZ5Iy Ϥ ru 4"+B$vAJ'O -a]DlXmwaN89fLd A`: cѡqd:s+[7`N˿/g/MXg;! * ldP8Yd@h"8E`ަ8XAC,"= ^f\ܙ܌BqN!wDS/spㅫ/!>ǰSmdxA <@> @ߛȂg~ԗul~Vd?d|V*d4\2|W$adB8I v|ԑV5A\v ډ׋eTdJKOMA¬G^̟^dcz1KH=xH ^%PY0.Th` 6xadA`08 EpjDJ < kJ5#nSd)[G; aT1F_6>͸8{㥍WK2OP6A ), 5Kd 0HJ @DYH,츹p -$=4`|v OuI$6J_zQ}_є_[dyAD`K @QozV:ySԀ]NǏ̋r9IiVMM}=Ġ23[ޔS?d 4`>`:wyIM6M ($ P{A* y1h) 6)1yKRdAbTC @l ClU;onTF;E=oRt[h8"1dyA  T+dG @.|ݯخ"s>oQ%?w<)bU6R{,!<ar1dd}`#@B ;z{W,˽Xj6c; ^tK3``Jzd=tBd"B(`G`ʐuo]%K;Ao5q<ػ zPH|.( @˄"hvCQ=PDj96鈅SJ 4da  ch`E` kk.¨jݖ2f̨m|$X󘁲u]u67KbЙ +d#8B G`eOs눷 HRaxRG/cVAlaf w(5ک+d@A`6 ]^gY y)9GrpD.K_r\au3y55=5d :d #&BD`G` \7yFU_-8NF~kڴu[*nL^2ݫ8ddk@`0TE`@c=Xҁy.)"o9*Su(YH!5>,8{!:Uф"c|*"H.Qdw@@  J W*D>ռU oCZѬسEiQ@y9\9B&ٛK{Nq]hd@`bBJ`GC:2RI`<sh 2,hᅡYk/Pv,1U:HMA])FdA "8pL @0qQ8f6pHbCTV_? myK O/-?H:4B1dz  p `N sCI;( ?B$@O&5unk5KX~r>P|n+d `0#8`RIՏ3VU6jȵĝ p)]?Ic*1CMEqH$:V'J[rd` #3`I4+%/ ^JS#I %R$B.1uH +h7=P^uW"d@cBA x5kS'AV(ߗ]<~]fa X"Qt"2ˤLy7DjZd #&DH`@ z CΤ"l~E}q֒^&qqg̵=2!Q؅ ݨ}\dK<dzA`? YmW|t2[b#/|д;yN&ZƢUE7*yd@#8B8`G`RR8,0bW " @m7c F:=mK=iRWyv֛`ĩXb")dw 0t`N`=2(~Pw3iS:Qz (vmaFZ|V.5kC%RQ$ʪIdvA #`J`Rmqr>V*Ċ5 7 ikE,r+eg/8aeadwA0lP`Æ"*IŨx4,y:lNEwxʂ+I TXĐ48P9U9NᨹQ!zndA `J`@2TcX޻}S?9Jf8&} ox}R.qkq[\Ķ:=Id"A|E`v] *]fpJ`fS: T4X*kEXǙ,2^J hMV&o.d@X#$B(A 1Wo\%F>9kXŬjPa0Je"vr d5  SadA B`@AI* " |L(͓ %c*d` 8B`@ 5ٗybr_p~ vE&:ئcRyIs*/6DbQ4=>Ӏ'duA #8 I ,c$1* )NW*Pj*Kc 44۳P]ԕ 8eq d @  AD @ƥvîqz+ 6:28 Et[̪[(ڑO>.l8CY$d@#F @iԏ,RUe1ޞ4}|-#%{Ot<;|TVs !d $D @gAoJsYAj.8C U ,"]bFmx]ʏ2fw-uNrdd 4@9`{6kM_q^jtsϱ:0qi|gMMI)kTɢjX}AXQ5z;R8]IdcBTJ`1MMHM 6ܣfOwo @h=ad@#A`G`@`'`DISa1Kw{z ÚbId]^7gzo G m *d@"P`H WRAw%LAME3.93LAME3.93d@(>`dAB C`dlA` ,`C`@d@#AD`d@`"8A@9 d#BAપdi 4TAGaaaaaaaaaaaaaaaaaaaaaaa vvvvvvaaaaaaaaaaaaaaaaaaaaaaa vvvvvvmutagen-1.22/tests/data/CVE-2007-4619-1.flac0000644000175000017500000014333012146763412020002 0ustar lazkalazka00000000000000fLaCy+ BzbܷH2ĺJl.L8VavL reference libFLAC 1.1.0 20030126album=Quod Libet Test Data artist=piman artist=jzig genre=Silencetracknumber=02/10 date=2004 title=SilenceL1234567890123X123456789012DLXz image/pngA pixel.PNG  IHDRwS pHYs  tIME  6D=2tEXtCommentCreated with The GIMPd%n IDATc?YIENDB` Yk?O?s???????9>?3y?̟>?O????ϟy??ϟ??'<3??O>I??Ny||??C3'?????????????g|????s?|g?yg????|ϟ???'?'????9????sϟ>?'Oy?4uYl??93??>?ϟ?y3???O?y?????9?93???<<3O???<9ϓ???9?~O??g?~?ϞNO'Cɟϓsϟ9?g'??|?ϙ?'?3|?'?????yϟ?g?9?|?gg??>|~sy?|?Cs|??N̟?3g???>sg?L??3O?3y???>3ϟ?'33?~||g???O?|gg~gg?ϟ?g>sϓ9?L><??g??<~?<?'3??g?XYw??gϟ?|?s??y??2gg???????ɟ??s>|?9???3?? ?s|???93???????<O?fss??>?~g??'g???~g???3g?3??ϟ??s Yy???s?9ϟϟ9<?|???9??|>~Ϝ?yg?3???f3>??''???|?'????$??'>'?'9??9?9|y'|??|@O?<|??>3s??&~3???????<~r'??s~???'???s3ϟ??ϙ?9?|'?N<??Y~???O??'<?????O???9?????|''~?y???3>????s?????9??93?ϟ?y3???O?y????????<<3O???<9ϓ???9?~O??Cg?<3'?9gy?s?~'?y??9y>?>O???3?9?y|'s?<9O?Ϟ|9?~?g?3???zY T?y??<>yIg9<3??>|?gg??>|~s?'???f??'O>?>s3<?L??3O???s???ɟ???y<????y?9g??s<=Y ]&gO??|ys??g9?>~gO?~????|gg~gg??ϟ?>sϓ9?L><??g??<~?<?'&Y Z???y??r??gϟ?|?s??y??2g~?9~y?yO?g?'O<s's????O̓?$??9~?>???|||9??=Y O33y?99ϟy???3s<'?????~<'s???????g??ϟ????9?O?ɟ??>?9??'??'?y??y??9??39gsϟ'??|?ssO<3'??3???yϟd~?yϟ??3???~??O@1Y H?'?????>Ny?<N??~?Oys3?3ϟg???>s?>9?ssy|~???9?s'???~O9????'??|ϟ?|?II?D<<O?<|??>3s??&~3???9??<~r'??s~???'???s3ϟ??ϙ?9*YA???|'???3?sO?s???????9>??s????9<'??$?????ϟy??ϟ??'<3@?????~?|9>s~?9ϟϟs??????3?g?'?????9?????g>s|~|???~sO3kYF?g9?<???~g?|y?>3?'g?OO??3??ϓs?>g?y???'??|<3????gyϟ?|'Oɟϓsϟ9?g'??|?ϙ?'?3|?'???ϟsO'gL?Oy????ϟ???>yϟ?g?9?O??|y??<>yIg9<3??>|?gg??>|~s?>s3<9?s~??$3y???>3ϟ?'3Y'y3?~~gO?~????|?3ϓ?9??>L??93????'??s?C~?<?'3??g?ϟN???????f????g?????$s???y?~?93>y?ϟ????$???9?f~?????'~????'|?'3yy>??ϟ??><?3<'y<~>g?>3|>|?????|???|?fg???|?ssO?9???3????s?9ϟϟ9<?|???9??|>~?|ϟϟ~'?3??g?<~O???|?'????$??'???<<O?<|??>332g>????9??<~r'??s~???'???sY?|?II???|'???3?sO?s???????'O??>?s????9<'??$?????ϟy?93ϟ??ϙ?9?|'?N<???????3OI????O???3?g?'|?yO9ϟy~'?O?D|~|???~sO3?33O9>?s?9???r??s~rs?rs??'|?'?????|?Y?9?~||?>s$??????3>I????s~~~~s?|9?~?g?3??????s|???>?'???f??'O>?>s3<9?s~??$3y???>i Y#'~yy?????g??O?9??g?ϙ?y???>y?|?9O|?????????>?'?<>?~?ϟ????'??O@3ϟ?'33?~||g???O?|gsϓ9?L|Y$33??ɟy??????y??r??gϟ?|?s~s?9?>~y&>|?O??~|?y??~?'??????ɟ??~?9~y?yO?g??~???O?NY-?s??9?f~??y332?'????'|?'3yy>?y??3'??ϟ9?????9?ϟ???ϟ?'?ϟ?pY*???ϟ?rg&|??Or>??Oys3?3ϟg???>s?>9?ssyOϟ|?????|ϟ??>O9?s?|??O????9??~O39???gg3Y?9???ϟ????39???9?????<?9???????9>?3y?̟>?Oϟs???'???s3ϟ??ϙ?9?|'?N<???????3Oϟs??????3?g?'|?yO9ϟy~'?????3?>s9O̜??'?3~gO?y9?'?9??3~ϟ???O?s?g?sLy?????<|@y?'??9??y?y9?????39??<g?<3'?9gy?sg'??>gO?3??9?y|'s?<9SY1??~>O?9?~||?>s$??????3>I??C>?9ɟϟ??y?yϟ'??>?~?3????|0|??N̟?3g???>sg?L??3O???s???Y6'??s??|ϟg~gg?ϟ?g3??>OY ?Oy????d3??ɟy??????y??r??gϟ????|3?~s?9?>~y&>|?O??~|?y??~?'???@?9??>L??93?~?9~y?<~g???~???O????|||9???'??'?y??y??9?|>|?????|???|?fg????????9>3'??3???yY"?9???3????sϟ??'|sgg3|ϟϟ~'?3??g?<~O???|?Bg??'g???~g???3g?3??ϟ??s???????????????ϟ?|ɟ??<?y?y#jUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUPTUU*UUUtTUWҪUU]UUURUUU]UU]RꪪuҪWUUUUU]UUUUWUUUUUUwUUtUUjUUUUtUUrҪuUUUUUUURUUUT]UU\uUʾUWʪUUUUrꪪUU]U)URRUUUUU%UTUUUUUWUUUUU*.UUUUVUU]UjUUUU]U*UU*UW*uUU]URUW]UTUUUJUU]jU*U.ꪪUU]UUUUPmutagen-1.22/tests/data/example.opus0000644000175000017500000017602012157371172017756 0ustar lazkalazka00000000000000OggS@Q@kOpusHeadOggS@QNOpusTagslibopus 0.9.11-66-g64c2dd7OggS-@Qv}T{ff^\Mtq)SJ<` ,SƜ\/r6P*90b9\—,ĕ&Ȣyc_8X} gLc5grBtƮ|uB[RߞA:WW9Fq0T5vãNIJL ƆAhKMtA!wǵ| P[XT<2E\'d?#]:oC1]sLO1MywJ F ~n׮дj&(QV QZ>:'άpkT3 -k:T (6W9s}<2L -y~<_$Z }o> Tu_"(J.BRͷoa1Cēq{.G D 8ۑ*"H")ɢL -x7hqM`mҺ<_) zA,^Y>u \ iB^z5;UII686gt#ʢLYW-wpnݚe@V?]s< Q 'i+x=+xO.)i. Uٰ. õL -y~_||K =^+sK^8+v;i7xxjcPtr߶2@vo[lJOggSZ@QEtWQ{eca`cLYW-x]b_ˊ5w ń\/kؖD6 y>wgCM^`zRs9LYW-x]Z4:D20h߸oÛ!i0-nބC\&Q*.lŚ^.oφ}+ L -w<8BF1i3gq({6RO=3nFYM5tf3GyWpk"R[rR;XA]~LYW-w<:{8$3B1}Z۷jk ᪣!m/PEKR-ײsZ7$,mvl!{);LL -y~05E!IR'ϡ(#C ;@&s{(/bdMt,] R_T߶2@vo[lJL -y~ݿ_PCzuO 6 fT2?p\xnF%XՖ0R9b]2|{| j =p k{d`b_`LYW-x]ZKcgZ 7"n*S@$_A8i0 =x޸UezP $φ}+ L -x]=C {AC'q *.v&{U 18C3Q.KGTF[%Z''+RJ6a~LYW-x];) !|cSɋ|)lt"k_|SN_zDWDJ̰;E]@"|;);LL -zO9&/R|zTh3obarݖ*BѪA{Vza,\/뙃6)t Tjovo[lJL -w<֌f{Nr u*GՇ4Mkm|O v9u~<m#7uKq4 5KޔLYW-x]u̍g6a.pC#N'J[080y>půfJMHa9\Ux|5)jBQ OggS@Q.HI{a^da]L -zm20竳{ ;,Q7&'9mdcՏky@~غHc؁Jc0_KanrR;XA]~LYW-w<7V/{wT>nͪ朚ϥqņߕ&@YgpqY9;Ε5%pE]*(׮LL -y~wF!Y.03t$D%0)̢&ɎL鐭XtNhu)8Fb@O\X6%$L -yKFd%~bU`ךX{rFIq~_P jwG{c3HpPzQBq4p kL -yE%Q@1_E`w%絆I}K!TZISjˉ L -y~ *&˼ S2@O! #N΋0+U,e<[rR;XA]~{d^_``LYW-z&\Oj̻) zS-ԅC az1ߨz8ץx fH>T+h\ȫUٰ. õL -z֧63P]r#voxѾI]2[X; ([1U 2(Ah-v`,O\X6%$L -x]Ct_] (EB{•;_uZv{T%E7UGKюq4p kL -yEdo/#Ƥ/^A@DqY]+/I<0 ԚZBr^ jˉ L -y~#kD -*2q=މ9^p a~LYW-w@WAjPЊ^ L >W=~Mv I$ b(΢FN?$0`>Cvl!{);LOggS@QWՉR9{`add^L -z=-W&E$ (pD{`^b`^L -y6S:7k$B*:fT(3 p^DpJ;Q*6HS&#5VVIjBQ L -x]<6iOk܏3;^$_o=>r0l şl-މ9^p a~LYW-y~$Ք$6OLΠU0yDyA1 }ږ.9ᬛւH˥vl!{);LL -y~֩n1[x:@~,rӎC_% fnv-!ɉ-܅qcr2Y?@4}svlJL -w1!mE%;Ng) DnۀyAP2oNk0~s-"fA8+agq4 5KL -x]16kNǪH#7NqHZho<+%Ձݵgx/@Np=^D,ˁWqfgg {a_\Z_L -w<_h8xyu5 s״7ږ:rVD*JǏ`3#DIrR;XA]~LYW-zm8U?_dwy䞔,~Msfq'VroQ 5Uowe^|z:!Bc cmvl!{);LL -zyMR7ś6nRx,Y+ EǙT?X \ 510R2Y72[AWdΊ͌:zO\X6%$L -wrqP䁵qv"R$2\[W3: -#LUD |0< q4p kL -x$'9-&Y. `yHFc!g[vS0hfy w t~ PTdkX)+ ?P\VVIjBQ L -z_FTbn ^2g`X 1 vF`fM\#gꊪ|Ƥx9^p a~OggS@Q,^Ik{c`_b\LYW-yP w;tph}6 {1&=F[Mjr!F+{ZC|g|G\KUٰ. õL -w<NqHWA&`:,`M^`hL -y~'7-r7VhΨOZWMNž/Vl-Hs<ˀ! Ulc"NUx|5)jBQ L -yX`QJ`ԝ/ɼL7brV 1D5`"!n:oZ7=މ9^p a~lNh7[w:t7;T+"CG"(ڤԡM%T)g4Owr-ڒy$Ԋ]L&B(dN0W@Wt4{[J6.-2|o1ʼ#%2$Y-LH \.r(\i $"$߶2?htw8lrh^I(~19 q)e͟ň^Zu6Ak.N@qvh4(8:&%tΌXr޼)7`=1:AKh;Fx 8i;ĿqH`Ҧ1zeK?Z-2*~ЕyWi V~gӲzeTp˙9`]TG̷ OayWmz7GNi8jJr`?"BPLS(sZJ/|OggS;@Q={{--3>LyW#*Ʒ/MNo*n'!x i4QHyWfHt2ٵ(a+.fJթ nO{KSTy ,v{XES;.^38 8,eȼYIh3;RBj6[42jCQC4> G'*S ءVKl3K2q"_ZFraƗo9_ _,rIH,Ч/hv8qiĵ m̸O, o߹L8ݲB%]PblsvekgqߐNB~p%8!pH؝{sqwlwzaTV lٹz9ܪa!1.oQ)':pQkȘ3di 1`tuȩCȣخ m]xc纂?;ǺBg'd>uc; ݠ|2 "#Cyy8Б8eh)7`4)X?<~,^@M(hO6)B9f&iGAVjiSReq & 62֭ҟCG o_07QiZ htHۃQ~䤈c{B!RMn52"q쫐tC$xc7¼ù谱L&2_K6bsA4g A@E9!>Xٯrm =tY=:4JQ3.b䣿knqVC[UujZծYg8y_Ҍw%1^<Nqr0 S1#أ%TM' 5,\r1KJ,#,8{ [qɺꭽ̿VAzyUw\aA:-&>Dv Ak{ BG]sRa6IR]szRZd+OggSh@Q U{hhjfg<}vnA5h=oګZh.mTb E{IXǷA/ĵFwB|?pllnw3E@/m,C1wY<"i ֏+yҧ#6혍E>׊o-sY\7˶arij(bnZe0>j]ZE>{)4;.&/K=nc7xPVCB1^hq,+U pUg2竑{x鱑(N/_kXN 7]rǦpſ c}~JDl%_CG>YʧN*qؠcݧR\PDHJ wUO Mz"$?s3[e1jh˭fT4ݳ};dUw=C1=%zyc{llljkA8,N~m8(LlBfآ =ɵi&c3xsj2'DE];DM;/_#>-g}J[+;? o"PmÒ"l.; ZjM۩3q4K,6#-edݢNPLZF$t997XlRX_+a#)ƞug*.#vKN<$-yH&4L-7X}e)-D+ ܖrBH#m5bGskhH:fJ̦Hip=[%,Ȥp? y8 'Fs?'dTh/25K6(d{طdD)ǖ̑([{LWrr1E3 (O$gg_]z!qrNOggS@Q ANl{rofni9(&:~\ ҢAM_t| 3[+1me|rpg,[QۋE<0\„ _8Т31(>~7 Aνrvb۔Gi6pCÜңEnY݄$p[6êG[I%-3ӕ"ϰ(Ǧ?o0D'|CLK\Rqxh$ƃT*Ļ*;tE5CVTd<**T۽#ާ;hLvp{-s7a `4ʅtiug];Y4s' >.R@գLѭTVJ\Ӂ',xpK%mWebGVyעfIHAr-s{qcer]/o]iah\d+B5) MW_w82sg,M`7F#Ma,71/EhM<=tSIhd d9;B氃ZC1,>U_i*ƊcQQDҐ`vo=6Ǜ4Kx'?JQf@V4BWj `N]Sy輦[^bC ''-eTAW!"NJζ,Rr,r p3;P/oYt oY(c\ $^Љzʯ3ܗ%Qߕdݕؘ=kArGW*fRLqMm\d)t9$͙TPRm7.gTCֈɂ=CM5G]C lv7ncHSX'`U-$g{OXs@&V4_u˳R,:+BJo=隅2܊rO]ԅ2fDiFӱOa[9=Ji=3~e 9dM9+csÍ/@_gN= etOggS@Q } L0{]WbYp͛8}ke(ha󞚧b$yחܗiߚ;"M聆ξ@Nƃh3f_!SheIhⱧq_%xyYO>ڈrdaі֚te">Cu^x.Q8͘Q O0lXmzMA?% Ț'zh&s0}J O S}J3jMk,1r)!ɂNj"38iA#^;rfm V_ o['GDs0lv`s u<ȳYīx…,r`̳K ;;`PJCB {YβQ]%QtccjQB- B*Mtkp\•K8xTg9DIO : P2EIH'khZYJZ` \钱%Svt#Ȍ" b}@A =%5?猰:pcf =HFC'v}e jI{^[`\`S2\(&q]_ܥ22|O)37DS-D j|dK{^{PG  *ZY.q6t==TIŢNUKDTᘩyISd'`;gۻQQΥB]ot_.^vg]^  ݲ |CEBLH&0rVxuC-o)+Wp߈]rt.w1|~u8 oyƗjޢd4YPJK]*Pt@"i9ɖm#r)o£@ =wY(cME"]D|^rˍ9.rIhKy95S&G]@R_K" c^ot31xCZX|Q /4U ){p==-I']iuDj4,kn州BCMDUxȱc/U/ }Bp'YOggS@Q Fji{QYVRW$ɧZ䜦!;mGy-Gy !uf;;Yb'4`Hm~۩ލ%#*|J? zdM0p^Gm?gq {3eg/~]q:s">W Axðe Ue8!&2p[ Aeas/e c~aZY!Dl0 !lY@Td*җym=oU; wlY!~nְp)T/1sEFv2lX0?7h {avprYf#1^56bOA€l D\]@kutL>tO ղM½Rc hGf^RӸ%%z_{VI0e5d+ |PAy?no7/ 4{[jdg\q %p]&Gx13ʯZf3&zSNOf_ ]My[NkHViXg X$P5n!ԋ_[E5mܫ˅QZnXdv+U]XQ|ENV4ù5%rtYn%-V9?"$O2ŀfէvf@,/%W"qz<0"yhH/ŧӠ?u2"8@VA0yhQH-~CÉ֌3~(mM3 8J\M4GbޝHz;>"e!]dv!''Mr(三gHED(ڱݨȉO質m7%2$X| KBp̹gJxPiLjAS$'1 w;cHΊW! $Ω~4dzD'pڼ;:Ń[aK*# 5IӚ+ex>Ķ(TrYKPW誢\._L2twmks1@1DfMo²aAOggSI@Q8Ax`{abtmj 7dn;?o̔YH/zM1f0"`X'&Q6җzLyc+ \K15\hTp'IWWOyXF2UW/!0u$f0ZqOwV6wa6f<(z: 0w]( umQƖ#a|Sˆ^@rN|rD|"wP?*9R'uZuvFK 1A7fO n?Rܖ4|$Ehvo؛bR?8V&(1y{+x@1t+CL~h(;8DtGVykOY1 $XۺB帞5"rMdt_zhQJpGG,(׹ٮ`Uz K}po0B1]2,:"jY 8*# }!5g!u {jd``e*6Ҥ2w0c8'&JQ&ٳ%ZzELhͭE}W蓔Bex um*zĴ~V/6Hɕ@<8+iBbaR5W4S0/!;AZC3iwd]M1L͹Unz95;Icd.|\𸓐,̤q[* j1>94v4Q]|N{*#m4秈ݫ/3=TtEl,U -I΅Ru*Gw~ؑU*`60?0'G񚉢'#u8{%BdP=L([[+_jIToԗA[Ӌ6Z)6[W pOn~2c_yɀBOggSv@Q6l{ikj`^[yۆkTиkNZpP 9Mq4*-:S4 #h!' 8l[= {Xyz.]7QŲ%jF~ ?(%%J~~3wl} ![R /ܡ#,,?Qo%M/ߨ 4mZ8W$ 04֬D*JhF6]ͭiWo'eZoV< b+A64ڹBa 4gYl䩥5^AT}2{FpC<$kP!G?AƂv~>yFFwJPCuF W}-3S ୎nOITel7!>wA d=?Lܰ=m$~(` ?ԻF-Ch[ t`(-)$J23I^6Zd nyҌreIئ·$d?ٔ%{nnmds6^D\B =AK7|}͸jܨlb\;9d䬣Wس9Z@=eJAQ~j;[] ߱r>9w+fS5\{ H7sϾ8r3E+ڭ&$b@w+7 &HsZ dd8':wwgx;DZH48{#!G9)w+6i}Q(\^P ݨr{ [3w|dfD n$^snxG]M(o͋A2"E&u=]Q.l>yHs@P"A9 JĄpရ)!m<-#O]#"ߟ+N/V?˓@@vdn "o2Ms2!WA!<'+%.9wjGaʣT2B6tQ?%=f&щI"5,au͐uB=񕌓6t=[wABK/)yBm/OOaxp!j6$0%`SE:g#GHAZ76,bI`D ~&OggS@QarS_{cicdY-/t+wۤEn[̓NFb$Jk ¥yEDV-eۛt[+1QaY5Loޑyv V!{Zh͔"/ig3ԯ(UQuC=x KAE㾍I~ɂ$@pŗCD_LWtuYwo=R@.iR/'^ţHʳocl尳;`:“R`A,O&QhoHv.5i.w3ٷqBi.0iB8"<0m"mv !{hXahgm7ۆ_>HqyՆЪ<5<\l3x:R0Moˡ kBYHN;qse/&{?Y-"~sFZ<$T&]C?U}㬨:$)9W=} a ٬6.*姆"Qk*A mH( IrY ./4o<xwj>S꺒ubFZ\|"6s>J+{lo&N}=b}xJx "6P~\ufGGb/F | i )}{UApuT^q& -(7 e}  G҄uqrSTA{iUUa8G>N_'sl8إcA?1jFߪy/}פJ Nag0 ^zl.񝩲bW›ۜq*Xw]%U0p|td(sI[ٖ^kh&/OggS@Q& _M{`ihea ƻ:iYa2IN{C͢mPIٜ뫋 fh~Ag|z,{Y`"T+|<ԡlQL_(1"uUVTϺ[ jAak^7}j$M~K!$M-h.9yfSoW9+E Ӻ:x: zBQhUI 85>8B'_ד?Q$]Hw~vG::&ΒzTtP%H?C7eV:,LAӑ'EpV_$SVh&"sDϔV'<}BeB/6H ZaFYrEA&fﳑcūz9A{$E"<eTǵ7TkWIzԍ<?+9l4;Sw!taûk>" $F_7H~q uoʀJ[ t qY ʆח8ʵ:;Χ-uMWY&M[x*=1c#-٫p{Z^faf|}@ Pz-xO8Ȉ$KsPv \$ѢUW 7 =5֓7HyEeW]!c;t2]|c ;68(pxxu-,yT n^q4Rh`N!vn]Ń{miҦpZ~V8׆$H@@͐PX2!fR~J3 &F)wҬR,P.>C׆jlC!LEdct<1e+91uvKUH_zc٧XRЕ6ưV9 ]""*g{gfX 9dceDd/@.fb"zxзCd7F_E Ś0BE.iYE%8m3\A >?z 23"ouR1Knd+EfɈ-ΒM׎G g+P&Gޥ_4C-[\lS[mr{b^_e_[6钟{Qn*0]? y4rb% Lۼ/=yc4 krsC F:t~da +#bM?Z!͖1}L ,`Övء1F__՚׼φo{VFs #JSցFgnp wxh#ɞ<RĪ[Q%9(7QZ@+^!TEy@+X:9x;JD RtuH/2ОNyv7Cߛp aT#fYfJ *JlRv+7[XHf{~FA)G@=,{~Ӿ{!7EO0KFOtZa ʖij/ƾƾkJ-B;eiI^4uT~~o80}DIvdjJ9쵗cʷ[4V:<`'xB3ťY40Z!:k͑vv 3`iHDwM(5ePUyDOggS*@QWO{L{iqpjcɡ-6@K8&΅7䗿\dm 6|iy{P:Zo H;M e] w?ɤTWWigL79%R[_­9cH `HSE‡ao O\$M`|^؋'ų' iڿL=iH`_']vپyͪ,8mzZv{B~X*M5f JkUlMU&}_Ȏ@f}Uw RI[K1-әu d`p'h"k=ry|'݃Ť=V%vWi㗠Waeߩ.ArI-48^}{aeAPGP|+$8=dldPnS?ƞP4>}ޏ=õP5^UdcUB\tﶞ!vFW_ZC=kQڀX›-E*'NZO3!rȼ.roYYS 2h'?ݬf>‚v*eo~{3hE"2y, wGb֟ѝ 9:Q$VdLK8]rUYA6Njm*PI ċLQ|`ɉN(aGhEhSCkVbA!vX4TL[bӨ]C!z)W[iἧ HlÏm_}f#8K\$] q%@+:61"t\EMޓR(TXړÎkAuD5x3rM uM}('H~"cgn%";΢\A÷D/Cײ&JDBK8=R`-K;|}&Chgh㥫3]K k. ozj 'Zg N"4J;h*du{nqceU7$Vy91M V&* 2Pg; I5.@KrL -:XWT|fo7JEE`PxԗˌĭBv\,YdM\7A3hA gMF71`b%7 }Ysx6 ,?J;yZH=\Vtm%Ѕ]v}]vX9,S~"QCDCPpԽ%С(I٢mn+V5 OQ ,:fl"n"­j<]s L$2JL !q4սa4C?:CqZ%C.t^@1" ҇cz9c/TIKT%`F"ViQ7T TD! d_-X mHafsWǮTϟ%0S%I.U[ԑ-:)q넠_&K+3[I{ki/#{^7C,Q3O }-OggS@Qiq{jgc`lvF:,J&f|D@_a>/^9hr3PSe )6=* 7c? OQ.ȌV!  U,WJB~QHQRqq~]y{G*aA_bp\s!q&F|5JV{BvWJ*4~Sbpo|:=Y7C 80F%0[ vPdLxTb„iWV5mv ]?=98"VBۗ0(YHվPO]8e0P??xskoRzJ \{V޲sEqbt& Г=~cݖyd?|^bBWf^ѻ5żC!ḗF7/.Ob;,J|P;6J)q qUg^Q݈z Ls.CJ#WH5 NdI@dZw-/GE0*{kȨ "ZOXvŏLNlSus'#G-N)¿7q{maaafGMyE|(#΄ 4+G;[ 4E['%PeoXFl/cj7g52Cl.Y?̏?aN.M=R lx$*+dֺI(,nn=װUtPʲq䜒5C|Ƞ{r3 .~Ab؂'%wN'ӭ3]Q:sV" 1f[[ -a!^82v4C*={zڋA_q+0cbY62* QY⢈0`KA37S>A: %>9mJEBV7Q;9,|]nI}5տ 5di?l: Amc]׬v T&^&tv {)t!qj=`h,$ ?$>6XC0֊n"eL(]њ?hߚ7ܲ`e1Q%X+,!I"H'0x [#n\,c@V7M= Mjȷ $iD/ؕSz[OggS@Qc*{bfdbd^(:k:, fͅ;jדcyogEB_i2̊8d '};$Bpsr)sußgm瑯{qv|)Γ~[rTKRltL\K5pehU6zƪѐzs31t"Km77+o?S"xГvx!hn|bEQYSkz7;g"EAHϫ)U+wSKk Ջo"aR4 vc s2Mǩ~^q wb4TEi4>3 ^p᎛Z\a?aC?wZL531 Vϥ{C {r0P$ċGZXਇmW}w#De,D;O2%W_H.7z^@OZ"k gxp2h=q_\rD܃?<{]Z_dS yQhx,D[ ;zC'=xe8/qLqWGI_cHi~UGۋuRM_l&Z+ߌ| ql٫{|a ݤnPV<0J}Rahɱ4>ຖjjlIb^@q'x%n[߅@|0ޜaU&jʯK2TVc\稧8?;jR9毴%3l<EDftv81Wۨ9 Dv4WL fOmƞ[05 ~Fd `s4 9ƍ r.7>scg78=\w3G֓OM\Rqח(Z 9p "iğ-ªAOt ~.z!(#-i9_~V;؟ײ6mzYBG@wMK] "0uiuW6O]܉2Iy;8>j"iBhOggS@Qn]J{ggiZ_w _},^2hhUv>j 3ޥsLx 򶹱zBuɝ;v\?FStg6X8-5[i-gL p͈ I+ G#hCw f $-!7 xs]?MecFMrϓ'bѿC)#։+rު>S|I Mhqe0:Uz'蕨kO2.ϟs gbE_ڇߘ{h#%@&WKq?pq3g׹5ijQA +A~SdIa O$lj_p itcT|wm ~&2Rz،ddB#df/,],łoz;Lް5])$r=y8<:W(pF4Xì_3~m{ij5d;9;+7V5Wt)ĕQhvTN /*r"DV3򥇈I<|k\O:{liZ\VJ½mm|(/^txu,ԒHoYsJ=r/GOvb]~oy?;vTMU8-l})!/]O:7鏼|BbG$lUHT XԊq훱X.&hx lʷt۞czַ؀^}+:;M_Ztj<}^!oKRS|D.e؈a8TWfRt9K  _Ἥ:ڪlZ+n#j.wi #V韏*jvgUZCuK?%2XGm2MS{Gvx5F OlE-Y7RE*@K$&;o ђ>B0w&X\^j^SCU};D|N B5T KnTy#p6 h2|L5aZjcN]-(ƒL@];z8|]o0+4öuHK|9<Te&-%PHyzOggS @Qa{DR{cammIrِ.kh^c.dFLңQO{?>Vs} #Iʠ9 [ئ@j"Ռ@Nv:`*Ԍj& aj3ߙ1em2{0m31eH,ܕW^Dλ Gɻ`QQ-|]'| 6JV1C-m]'7,Ex}j?RS)hHK)iC YJ7J)8*Dsp'4i" ]O,SPT%  =BV]Yջ7t05#aC!r& %.S˜x n?((Xj^<%`PDc8m8$ǜ@soPKG9&h+:L!dXӫ e3Tt3ڃǭqr#{o./M:mwU[Ԥ_uY( D ڣjS7Wir:+篲GC7 U2 l ?CtgfğOggS8@Q?{tltijVEdnd.sK{ƥw5*:P-EDdc]nr.uaf+}`&aa,ɠ.@S67ټp`Cv%m@!vE:mJo78O}q7VtjՌg[٬Mx*eAfs좰G; CҚ..U)NQRSfeW[RB'Z|-$=KЪ.'/W{gfhlǧi` MӮ涤)fn_<\>"sKM~UTpmᥱL[ae+LC:2AFϪC5~@ޡ5*3rjY «tI+=PuYv<.)#-)͞:0*Ll˱gJX|4$ 3&լTkep`Hu>ZM>M"g`akp VTXK4ɉ#&ty^sW#tgV׬8Z8o 3vs:H6ZQ{UTbbi @Lő5]RVSLf+uڔYp[̜tP䡹$LA\t"z9|od*P}UqvAQ/9h=k ۦ}y َS>)~AC'{b6prķqCd11>bHGI7L{ IѦW>W;qZa=|;U BO] ._ Ȝ>P:H(6?f8z7%>a j8M#j'M3Jnm5^$]F0 l/HV1Jw]jb$8|7I%* 7]z@?/|# H"4}ֵqG,߼ZYOpm7J zϪw0.9~kT &/&Bݜ]m䧲hD\WrFCFr\5{dLNWz.,#ZDždL^֟:,cNaܙ?k7H',^hOսQ~ycSf][ P_ ;1 Q8=Il혷HhZLaE[)׋_ڑw GI=)j# 'A=B_vӐHwh=ʧ?Pg'~ X3`u摄mz|Y H:2A# ݬ #[ %Ky~g Ul#{HJukX|OJ%N,vu <5-tݒo1 \S"Nj7{|LaR (CNYe]FP meƭji6%*":J~U&luj3"GAg|4\ BR$u d/QRAkTUconLo0gY$Eܝ"ЖiǍs^N/TCGd3)&6<)[µ&H+OqZQʉ~Ќ,{N6(n6_( _@uw@zֵ<* ]] 1Oec_=%0s/.@ Es:yA.]9BDE@M?$19.ݽubƟKZlR'Ź;yEej`C[,-훗 xklܪ XB~.l;2gb(p:%GSn_S\MXWALug]6^q(ѭ`DŽ5xL#~) T@I(+wݲ3^dMj)y_%o/( M'xY. "#tOxF 71yg:P/6[%0i"5AtQJt> k?}VNI;KٿFJeϴeH6'f`.rOggS@Q?jFKJ{_VcYf?|u/z4i Rh2δg|'y+1MQ1 A6kmՇIA SG[UXɡ4VSґ6vofBn# +%Xq.j }#JtE@C OzQNH]2;ź1֚; DaQ43K@fjT,AH~ rZsmَ5%#}E $$ǐv-`2:9M"ʕd6D]YЮ" k߰4F"z3w)(|܊d';3:!0xT|}Dp(Ś }Kt4v ^vfm8]*jGD̊hh$pp޻ ^Z&/BD9՘ڴ pvvjp"0*x桕ѬU *𾧜qc0C^"/qTd0;痖GOGn {]G?\Yt|EsQU;IO0zЙTn'+^b`m{lab]VZu.JbRNJTF#1Ƈ(W«aAI!.],OFalK -,6ZՖbXfe^U?,nF-!MFkMK?'|C )#eŲ"]cdb~wC-zykY;Uh:]}PދmoɄp\w0SobmG2O9c)/=.b>ȊV4`2U<8@(- sYzN`L"h3|22Ky)7tU%ILد:>{v/NڱHoM[pN/ubM>FE֡tD?>tr,I|°? 4RRh dX$iU5[^'~J.ߏxtr,WUOA;esK$;9c<(}y&M '^ݝؚ^_i ŜRG!V=8 ċj{ٻt2`w#Jl~cT6m9p]p&Jfn>n֖OggS@Q;[p{qgghcBBF jN\NPPY,ֶ;-04 AF8^LYRx$=D&L  cu/P, \^)csejʩdcRWvlp+W:>@i̧#.pC NvѬ^POYy#sXR"_˻1D`NZ'ȁwJ5>G \En5BK_US <Ϧ-P NG*⊭;&}ؒH 6rb<ۤSc݀:X'uSƏ\K"Vuxfu)7_5uԝֿOq ;Tq]"ڵʕ~j(Nl8\Mp)'$v-.?gLSѕe`ni,RAXbHBZ]B%nS}؆i;fYdЯއR( D'ߘK9YޫHeB>VAM2:.fFZ$7 "#Z53eb]/лRG(OggS@Q2m{Xg^N=#}hd^10Sזh.$snM K@ǺHK7#6^k&_k = p?f5V鋶*3秸i4<裧G#rڃ dE4d986 å2T3#\Rb K$_4Y&Q5ϩfvljc󂭵6Gp-Ĝ ;,pRޝ hqn/>0dB?ƻh>\D(^LB>Rr_!DѰP}KF?d+jĹz6~ Og:!: Kit-4e #iOf %%_4DPs<"iGՖيBA)XDو_)<; H Z!/b߽xcf {E >Q&5|5{: {.----yeM]Ɛ׃|s=ծjz<…I'%MjyWE,x)7M^MD1ʿ ({r5@yW#Єp4qqZۆpN8/<yW#z~Ǝ~ˌ,6'8/<yW#z~Ǝ~ˌ,6'8/<yW#z~Ǝ~ˌ,6'8/<OggSF@Q'{yW#z~Ǝ~ˌ,6'8/<yW#z~Ǝ~ˌ,6'8/<yW#z~Ǝ~ˌ,6'8/<yW#z~Ǝ~ˌ,6'8/<yW#z~Ǝ~ˌ,6'8/<yW#z~Ǝ~ˌ,6'8/<{yW#z~Ǝ~ˌ,6'8/<yW#z~Ǝ~ˌ,6'8/<yW#z~Ǝ~ˌ,6'8/<yW#z~Ǝ~ˌ,6'8/<yW+2^Z๫ T?+I_dB2yWƸj{ęeEfp-ass S]qTBOggSs@Q C.A)?{9/410>GX+R/f+07lIN-5ٛ8Od{;o.7CK|eq6ڱ3DҲdGyso2_zh ^72m3QMdōC0J"OFGwJgorF@Y@nnq>ʘ恪Axx"륵BDN%_t`>6 Y: Rs+1 {%[\]56ɵ[qyY[?ÍhZ_}=Ad>-' ݗo5)m9y"mxSF$&L˳H n%p>QD(aniUڅlNd?ԮWso1EZTF@I׋ _<[R8TydƝsmL_g0UasK{]U2.$籥QjRE3; q>hD>tέ/&mnْ}OggS@Q! {mploZ].WS>eU%fN3"8y/ nsѠ{IC+N$5/+h.Uynj̿z<\˚I;U`xޡo_c?; ZbbpY4hD#uo_6Qm.XH?mIsD Z~BqaERW1* T93I]5.9ŀ{buwobdz$O$OUFc̾)\"z(K˵|5$3 -2u8Ë^DWf!Nʝ8_'93МL^ha)<hBE$6ޫYs͔?ySǘ*5#v0ᕷvw,?S8H Ԝ ʉ"6w:gىϦzw*4C$>dq ^=EIBc5F(/~PIf6ӕ U5p\džKn<!#*Y9BEZؓMr`+. !PH"gˊ2#4ƿEA[2M*$ŦP܈`됛hۺNs!wF0wfK)A;" I̞S,PF4'4 &qsdcX⹎5JZg⣶:bxrػp^~?I1HD qN,2E(viO͹5rn-z] vG^sl`R kAVHr'l[J$TA)T9|ⶮgWMOggS@Q"ߞg{`uwmq?9X!E  ];}͝~y2J5Qղ| zYT؈?.N,.o GXVĽ-ҊE'y6GEˑɞz ;mBwfLڏ#T۔Ry OՀH6@@+=}8%Y̑`q-xŤWCm%X>mjoY";Ԛh J ` *QY,[L=DX:zkZW6]5 2b\fi9]Z2Ia5Bx+13TȦLA9j=F# AX$fq_M.1yTb93vkWK'4;_1V(覸9PŖFHIZߖHf7»%D #J"򹴭ikyio|+*u3R\HuU|mı}dYl}VB3hE]`b ܋c{\bHm7r2mA1ife Yeb]{~?PJXOggS@Q#%fT{egdj_yZ2^nIW~<6&] m) y ~R.O2̀s F(|[eY؃(Y}^=K#:zze"28Qc>ξ*v 9Hd#S:XlUϨ}+06|G 8_K ,Ƈ GF$XYUeciy˖gH+i9yPsOMA#Ё.?VǦ+O襊H6_\v Hܖp=!AG #.6Zj>UҀ!4f ,?uP3]яYw6ri޷?xL5E{:Xq3ikF s $&yG$/ߔdKqg^`Dg㵄U.uN;S~}'{2vOt"LW^B"-3h(/ŁHyjPI8f{bRaWkE2: rk5[7d{]]]ncxQ/u{ YG}d`f:}A>4uђ3@~- Ʈ^d~RZŲ mK-4 ;t36[w7_T_h5_ 6J5$.1O# tWp;_x.E1iq?fR;?R TЄvn'wo1CR|s@‰UiIݬ2ڳh3- P1Hz1R, P肝P(iх:5'Zxz˾ ?ux@^`O,e\y1% {ЫSAZhK|!cząPvA.tTV05BQP+ !AVǟe9 qUsg|(8XuX))Lm*Qf#@h9=P]fatGa׉Y>!H%n޵~!Fl>pM!={~<1;R/p8͚l!MHĢm7+VɓJM[+Pg99Ou \ixP<څuੴq`cܼц`6J xD;HpewD' ckFD^D!ž{9]Ywc{V\ZWP$[CJ#塾ME`׬ҢaZZHpY]O l<(c OD3S>댰)5+W݀@s 4:?Oh?:Y#?rQ۱p4[kBD܋ ߂q=^QAyݍ8VȌDtU 6 ˿4zx=x,_-?МNˎ` lde}H"GڌpZ· HJrvV@<{ćR7 M'^1Swd.QxmE7-6N(V$?iMxY4-.LqǬbP[u/}L TG|}}?[ 0GےշWQ4&o~U q+Ah -0W8Ait՝5<|i"S"ѽiAqjj \r]| ` T4sQ G' Sh"]~*wOggST@Q%]x-=Yo{MSdzkNrG''Ұ:W<*d%ZD؟9,;wGnur0|}^!C"gbV_~uqͪ"v̤_jE&&y+F|dݯ++#k5KRCdd7FOZ|p"7ȕj?Szoϣ(g®VRvRlswunW2q#u1qjL}^o)c.H?lAƟ'^ Yr ydx8K-aJkcb ;ZFa>uv(^:Y45&=.r6 fm0o7GDڿ y=!L`wl TaRҭ-(ej9c=my,11iw'2#^r`hr=`[awkYׂ!Զ ً8ߠ`d.p/Ah"k{$(sNL?)AdrJW`5УKFޠ{lc[deg& G\%*9jғO ?˖EA-L gDY@(jۊ=/s~vvyФ:_.2c788T/xȏ\{&Mيi_i96[_b,{B(J>̶SXT[*Ws;eBN/8Rzl/=>W$?CF|\uHV~ >dAFOS2woZINB0v! Mɴ,v]x.`}̶бhdHcc1=IWآnHMZjf&n͛y*dFg6Zs>xܚk38<1sC n9E%BPf>lO3  U}c$9c:J(=7蚦y߭0QGhP%nJ3#S?&pV.'p-] [AWݢYBSo$%̓+( 8N]9V=2"fT" c;-uPNB2ph7^*JA(-rd*i"ܷX ,"OggS@Q&m{agnig)) \ϛH_VwC"NI8T4lZ6T[a?T 00!yf83Bq>b9ɝfey 0Gע?M⣜SEB mMQL\Ni-l+h3-oM&cP~! _b?mD!XVZʒÃΨM?!4)R99èR@·, h֜Wi2\{-t"<Yl~IO`_C*hCwgBN}[+lLWĤE 46 ZC'}@v|eӀȻ FX=LQx9]oMkWԫdy@rK0NUcjK,AUǦӇ“1X.sPV1+uZ#Y䱜D!sFіq]Bg6MbPF}}G%'0n|do. B`vN#͘,w 4߭ e^Oش,e,h3T&ordux{uffc]}9)m˔ө囋utj+#҆2ގz ؖ)O/ d8ؔgA!ފEEE :34oa3V=a+;G଼իpt̤&ýgw7T6P*춇 [pK<ڛD~Wan7*ӧEæۧZ)ɍ)"+4iM̺10aY8C=gV|2%,a,ፁ ui]Wlx $|c 6?M~aBIxx"|f.Ÿ[`*v~= ~28 v!_4oij}D\W(L«8ov)1~C2mP:Re6, vIZ>/uG]J(eԟ ` cg1Or=VPLWZ$O'B,'Z ږ+đ0^ .n ʼdFb+辋1E{rw!|#Z0i*?bP/򣱁gO^䬢mG_q&Yl i%֛ |Iž3aq]و}n )NMAgCO?">V*u2Oʀ'BX7B}p[vkT7H2}h3ՏHAsrr=`iruLƐk{Ѩ"L9phοg0u9s,5q"wi_y?Rw9\ظgΧn[CRLwRw_.Xr s/^ ԣr;^X?DyӉEKPUi5 S3vX Z;lWqhm#-UdkWe{fncgoX+{1qZ"inX3R.NwEyv@CO6mһ!`H"7O 1Dh8$SFdS4;?mx [YG/G$ZB䀕+RC5k@jg`+YњYCom\M4u4;Wt褻)E xxܬ)~۩#fE)1sd9dEc+1E5:)i;Ⰺ ѓ Kb^'@;s_+>3BB!G%I>DA GwR(=Ɯ!{ÜMc[%* YmXRyMGaˡ! A\43Z ȥ,/qU*-nWi7~>A==4#*rLR5R{OBoȽ#+QrSȀd9Ҋ$AWl5Z X3)%rz딌_%4`!!{wE":{ͭd)|PdpH:8{v-zTqY$%i%7POggS@Q(}^Ui{ficb[o+\8WӯDanpiW5X4QoYYP%Ń 꼡oP)3\K8_4:nj*s?*1*#$ Aa`!u9S`OfGtH^0$I,l9p\m ^(}FKĦfF\mSX^H ]YPAӭ3dߟuPxc㯼eo31JXf^]=;!&e3]Y ()'d09M ɆF= Rdw1z5bSlOM;Yj?Vs(Iâc, U/[#Hy,pګE6/WO%͈xeOMnǚ"j`'*$QGOM1IW([r!,j@(qvcr]{K\Dlg,-<0ӒGX!{(/BlϓBv'LP T+:ŭ6oΌ'N!/"\;h2Kf,0PLeGolW.C[ `˖0KpoR{lba]qܼs/@Q)f ȿ[Z8.@+0HԼǁbG;Oe[L<RA М@1i)2=oY[dݾN]jx"eSiM?"Zy0ϬƍtnrW+sgʮF"cI~g04T38K?H@ F~'Ng^3!z\"]<{pOggS@Q)c%{^ihdafq)Y]ds:q¾ڍmJ,ضWF|\E϶fĥϧE"UMC9Fik4rtǘ@=֮*''ʼn.Y7Z͟2|Bwt ?6Ejm ) Ꜫ3ti xjXH>~ W~]DGVQW:Ux{7 ^yMK3kH~R*ȑ6u hE} )k;cFuEzޢw|Pk oE\4`a龏lTzmVe.e?ݓ\Egh94nYsMCYͦ%H|+$=D'24 ̟CKHwcc DqJ)f_ }܎,cjXEYht&s1tT>(2|ve!Q34hxj Ԑ/s4 @қl zyS;GM+\KJC?ՎFaCN==O zn7/{bW_`S.׸D zr]G* 4j+A%]8?I\1RH+kYE8Eogp#`,ۜM@S0S̫_sg_spjGtm՟Zk6x=͒~d33mSF?ʠsQ` m SYeO0? eӦU_$@fJ lQ6߼SCtb棛3 a`,-& jg?̆6rxV…*=ZG*QBJ.rXifC(Eln ;i Z֩X(({gXE-\Z¼u=peN^až3pjFk~nDn5D0ؒ,pHY=Y4Ͱ6N&h^4|e(ȏ@pfd8 `\QOggS5@Q*k{{QQZYT Ă4 lX%2h fІ ?vAk*U*m88nZb2mx X[jD TSyoħq* ]%{ 3u^!z"(z>1oc'5ÏI|'Av jJ:.d$[uԝUe#'F$c's?jHafR=Lְ A }85#x*G|xk|t#}JA*QzMzIҨmVpHڀsm*UVdDu/pfeJ(P=*xa=C/yiU ջؠFCVDR0 _g~(Kd@H톒k4<8`(0+s.AFVRuI!~D_|'qJ}RkǴaX={~/OnlJ?tσHGa̋!{]T]VNZE6oF\!kUGAƄ973."EUiɯ Uk\7糷9?k4P8 g{F2K74IDLR]_ceľ8b"RCܝ{ .;sCQCq7ukyq<;-?um$q_c;?KeA2[{B4(K#q])~1+±DTE/⬬+ y5T0ʔ:7$1x܇9VVgZFxfXg:xir~`Br!" 6ǫ>\Դt d F;ܞ?,\|h"ռ|#d ف* e~ă=S|҅ OM.y)iM fHm+ؔ]=UQ\Yc D!K|13V~2OggSb@Q+b{O[ivj scܧ|XotqB =!qP0Mk9'5ƅQ!|s}Βb550>|շ4)rffB` Ft[]|@(M}:S҆Z  ͋*TFeFى͝۴AlP#1԰5bBaNSoD&NT|~@xs[71HSev./4jƓwy5*\|߭p"Qy=+B|HPϜ:8TDP,z*+J\~_酭ܢ!͌3i%A$ە [Yȳ݄c,!x<`uwO b=H D1Qc6C \P:s h06ܭ']Tg*' =xψ{)nO^[eu8]oz>9eqj[X.[U@;7,6ml/%H,%<"cswqs^BAY_ʂӰ?]"5~tf<y7o-qEAN+g~d[K_Zߖ=gx8؏Qլ{cmfkh8\)tVy7lU>]htWZ=+5.3M1r{[9CLUh4VѴ>J/TSH @Y{XD;woеS̽9#*q7TzRy핦]ye].,a pIʝ©·_qWJ M飁Rs5rH#nj|f4`7eAV5~VeפPc/8T>B$o(yplK]Pz,Lrl"sMwg%V:aU"hsg>fG q+. w)QnTfx_{0cZw]zzDIs)B,P ^)h"'/+GzJ1 (rgC7qd. 86#Lؓ5^fl3JFZSd6 %yVX;o:>K$nLP+[̤_OP0_CMbK BfclP3s͑~pP6*XDd^~> c6ʢ׏|;P|Ӧ GiQz.|^SJxʕǔ-S%A/$SmVˌtdpے)b OggS@Q,Μ!k{uh`cai{zP(Sd癠JC+$o&W:Cpdx"fpXKĥjԍIljq)N=+C6<-o<+yx){\t튎#CPE3H+-SzU50L)G|aS+z(9u ` V|S0n޷['B};ZCk~̫)=\F3̓Vp]Go &#9 ~.;oH:椑yG53.*f{ 5&evzs‚Wؘfm̤dw3أמzRaR,j6?,! sق/--jhmwY[г~VBA;${$h'R:e{O(D؉N^hߗFj CxGǺ̟t/up>I$ ޕ7OLq

+}*[Ųmѧ>'/t-[ k ٻ\y8v -w$)qظ0`ȋ3k,rюORаSr˖z? Y䦚Պ# j&[H.ޓy+s=2O9Q8xk;ҖX-w.W19J`L$i%Ϫـm^j;"|\ƓNsҊS);7(Oe3bԡGZr\]V_k;dv|C&^v@~7M$fzA!y?֫ sG]H]PxR,yGCí*"cXaG}16PS;LDmsH`(dC#LhK1WbK1vy#ЩJ(k=?Ӓ||MY{|OggS@Q-3'fh{krJ`k#2xgC$ȿn0[%OBG~#20]m-!s`wE~cAav򦅥׫-XSFghTD)tX/fA; gQŹj/kQ* **_?9:~FW`H\T@8Vuvv;B7Mjm.9uy{Lq|aE`X-|g #&}N_5).y":ͯ ԩ\%itfAҾ&۳6!gH`jJYۀ)^?9 uA)Z&sQs V8OP%UCԯ>(lH+Mv!ƵXG#u~!S@Jٓ<N);h~^qO3GzjTa *ڢ:ݫ'a?x !QěfӻBir4mo >:gCב]ۮ/l L~ɮ'GŞ+깞H9o>+#A2W@)ulRgJ*l6udsMB0=ZLA}Gb{d[]nkhy^0eeތz9:%j{&<']s|íO:eGv:T9k{կyYJ|M{'%oϊ݇Ҕ9=𹥈]~uKeE%A?4! f`yI)Ӆ{ls"QkA7vP(@., '-)9Ҭ)S_}ULr46 #bHt0K]Syku\_OgRӸ ?m"1~ l5hZ㲆7rDy\t˶5w[9vŐX ق u Up١?2BYыnI,}zʖۣ{g({WZj&ߣ&*h5zr+Oo}oة>)|,(ۃD8l1ݮ| vo% ֟_o4C&/;xv~Jw7j_m?Yz'ZÔڇ|O(S:N䲯 W E#=" < CݓsEYPn5u"sA~://d z >{I NXۇm3S u.qX:OggS@Q.$ m{{efiglY[KK5@U!eſwnWP@rcmJ}$]) !0t3?>٩IasWX;ZUblΓl}8gKkS~R' j8LB H|P@:@يH'`?.ݹ!Q/=O [t2d`&'[q_Ol()Xȗڧu~Zw.MS.ᶩG"C İsz'ߓYj-) pۺVp4) a.˩m$liT/(>,:0܎_ԯpy%DC[Jw @ꏝ2/.Weܜ#n,^c`RoJ`= əO\Ý2V;AFŀ<PQjUogs ;]hY!W* D\pD$kͽdy#ί%asa;BT 2~M11TeVauoi49]d O&mLX{렵HKn"2<&E>g(ƨJ/BGq!15FQ쵾s8L-s&{aotgf|oyK h/Z+՘7s 8={֖W7?Lh_4C֠P k׽mJ`xB,,WgE ͵,EKLEwk!b2ߴ1s_j9*G?r$tq:Z\73bTwϞbܫ?ZA}>&` MHd>7IZk3~|4ZI r$'(:'}:m3:bs. J\~}vw5tP EK_&7iW)a檞_xsm.b^I_ h #_?gjB['Uz7=l[Ɉq$n#9|pVIn&TOggS@Q/XUa{aae`g8t}W{fn0CY~^UB 1.r6cӚ'(.!_8!$C'*OΟz?) 3]G ՞Nы_TZE2_ @IqԢN<JZ6Mh1p/l3R [-MrubL|8z#3Zb]ĮvC'BmVanago=^4YWd1@K9+.}ՠK7Ӂ}<şqmĊ,W2'(T"8o[wq|v5QDJňxb,/ϘT6` ?܏2Yt&-IVؽ?Q6ep(E>}ТڬO6#fb[p0_ 7 66,`61$™Az0C'^Z5~hr-eoP N&oqx$PV +`H"uԶ D?”g$?j ٿCpj`|ˋa_pǓI%PD%{^a_nh C*ڄW R/\L2YLb͹_/uRDB| 4}n©RLz4O}:j-L5~Ap ZXꕎ:*\a ߽ynOa|qǸNl^jꔦoB59;.}1ʀ<8i*g셇Z/b>42xG@!-v Ù :mA\t~ܬ]jrtlrn:iG3LaDw}{"j4Daz-y0 _L(NoK}5VY޿[a97\p,{a̺!s cfL|?^@mvJR Ʒ:io8D=;u:I+cӾvclWTHC~%ۿ@P*ȮN}D$1RH W0gXW9؄8GJOL ⪩ۗ_@n~\:٣i6%OggSC@Q0nTf{^Yjdj%A3GuJ(>oH5{oj1;iZ-r}Mf54D|,QϹlVŠI`ѻ}H3xAU#wjε G"y8LcPN'c|D9hKo^;騥^";^xh(~T[ Lm%Ғ}#!ZHūJa``mDžPuWҋ l_*wO/{64 -*IMGC;:2."iV@!71/c飽CD jLcxq1Psy\mt=' G).V32]V?PQqDM-dQyK:HTX@'j"0[YxTqC^ѣvȽ&|4F}U6s4_P:յ{m^@in2<@8 FDFM-"Np̸, 1 T}q L՚´DO.K9We<!v3DQnaUVm۔dෞXJj6WeP۠jU@Wy?{cgd^d?ya.ވ{d)ۧSncr<7kk2fڳq ̩>.MA.0h@?АՅbN㻜u^]8ٜ] pYϏks7%_rfL,2ʻuK?)dlT~*Gob!BOn 7?Z u;!X==7YIҀƽ+QT ߽3aeIEÊDuKjއA!Yr7yOI&Zqک~} dˢp5E7XO]STzpy d6d=.rE\Sy}\w"Aiq8,gY~aK9A~$RF>gmk$KD̳7\5';NB9 v8G).4L}!!K*L :"Tp*/hmVde։઺nŸxj*0_w縇<Ά@k{9Rz8_.j:UpL w[rP+M; W`fbF>`49OggSp@Q1S={s`llq1G(VZ-9^@S0sݛu02)< +Mfs*Pб]k6~ r~| )T' 3{&c ZmvŒc^;HL)aI&in>X=7Wo25UQSɸNZ^^Ge z.q8 <;_poN '4=L?&gn^`)49zp]K&♱lngUv1(VZ\Ba+s>қK'FF{LRϴJ(Q8Y_\ؽt5V!⿸sXӬWݿ}&p^61q;٭^0Hf޴m_,ӝBuSj5.zU%@ʠ O!F2NXPVe~w^ }w{} D["X,rֻ΍vGhm+zavȪV~S?<yJP&uj>e~|B$\}5ib{5G uqzYA?ba`a{q󘺱 .HI!&ό;ؠASR͜!}%Ue[;ۣ&]\01Ϫ5yxAS؋ HC}{.nʢ{Z p:?Q2M~Q䠊sefiu8_Eرtýf$=zMH.*jȯ9/kAW,q05{[%E|[XJpS27,ԁ/} Ql}T&9Iv]l@{$mz!ŢJwF%ŠWB]2X](5o 5\hݟ/`FK`{{ alUc< Ǡ)C'vOwKBcN_:qiCDOggS@Q2 rMI{[`_c^xQ1|Y lWW"ܱ9GţdoZ:[&h pbZu.wPёjPl>.=0o'.\~.@hj N@وϚFS 5"+.3k%@m\s ˣu/\d=v"s vY9%o0W(@ق7u›8" Yuf綾 cb~|eAj7_g,mT cv')= 1SkY'Iճ5'w( 쩂+ğv.Unpm7lc˧ 9'ӾrDk#WGyJ8;U;mI-+諦Vg  9Qr5>(̴tœ6NA3$p$݀ݞw :R-L6Ω16%]M@r_rv;xjUk.cKvb#Wr}9ȏL8N  ϵ:_r,b 7CRXZuב5E%P$={V- >=A'w@K9 J76HCcзbO|귍E_"Z]侈((rCrF`l`K&\(40rtGK=` 3]ya]~玪ZkwGom(LaE:1(M*3CEp[$]+;)vϝMB8uL^Ils:%&+K1&hB좳Z>KeʨowOggS@Q3'QS;{`\Tc`>TL~VkJtc5΢ķyqx9 v4vX$6Q{#2Ͷߎ]>3´N,U1{V³gJEbf fidi;qy(OR˨O{YujJ Ez@VykeizME]r/ū{-/3On(<wtr.10B1-<{E75!bWm% % _cD3귖$a€ >ʕy#plbhzu:Dpf&[A IAwݵ AY6o AA4ٺ/ !@& n(VҨ0ҙl@rS*|ǟOWlL44Rp0Gy>b咸2`/hfJoTZT,2UOSe+';Z`26Zwwu74g;6^Pa[^uxuv@'J-nD^% ketʎ1R#E{bhjkl z ߢF VUN 8w- X & G̓0[Im=E(t$# MF9OdIBb5(+\:fcUܑإ4U?5BتS,?2yE~[R<3G}}~v7giY|άUaLX #:v^wzUؘI&,kt̆>^%^/@ZoUVkGJc %D"2tu |W.cYD d[-g lpٕ )ˆp_?IkzP3" 9^z);>,N*W5{ǡ> PT-IbhR8-vl?\\E2-v-[]bxig= Δ7+aspeZn ֟ǍIEiAy#L_HߗG@O?󷡿!d?LvA[d8 ZABR>8: VGyMgfL8̿qE&SVOV NTހ3AHAΝro?N2 \>z?E.8gCM/,8IC)*p6V R3ݔ x6_NJ PoP3URaNiA?逦Rq?jIaIb<0Xmt)t쓟s$V<:4vC[H8ZU^N*[v}vt=_uINy{mJ{i__a`‘P] s]KF@WBGwgvٱ,tYuuGء?s,s@I+>.hu A3sԢlDU$PE@hCx@Zܩ!W XBm6g࿁'30r`TM7QӷiJL1L]/Jqy=LT3dd~%&-eaMH{_X|8O~Tj]> U=IWi:r,ZrF9Vsgjw^ >y.ly"^1v[y ΎТOoXI-E01zs}]r|lmn'w]Xh'ԴidVM|cxM*7݉^R| g'Y{M:ku.tO k>tU7WfEy1!/nRߟ- r暛{xH:H&,OggS$ @Q5qBNN{`Tdhdv)Fֈh^xn*zCu{ͦR3[lf@ <ݝJ_eŞȚÐC6g5KrL 'y5Nk (y̱ 9( - _H}ID\b6\]|U߀T;)ap ,Xi]CE.(Ƚ0P6QL;v7y6m .3qp)vY6Ǽ@_T|tA蘮I"ե!_}EtZQE3\ CEaO<J.aN 1k52u/$G_fQK`'/XUM C'uLj~;te0a{*LD"NrpS/h u()XLHHa |YS޴.~kܺn\cL4HGyL6A}ΩqdXldW{u;*<5T> vͰ50Gr^c96XUc{ZUajl "hXdఫ3^嚜Nꂍ"*́c$vTdq7[O`A9I^Wuoq?|.;M~uݞ'؝;vIȶ=+`b:h),тg\l'WS>E?=)[gOs G5Ta a$/h\|TP۫Rû2clYwbgm/}oeA\1; rh+jDg/ƞJͭa;d4ەKO;)wBԞ*u{X%@uu #ca{1nei,XН!'"eYHo\ ??$6<`{}<ԙ~JXX`]cL;g1]Ӏ49f}wjdZom\V[ --NcSP+,йvjLU2Þ.`H eu'=+I6_u@NќFR;g LlToRVV=)]ޤF3k haZ)AzW/ƥe=7FԄT7 {ґkdEH_ 1X &62Č2Ð<ғot`tsNf7mD߀ %?f9:f4£1XOG{`VQ[ic PJoWjQ6Nٌ",BHU ? ]QD4Oi ~ZWT/c:YhJ3/RmH /qCRQۉM˫ϔg 9 v%)xt"ƑcW|v2cJ5GW˚^Ur!#S Z7'^(k+P̳Hl\_44_)D,v:OXr'g 1.,#ї76(ޗt)o*ͧRYWONk-n=Pf_j&OXiFwɻO[^WB@ZHziXI36-J}uWzn;F] ){SA II VuДQ?-02"+ ѐB:$?^% Z}<=L -w1I0F] 2Q_@B1_Es먕;->~@wߡ9wՄJL -w<~]G*z牠bbTe+aV:%T%<vm?PJ;] ~{gJBynp kTL -x]؀tZ(luyɸ?`ИG5}1R֐nXuD`/W}g L -w<@|QJ5+y[0> +c,Kߛ0UY&ڟ|ib٩*_KanrR;XA]~;G@%w x8s:ΠĚ)): K0*+@J.bd xpgОډt#$./_mutagen-1.22/tests/data/silence-44-s.mp30000644000175000017500000004000012146763412020127 0ustar lazkalazka00000000000000ID3 TYER2004TCONSilenceTLEN@3000TALBQuod Libet Test DataTPE1pimanTPE1jzigTIT2SilenceTRCK02/10TIT1Silencedu=@"`1 @?˼N(|rrDO xKإ2Mw3ŕQv. sR܄$dyA 0#4` ADYa ^O-c7 EbJ!SwukrA%5:=ʹjw0l%RPn@MD] uؕd oA` 0= [[6mes| !)V+ -.Xڤ4ϹR[" !mdj0< =|TVYrK>͎pu1D{-eRa7tfTb)JT*vdkB@0=`PTdZOz SGm=F*=)2gK̘Y +ci7bpynd dA@ `00aU^!62z)}H:]X7NqUlλcjk)h{ech辅*kTd(jA@ 08 {T:TVuw0QlKJ48'ދ}D9<!cW=vEFbJd/z 09 \E7i*of81CQIxD1,Q,<_/m u,V)JȆݕZkU d6SB@ 07 o*ikP)U1u(1wR+TQԠt뻋<޻ j29N؄[Jd>lB`` 5  صĨ3`:4\ȴ"bY Ӟ"0-d/uҥmguΔB(tndEyA 0#4 HG*P̿b7xxrmCMMm*̺t A"(BT.?8EdK`@ : Hm~˩2 NPrYЪ1E叼XRZa饔ƠmV߲-C8غdQz @0P@8 In͠*Y#V$.9sgnF̛?ԺbR\Vko*U(VIi{dXXA@ `0/ ,`rј MvX*<>YLzJ1ץ/;  ]&up+C dbz@@t: :D\=XYzSsWN}x^w]!F U̐FY׭$FЁte Q;1F!-UPYoLdgtA 4 j6VӔ*{۟8^(rm&^HXRFKA"Դð"j#:* dmv 02`̯ar-Xʱ+84ΕT1XqǸQs醘–ݩKe+ ]S*cRduuA@ "- |AuOހom61WϨp9 Xѵ9}$KS<Ӹ啇Bd|`@ 05 iud5'2haµ2$!/&p4  vZc9frdf@ ;`@M93bRqό]͹(u2"*+bխǘ?C\(tsKըV6']9[h;s=PdqA  .`B2ELv6dgA  #&< (krqt))G yN Jlz C)Ňʹ3*79xdrA@3 h(Uƚ,r ET7_۰}L)!!I;Ifw=p^S _do0-amWIn qѭ =gy+0V̪'L9 (ֵHsoZkklh0,T>d@#A9`Y̎\fmy$'@WHbFbVyၢڌqel) g jdcB(G`@z'8jF@, ZY&&BrVI킡g?]}vwo:-[8VJd A`? @1i2iy/9q]UB Lː8bF@SX2r˫w~xyd C`wlx:iL)_d(fcSIKi&=A׳mR_|(ed@B<@?`@t ;L&jVy#3|2mbb6STÙ vm <e(d6>*Ld B\@Hx('p( i^ZÝNF 95 ^A)xf.pnJk*X|ukh}&dj  #P`H`@?l5=F93/`tIi&4͇aQr__{Pd  #8A`4`,1{o:+ile.Yn B)VdV2R.y˸^X-sgʿQd`A:`krFRN;vuHe8C-6*IlxH<+lqL|貔p9rKd@7#$ E`@;}LPF͇ cLcZFVS:Vޛ :4*&⬏m@d@` C`.]P iUM2sT0q  i00+&ctdC4dYhQ" d@B G`Feڻ̿UU |[,aRmj"l 2<7H[2w2Ċ<uJd@@ #8I @N*Ne<}^qG_Q./My};(Bs.&Rd~@ 8N c]Zx)jm6Dle(At: w)SEt4\95h{uOAQ !dmA #8hH @ :IgނYY;* }ܶlIr.;Ww{F=/ =MCdA  #$ I @eG&/j7 (ʚ_](y9 b(.hRS⡋ k>,FXjqPldA 0TJ @&Tִw&igE2l]Hkuu%ԉ45Լx,Yd` "F @NJ(ʞPmUF@*vJC7wl;x`v'.X"eg81d@"8D @.o&Sdɭ"+]"'סyh ...lP)C>AXSjHtXdA#&B$F`@!aκ^Y%ϵC([uB7kCb]Ej)Zy iA&=*+isOd~ DI @R<6N-/ۀ;,?'jYMʃBBŦ^OJViMdA0\J qnat9B QzU .*4Ir9X(Y_ed`  @E s-^8ysK!qVd:@(BR)`7,s8Caf%&\M ҙd@cB$<`R[O*Y+T9<v)iWj+nyuk1朗k8;,kv}?9_ldA $E r>Ol]ڹ~ *zXA׵.JE񖔧LBjԊm,L> G{zM37d}@ p`M`"o㯘wș6sz}ۈ Kf( SIsݼ`vlYA&P]UE.d`#&@ mF m|MmQ7R;A-EkDEڻ5[tjwShCbI8_BdA B @D`@s+;)3gӭD o\kxFaeE`PjtikFiJr hqqZUdAP`J @\t-{onAŚ[Zzw])SL"0_:y?RƤpo%owxRid (@@ ;o-)dy2U zBfQ ԲH׹'Զ0,fG6R̙TTR7d@ BDH @9 0vPW6ANufm( r-x5ZuY#nʽ̎b@dA@ `8L`@၃dbD-|r$$XJP-Yȓ29>IT7bkȏ)J:ud@@$I jA/._Z0XY\J5*ck qΕk73}MUߺǨQ X duA A@H`@=k!g\/d,fg19ԭ ׿td"B @H @/Mc2G~M Pj]4iu`pAPB]1/75AR%Qހ<xUdpA @p@L` pv8b*ӲyÈ NCOϣRc;ooAԤ}8sg^Xj_dA #`?`@#i2Vt2KڅYas$Aık9ʠ)g/0wG_5ta c6d@#B$`F`@U!.=j]| w8[e)l^lIDpUUrӣ+Uѭ35e)r76<d "E @yOm?۟wMwYP*w\Bv1BRPT pe\(ME{AZϩd*d}@B<H@hO05ZT*koSu(9E o˭&{aً9MOoȻed HL @޻?EP r/ hSc' :v)S6ruo ȊВP@FF]ιH@5d@ (F 6*1X)P `:cj 4yuB ,6dOPTmOMd"B E RZ5Iy Ϥ ru 4"+B$vAJ'O -a]DlXmwaN89fLd A`: cѡqd:s+[7`N˿/g/MXg;! * ldP8Yd@h"8E`ަ8XAC,"= ^f\ܙ܌BqN!wDS/spㅫ/!>ǰSmdxA <@> @ߛȂg~ԗul~Vd?d|V*d4\2|W$adB8I v|ԑV5A\v ډ׋eTdJKOMA¬G^̟^dcz1KH=xH ^%PY0.Th` 6xadA`08 EpjDJ < kJ5#nSd)[G; aT1F_6>͸8{㥍WK2OP6A ), 5Kd 0HJ @DYH,츹p -$=4`|v OuI$6J_zQ}_є_[dyAD`K @QozV:ySԀ]NǏ̋r9IiVMM}=Ġ23[ޔS?d 4`>`:wyIM6M ($ P{A* y1h) 6)1yKRdAbTC @l ClU;onTF;E=oRt[h8"1dyA  T+dG @.|ݯخ"s>oQ%?w<)bU6R{,!<ar1dd}`#@B ;z{W,˽Xj6c; ^tK3``Jzd=tBd"B(`G`ʐuo]%K;Ao5q<ػ zPH|.( @˄"hvCQ=PDj96鈅SJ 4da  ch`E` kk.¨jݖ2f̨m|$X󘁲u]u67KbЙ +d#8B G`eOs눷 HRaxRG/cVAlaf w(5ک+d@A`6 ]^gY y)9GrpD.K_r\au3y55=5d :d #&BD`G` \7yFU_-8NF~kڴu[*nL^2ݫ8ddk@`0TE`@c=Xҁy.)"o9*Su(YH!5>,8{!:Uф"c|*"H.Qdw@@  J W*D>ռU oCZѬسEiQ@y9\9B&ٛK{Nq]hd@`bBJ`GC:2RI`<sh 2,hᅡYk/Pv,1U:HMA])FdA "8pL @0qQ8f6pHbCTV_? myK O/-?H:4B1dz  p `N sCI;( ?B$@O&5unk5KX~r>P|n+d `0#8`RIՏ3VU6jȵĝ p)]?Ic*1CMEqH$:V'J[rd` #3`I4+%/ ^JS#I %R$B.1uH +h7=P^uW"d@cBA x5kS'AV(ߗ]<~]fa X"Qt"2ˤLy7DjZd #&DH`@ z CΤ"l~E}q֒^&qqg̵=2!Q؅ ݨ}\dK<dzA`? YmW|t2[b#/|д;yN&ZƢUE7*yd@#8B8`G`RR8,0bW " @m7c F:=mK=iRWyv֛`ĩXb")dw 0t`N`=2(~Pw3iS:Qz (vmaFZ|V.5kC%RQ$ʪIdvA #`J`Rmqr>V*Ċ5 7 ikE,r+eg/8aeadwA0lP`Æ"*IŨx4,y:lNEwxʂ+I TXĐ48P9U9NᨹQ!zndA `J`@2TcX޻}S?9Jf8&} ox}R.qkq[\Ķ:=Id"A|E`v] *]fpJ`fS: T4X*kEXǙ,2^J hMV&o.d@X#$B(A 1Wo\%F>9kXŬjPa0Je"vr d5  SadA B`@AI* " |L(͓ %c*d` 8B`@ 5ٗybr_p~ vE&:ئcRyIs*/6DbQ4=>Ӏ'duA #8 I ,c$1* )NW*Pj*Kc 44۳P]ԕ 8eq d @  AD @ƥvîqz+ 6:28 Et[̪[(ڑO>.l8CY$d@#F @iԏ,RUe1ޞ4}|-#%{Ot<;|TVs !d $D @gAoJsYAj.8C U ,"]bFmx]ʏ2fw-uNrdd 4@9`{6kM_q^jtsϱ:0qi|gMMI)kTɢjX}AXQ5z;R8]IdcBTJ`1MMHM 6ܣfOwo @h=ad@#A`G`@`'`DISa1Kw{z ÚbId]^7gzo G m *d@"P`H WRAw%LAME3.93LAME3.93d@(>`dAB C`dlA` ,`C`@d@#AD`d@`"8A@9 d#BAપdi 4TAGSilencepimanQuod Libet Test Data2004mutagen-1.22/tests/data/silence-44-s-v1.mp30000644000175000017500000003533612146763412020473 0ustar lazkalazka00000000000000du=@"`1 @?˼N(|rrDO xKإ2Mw3ŕQv. sR܄$dyA 0#4` ADYa ^O-c7 EbJ!SwukrA%5:=ʹjw0l%RPn@MD] uؕd oA` 0= [[6mes| !)V+ -.Xڤ4ϹR[" !mdj0< =|TVYrK>͎pu1D{-eRa7tfTb)JT*vdkB@0=`PTdZOz SGm=F*=)2gK̘Y +ci7bpynd dA@ `00aU^!62z)}H:]X7NqUlλcjk)h{ech辅*kTd(jA@ 08 {T:TVuw0QlKJ48'ދ}D9<!cW=vEFbJd/z 09 \E7i*of81CQIxD1,Q,<_/m u,V)JȆݕZkU d6SB@ 07 o*ikP)U1u(1wR+TQԠt뻋<޻ j29N؄[Jd>lB`` 5  صĨ3`:4\ȴ"bY Ӟ"0-d/uҥmguΔB(tndEyA 0#4 HG*P̿b7xxrmCMMm*̺t A"(BT.?8EdK`@ : Hm~˩2 NPrYЪ1E叼XRZa饔ƠmV߲-C8غdQz @0P@8 In͠*Y#V$.9sgnF̛?ԺbR\Vko*U(VIi{dXXA@ `0/ ,`rј MvX*<>YLzJ1ץ/;  ]&up+C dbz@@t: :D\=XYzSsWN}x^w]!F U̐FY׭$FЁte Q;1F!-UPYoLdgtA 4 j6VӔ*{۟8^(rm&^HXRFKA"Դð"j#:* dmv 02`̯ar-Xʱ+84ΕT1XqǸQs醘–ݩKe+ ]S*cRduuA@ "- |AuOހom61WϨp9 Xѵ9}$KS<Ӹ啇Bd|`@ 05 iud5'2haµ2$!/&p4  vZc9frdf@ ;`@M93bRqό]͹(u2"*+bխǘ?C\(tsKըV6']9[h;s=PdqA  .`B2ELv6dgA  #&< (krqt))G yN Jlz C)Ňʹ3*79xdrA@3 h(Uƚ,r ET7_۰}L)!!I;Ifw=p^S _do0-amWIn qѭ =gy+0V̪'L9 (ֵHsoZkklh0,T>d@#A9`Y̎\fmy$'@WHbFbVyၢڌqel) g jdcB(G`@z'8jF@, ZY&&BrVI킡g?]}vwo:-[8VJd A`? @1i2iy/9q]UB Lː8bF@SX2r˫w~xyd C`wlx:iL)_d(fcSIKi&=A׳mR_|(ed@B<@?`@t ;L&jVy#3|2mbb6STÙ vm <e(d6>*Ld B\@Hx('p( i^ZÝNF 95 ^A)xf.pnJk*X|ukh}&dj  #P`H`@?l5=F93/`tIi&4͇aQr__{Pd  #8A`4`,1{o:+ile.Yn B)VdV2R.y˸^X-sgʿQd`A:`krFRN;vuHe8C-6*IlxH<+lqL|貔p9rKd@7#$ E`@;}LPF͇ cLcZFVS:Vޛ :4*&⬏m@d@` C`.]P iUM2sT0q  i00+&ctdC4dYhQ" d@B G`Feڻ̿UU |[,aRmj"l 2<7H[2w2Ċ<uJd@@ #8I @N*Ne<}^qG_Q./My};(Bs.&Rd~@ 8N c]Zx)jm6Dle(At: w)SEt4\95h{uOAQ !dmA #8hH @ :IgނYY;* }ܶlIr.;Ww{F=/ =MCdA  #$ I @eG&/j7 (ʚ_](y9 b(.hRS⡋ k>,FXjqPldA 0TJ @&Tִw&igE2l]Hkuu%ԉ45Լx,Yd` "F @NJ(ʞPmUF@*vJC7wl;x`v'.X"eg81d@"8D @.o&Sdɭ"+]"'סyh ...lP)C>AXSjHtXdA#&B$F`@!aκ^Y%ϵC([uB7kCb]Ej)Zy iA&=*+isOd~ DI @R<6N-/ۀ;,?'jYMʃBBŦ^OJViMdA0\J qnat9B QzU .*4Ir9X(Y_ed`  @E s-^8ysK!qVd:@(BR)`7,s8Caf%&\M ҙd@cB$<`R[O*Y+T9<v)iWj+nyuk1朗k8;,kv}?9_ldA $E r>Ol]ڹ~ *zXA׵.JE񖔧LBjԊm,L> G{zM37d}@ p`M`"o㯘wș6sz}ۈ Kf( SIsݼ`vlYA&P]UE.d`#&@ mF m|MmQ7R;A-EkDEڻ5[tjwShCbI8_BdA B @D`@s+;)3gӭD o\kxFaeE`PjtikFiJr hqqZUdAP`J @\t-{onAŚ[Zzw])SL"0_:y?RƤpo%owxRid (@@ ;o-)dy2U zBfQ ԲH׹'Զ0,fG6R̙TTR7d@ BDH @9 0vPW6ANufm( r-x5ZuY#nʽ̎b@dA@ `8L`@၃dbD-|r$$XJP-Yȓ29>IT7bkȏ)J:ud@@$I jA/._Z0XY\J5*ck qΕk73}MUߺǨQ X duA A@H`@=k!g\/d,fg19ԭ ׿td"B @H @/Mc2G~M Pj]4iu`pAPB]1/75AR%Qހ<xUdpA @p@L` pv8b*ӲyÈ NCOϣRc;ooAԤ}8sg^Xj_dA #`?`@#i2Vt2KڅYas$Aık9ʠ)g/0wG_5ta c6d@#B$`F`@U!.=j]| w8[e)l^lIDpUUrӣ+Uѭ35e)r76<d "E @yOm?۟wMwYP*w\Bv1BRPT pe\(ME{AZϩd*d}@B<H@hO05ZT*koSu(9E o˭&{aً9MOoȻed HL @޻?EP r/ hSc' :v)S6ruo ȊВP@FF]ιH@5d@ (F 6*1X)P `:cj 4yuB ,6dOPTmOMd"B E RZ5Iy Ϥ ru 4"+B$vAJ'O -a]DlXmwaN89fLd A`: cѡqd:s+[7`N˿/g/MXg;! * ldP8Yd@h"8E`ަ8XAC,"= ^f\ܙ܌BqN!wDS/spㅫ/!>ǰSmdxA <@> @ߛȂg~ԗul~Vd?d|V*d4\2|W$adB8I v|ԑV5A\v ډ׋eTdJKOMA¬G^̟^dcz1KH=xH ^%PY0.Th` 6xadA`08 EpjDJ < kJ5#nSd)[G; aT1F_6>͸8{㥍WK2OP6A ), 5Kd 0HJ @DYH,츹p -$=4`|v OuI$6J_zQ}_є_[dyAD`K @QozV:ySԀ]NǏ̋r9IiVMM}=Ġ23[ޔS?d 4`>`:wyIM6M ($ P{A* y1h) 6)1yKRdAbTC @l ClU;onTF;E=oRt[h8"1dyA  T+dG @.|ݯخ"s>oQ%?w<)bU6R{,!<ar1dd}`#@B ;z{W,˽Xj6c; ^tK3``Jzd=tBd"B(`G`ʐuo]%K;Ao5q<ػ zPH|.( @˄"hvCQ=PDj96鈅SJ 4da  ch`E` kk.¨jݖ2f̨m|$X󘁲u]u67KbЙ +d#8B G`eOs눷 HRaxRG/cVAlaf w(5ک+d@A`6 ]^gY y)9GrpD.K_r\au3y55=5d :d #&BD`G` \7yFU_-8NF~kڴu[*nL^2ݫ8ddk@`0TE`@c=Xҁy.)"o9*Su(YH!5>,8{!:Uф"c|*"H.Qdw@@  J W*D>ռU oCZѬسEiQ@y9\9B&ٛK{Nq]hd@`bBJ`GC:2RI`<sh 2,hᅡYk/Pv,1U:HMA])FdA "8pL @0qQ8f6pHbCTV_? myK O/-?H:4B1dz  p `N sCI;( ?B$@O&5unk5KX~r>P|n+d `0#8`RIՏ3VU6jȵĝ p)]?Ic*1CMEqH$:V'J[rd` #3`I4+%/ ^JS#I %R$B.1uH +h7=P^uW"d@cBA x5kS'AV(ߗ]<~]fa X"Qt"2ˤLy7DjZd #&DH`@ z CΤ"l~E}q֒^&qqg̵=2!Q؅ ݨ}\dK<dzA`? YmW|t2[b#/|д;yN&ZƢUE7*yd@#8B8`G`RR8,0bW " @m7c F:=mK=iRWyv֛`ĩXb")dw 0t`N`=2(~Pw3iS:Qz (vmaFZ|V.5kC%RQ$ʪIdvA #`J`Rmqr>V*Ċ5 7 ikE,r+eg/8aeadwA0lP`Æ"*IŨx4,y:lNEwxʂ+I TXĐ48P9U9NᨹQ!zndA `J`@2TcX޻}S?9Jf8&} ox}R.qkq[\Ķ:=Id"A|E`v] *]fpJ`fS: T4X*kEXǙ,2^J hMV&o.d@X#$B(A 1Wo\%F>9kXŬjPa0Je"vr d5  SadA B`@AI* " |L(͓ %c*d` 8B`@ 5ٗybr_p~ vE&:ئcRyIs*/6DbQ4=>Ӏ'duA #8 I ,c$1* )NW*Pj*Kc 44۳P]ԕ 8eq d @  AD @ƥvîqz+ 6:28 Et[̪[(ڑO>.l8CY$d@#F @iԏ,RUe1ޞ4}|-#%{Ot<;|TVs !d $D @gAoJsYAj.8C U ,"]bFmx]ʏ2fw-uNrdd 4@9`{6kM_q^jtsϱ:0qi|gMMI)kTɢjX}AXQ5z;R8]IdcBTJ`1MMHM 6ܣfOwo @h=ad@#A`G`@`'`DISa1Kw{z ÚbId]^7gzo G m *d@"P`H WRAw%LAME3.93LAME3.93d@(>`dAB C`dlA` ,`C`@d@#AD`d@`"8A@9 d#BAપdi 4TAGSilencepimanQuod Libet Test Data20042mutagen-1.22/tests/data/issue_29.wma0000644000175000017500000007640012157371172017564 0ustar lazkalazka000000000000000&ufbl@^P WM/PartOfSet1WM/TrackNumber 6/15WM/AlbumTitleLive at VegaWMFSDKVersion10.00.00.3702WM/TrackWM/Lyrics IsVBRWM/MCDIF+96+5CD5+92CF+CFF4+11381+140E3+14CC9+1871C+1E1B1+22E84+2C7A0+31B30+36CA6+3A740+3E8D9+46095WM/Year 2006.WM/MediaPrimaryClassIDN{D1607DBC-E323-4BE2-86A1-48A42A28441E} WM/EncodingTimeWMFSDKNeeded0.0.0.0000ܫG SehR!y!DFc q%+XX_. Seӫ SexFC|K)9>A\.de en-us]&EG_eRů[wHgDL4WM/WMADRCAverageReference-4DeviceConformanceTemplateL1.WM/WMADRCPeakReferencev IsVBRt E˖ ˥r2CiR[ZX++9v`4 ު|O(Uݘ"#DIANEpT@Rц1HARц1HWindows Media Audio 9.1$128 kbps, 44 kHz, stereo 1-pass CBRaܷ Ser@iM[_\D+Pÿa 1aD>9 \99u{F`ɢ 3&ufblr,$Seor Flamingos AdieuKaizers Orchestra6&ufbl N R!y!DFq]s9+ #DHwWrI'{ĒG$tw|SvULJR&IKNHls4qXh?o' Y`i \o{6$s,޸{Szk5NEAJPQ3Rp DJ ̨OVPC4,ƘɬcJE?q%KP-` '  *Y:xb!I=.pXoN!(J2İ )26,Y ~3!z\ mCR/v+kPQM)JPHA@cP1BJ H C 5EJU! L`;yy2"&`5g`i 7 Fv $€vXhSmPX4.@yiv'"t)Mͪ\PPdri=ȚU2p&2ICcBCixʅ<$11İĘ\Y,/587H+goȴIs]oȴIs] 9IM oEsoQ[ Ʃ/J€II~0(Rj,0AnNXTR$"(izbZrŤ.$DTC@"27L ê1AlH2`jLl, Y;$L5L蓦 f@P1©%Ptf[Ai[eMckb4 m"yx]N(X&gM6znto\XZsȫ6 >1 !qg=(IMԛ XJP©8cI 6ƐhcI4ia!mBK HmzM`bitd |AHHlCYbl@lHd$6B`oOE 8,F$"buh,%!<4IDc!F!A6esUefV]ZI._?VVeeծ]4(M mi?KoqR>lo">BiEJ) P_SA(h/H| I5M(Ò @V*STP4Jf&5( C c$(RjA2RJD@L@Q&j 0L' :Idڰ Ĝ2%!5*%;hkRfn0I#BD.eS32 bwK Q i&@L<>!t(zM>7"s} p\p.u21'zdq .6&1C^8 5 !$%8B R6}c⠈I$b \T! !%Bd4pbK(XD$f1,lilxLiF,6m$&6!ZjuX.sU[ĉvUj]qRPRƷK?b+t?~i") XV,(&T PA( B)QRޔHH %;L5$BdJ )QWDj0q Ph1U T, 4İIdX*% A B@$lJ%a IjU FEPa;0("F'CE$n& Q5pLI7 A- $TDgb$( Gb &AHga|v0Z/֔:UBǦ5p->@ H⏆Y G{:IiҔƸ$ObE,NMb>\0xQKmQhiM1|\)Lȩ&А;gH} 4./i+4CAfi%9 dkHO / aF&Œ]AF$'Є"c7>,,e$&CK|lm44$H m< K 6 2SK $GN>!LLdWT(ϳ售Ցu.\sYJYRw9)O6 V"[-l8=ԁo~Aʺ J8|X (@)Z!T"V@ж)V$ ]D$LJdR_ DP X$SE LT"$, U_M+d"H# j! V"CR)E$ Z5j E@ ҄ DPLH!*PᙪiI* PP@?):+L06"A$I"fi) U20B(EIdR$&H1S @@ $$I;km4>r,,yOtpHH,Q$ =}W,S:-2zSc$,G{ҁ֜]+)qΟз(I'i PbN -:P\\X6Q8BԛOYIK9 P%8Q6Pi.㍢'KHXk).6s'҆ǎt,)#ڱHAF6)] p,}]zMT' hB()aUm\ 8.q(Hca\ qK8/ ll,Y~r2"\K|SudD5|x] O~-ƊYB}PxZ| ]ƒ@vr50* A@&@ Қ T&NPE1&J> VZ )uRDR4@~HR TA0 a! RT!& (BJh$!lՉ( ,hjA HR* "J h%M 5 h' ;Ъ+:DK (I ( BJ*0PKHH~ TTJT! )bQPPx22*BEBL]/%*W4я(ٝOr "Hy=5QDN:('lO|7uO9,㊜^ ^N%ԁ9CȽEi&!qpcq"SȈLԚC)(HIe(QPY^4Xj(k\(xB \ceE s ,v+)I Hkc\Kye2Y 6J/ c"De>5]]]]QSx]N#M BhOҘ'Я[񌁤(bo\(Ix] D(R]N*sJ;(9$P.\8Z'E 448Б >PPĂI)Bdeq.s8I BoYCl}_D}K%RI$#ێKf()(jj! ä K"IX A %?j  JCzH!$(M Bt ) Y8%^/6(!Jk0d*0I Iu|wZ֚sܺ4eJ8HR(K/V_VHX& A; Aa&L2 ?BCf2dBeשdۼkRoIE!(CETBI4$(L42D0  ] )ĂF%IQw\\>jԒVV>OBk#u-AH()2KIJDd;ID(_ E@#QHATB @&$@H)k`5nHdH$\"L6:k:;g4"BVЇ@Gi ei9K.M.ܤi))KXLJ& J[)jB$BeԂR)DN]$͒`P D$B 30 "6fF@l3E&][S+eԒ犦er˻K@ 0ؤ: Ut-߂af,"#)wP)j?]K"*PE@ԥ!4@M BP*AuQH (5.eL( mTIJ*A4ij`5$&IU1ԑHCj2L`H' Q_DhɈkcodHdX A $YfՃ"ADR@0` j BHǒivVT|Aml;)0(~t򔭭TUl h AXSE IBjQOI()v )@jQK4"I5DU$Ɋ + JQUMZ @#Ih(!*!"B i`}iI`(*ᔆ(RH "Nt ?0@Ԅ$RA a\ jUUD4 "t I#$hւa[`%Xɛ3+ ̥I.sFo2K$<. 'P>5n+\tXki{[H|Ⱥm|NÈeEh-KtT!(8 m)MQMJM$JU~ո"TbVPBYT>I)I HR%P ~QBb PIXU!!J"RSH! Ib X@! P pZ$5V&% (K"j5PSP((&F)RRP`ZJ$ IJ&BSDI JhXU+>BPeUL&&'j.]k 3yEȦvZc=]7ӉD>M<3iAnӴ9ZkJyֻ<{GS$W+kObw{ޡ6!wME{=k/YG 6P8lm$RQOqM&-> .:NS%FR&LLEQ!DciDQ^X[RCPMt4c㈹‘9i4Gޤ_P}]ss9Ó)B.e(b7)(K!)O>LOI-2|>yIBAN.EM6!2:Ł66,&bllIc[p~rܬE\V3{HK}R j*R[yC!r:]h|mOxHXW&HiwJxlj{K z9  _qG8R"B)l}N.u=PY1$(%"iw{PАD$Mp @VI~s%Z.s1|RU.嬧k(۟2PF o8Oc#>(n8(֛]Gդҗq`rR?$Nma bSޔ^%ue .4Ǟqx>N/4Ƅ7-Uؽt Xs b1g-,wL*4±ORS@NYm5ش ޴J\DQ7')eJ)L9\KAg_VRP|]oQx&Z(K ځC%ֈgmʪ/mYYUQr5% ÏE"a~s ˉ߿|(koQo0>!XC I)i"|y=3L@*"bb5K΅#[>4E /q"sHkˉ.( C _m8*Mt ĂޟPXq" %IO(#(O}(XD)Pbv';)@ĻE=([Q"ÜAu)Bn+.bu q;؃Z|]،*QxڦBM6C>$\ BwH*7ޮBce/"mPZ 'wVhNGZZ[jh 488Q!( aJHIET?TK(}EG|RLQR Y& DTI)%(%ST>OY-$$UT$) E+P!# q4?X҇@J (Mo @)+B])BH%-4TA5p)@M&p 2)AM %aHXBiECJ*QHIZ (2e ҂IUֆ\^5zoYVΆN E)7N.oJ$oDOe(<C#sJyKJҞ7'ޔq7=G Jy{Sҍ4.RҊt\N/_9؁o:z(D|)(IEiOȩ]QLXb"Ki'Ȋ,P5(AT<:b'"q.r+{Իŋ Ki$9 , BEoSWV['yĊGDiO)RS So]glT=%4P;9,XKr)]c&*mr#o<_SD«m /ICO4;>c;"iZT^"S\iP!eD%cY(lk(4ȳUYIr]~۪LI$.﵀iKrqO֟:R?NS)."-o`<jmyJߛcI4?+\yM)(4J_SʊCB)}ƚmσ'cH| J4 (ZO_BhRRPHD_?HM((`%4&RhiU &H|RiI|M  `UbSHI@P!)4!)L%4ԁH&M I@)|hIb3ED"@(0_)AE!I|jJ `aTMP[BIVMD&BT HAJa$R _ Lq%45D*NHDЄP$U00 `bRPړ"4(֚Ҟ8Oޜ]ew(7q@ / x=ÀP:wN+/Q"xSצ([ '\޾D8jὥئvV'"4S/zs%olBw҉D.0OtE ""SأHH\HⴅD>iI#0}p|B^PQbE FƄ='-4QuE-SePz>&.)Jx yuEޭ!PRQ–RzȋJ(% KMM6ӉJzm'/Oб$!C>6$ƚN*N"RODQ `E"Mq-(/PߍrV\%VnVJKAG][+_Kpӥ[-|\-Zqcq oqRͭ?D}o|$h~J_?} VE4II|Bx'jܴ>I~H JI %4Tj+iJU ADi}}RAjIiq,I72|q8*\ q\DqBJJ)m1.IQ87/x֓X%"|JRsJ/_G.lOtm\|BYmsX}hU,P5!ϱef"%rsVEKJBmomSBb?S>i?t<պU+zߚuiI`V4E[{04-жSUP0h/>&܇M B %nE+X$(B(B TaCQ 2 D&i(¨e3CEh! 0$& E!E !  PI &AB҈JMppd [J@ $P0hPAP0"JP ! J2Q PPQ!4 $BS0A)C$2JI[UDI ""jT!% $tQ4b 4AH#:o#:7˱ ?Y=Ri>i$Fo{3OTb؂$ mx.$F1s.qqBโR2E,iĸSRBlp  6YNqYRH'>w[Vbf]NE(ZZ cnBhm\)q[DcۓF{[%~Ki4$J+i/)Z+ DP *$A)5$,]*~(B IJ  , I+&!"@  A & 4$af"AIv)*MD$TJ 2 /eWQ M$ )Z4"eMPH  DR(DA DF̊PH ؔ*e BPp&"Pj  BIATD2 E BDux "1^k -KA e9QQ[0|Qt4[-J?/PR}HROMQYl(_BR¥I)|E @F]s9$i QH%Y,ST{% `,0~ a$%UX+0SBL&%JA IV(E4 `@A "jP L' A $IAHte`I)jCĀ* Ad̢`)Jj"Q aQ c`du,ٹzzr\.w{]wܿ[&[֓K"x|kKYB͹6RdtR !.5h%TB&P "hHaEIAXP(@JP !5 Db @( D 0$ $)OEĒ` % `3 IP@pVܕ݀Z7SKB)㌩HH0_) >5 MAMPCB' " DI5H~d;( d 5a ҟ- KS" $V@A`L)AEY@q$G+/GW׏ ZN~}s]5Z_6+JV5mऄ%b-JJJYQT P(IB&д).f@+hET- mn|_~/҅!2(@/T9Br?5RBC%c`$ЂRTY25DB@L'S @޸sz%ޫa)tpdGSf*T~)BE4JET  '/QMpV"ՠ'#(vuP/ @S-c^xuK.Kȣn-IL" >@&Rj PJIDIxS!|^oT#(mDIIP a X!5($@BevCG^.8^_$')Cl>җ5MI4Pa&`%DJHiBAh/SO@ C䢗ehD JX;(A % -L2KN.~lkIтZm߯K┄N*@Dd  KoZ43x,X_RBRJ4!%@TP@@"#p U.Ƥ~}s$&!?+:Ƿw)|](Jbբ |PB_VT@J BR  (L%աBPA nfqy6[~%moaJjRIX4D,fA(~\!(JCU!'edaPdh{P.w/K\ǎIz} |BWp-t8%ng‚E @CPKQU)RP%VHi)e$LB$$)DT6ET$)Ii$:HDe j&LADEUDRH0DQ&$N]H!>JJ)oBJ(iMH "PhC@ILJIDP M j;d U$M U@ $ %H0$HKUJtA  )0 H04KU"bp)*WSyi.I%++{|-rKԾJ (KҔ%D~LBݹ!ݵ=lOoMK-e+_ķP-Ў0@+T!% (5V%C媩C`IDDJ` @BRMQ0&PRSHJd! R$& P R'#B@T CR Q X ! $!(IPETe LMT!( (R&a3J(h))@$ H)h`DS$UIB5lFi%d@] A,MFMjI$ hL 5J&!ǡlH[viZ+i| ui=Le9OYBׅoE`r>[Z!i4f*>M48ߛx5RP%bB8RjIvi~IB*- `!hPQ@LЄHB# RR&(L@!m`*$q$ԄIIM(,V@EH$KRU S!)dC(L:* I I 1TR$JB`4$AI3edfS )h&R`HXA $ TT AH"c; Lh-2ɏf7 FÁ}nfD$.u앰j-QAJݽ?K:m7>WYGqm)vkB8Z$ۅ8bPnZ JBB(!(JP*SBf"B(~&(~ԪRRS4$$)HHDTJP>Z$TA) 0,@fS0iEI  A"@I@ ajAS.SAM H0 d $?AL (JH&!1#)5Ru(PaɪA2C0"c --n2* C.$'ly4S]Ӟy<7AO_'S:k iL-n5 ^b <7 =<+:g(F]\)Q:Gh*(k>2Qr}\ "* b\O2RFiCaiG\F(EwkO}ridc]iR;2}\M()ȥ1:R$@e)6Q7)K6bt>}J !qb bJ:"S!> 680BU!mci i1< &XyHm, lykR''S9|ecK$9YXo5R. kOݵ4>+yG"Le+yN BNUe^ilgluJM B }M5P hB DKVB%4MT$B&RBKM d (}JH e >@JPc%`5قhE2E!Bd̀CP J+@2M%L5(cI&FRaI!HIMXBB2I$L. T%!2Hh &Ll\ N&wq`8(}O"I靛=3ߞ$ER H~SzxpwLMs1bg<8&]>(SȪ"t/]L,T.!OTQJF&JLhoM7.QmD"iQDDLMwIIO{ [}I4(Т=|L,yR8EN#}yk\DĆ!Xm4IJDyĺC >q {>&HHrcMip$)m1HmDKm"BM< Cmxo.6##Pk5[ PÜR O|(}xJiDj!C SXHa`sj#(."DEK)x|iM542Cly/UK7wwr任w}*oxFպE!m4~t-q)+I[650QZJ@KiL? MI˰ΡVT"5E61(~ӐbȡbFQEC "aiOr8 J FF$Q.7ia A 6C?;[wn%ޫo/k+X [~n~PJR)JI@JRERRMi$ZQBHME}q Pd.q&QƗsl.LxF D~! OE EڐBc4$D"BjJX Q) XSAA=sN'_J/;,O2ж!)64S6ƞWx;̕W$.4̕W$.$"> ߰muc۰>/۫)XSOSRS=Bӈl{>\@ #$G(>n Jƒ)BSL6Z㧉(L %he4 YJ2%mQI(H th %( ?!Jp% $P!@LRj4 `J$PH ҒHi0RjS@mQ5LV)u`P$Q*PS@ b%a0$$e,)C `@PIKKiUA5aD&QBJjU & &B*J Ѩa%E U!PMKwIa{ym\@{y.66Uw"wMRǁ'Ԃi2EKBR$)|}IbudMA"Ϲ^[uIr{mm%K$Jh/ S)~T)֖дn#g/kEoxGSXV3҄$qGѡ Sǂ[ccg0lq>R@[HA VV3#m\IEH|  oZG4SVIIRJ_ (JAH~KMTⲢMA"(}&X,_>aM)`P8E8\i_vy`h iXE)D!-PfI 1Ja( U2@M&J%@A EC `JR SV P1&HdJIH&B&K(EJTX K$BpA3 bKI(0" (&R"V,$ ()I8HJ(-BhI)C4 ƨQYuPQ5e"`HJfH2Z$i! IJXPA0BU5Pjj$Aoʔ% -Nđa&0Ģw'i&/M ,ѐI0I"R:$:P (P $JzdmbhU"a"] TH Haeؘh3zdidLz%8ar y Ac:?mQo;ȀADRIiiO+F,G/|z\70o\ zc} EҊEI.(XQZE)D ZhwQBLu M9I]Zs95bZ:( Xu *Q+%- t[ƛ}}}li'е|O̅".B|YB|l+h"ޏH*IpMĊ&CMER|E c:ajb\\\H]OsyE-%Ѥ4=JJ9ȼZWyLMsbchRqR1"GD!6 D'PR>d6R6.(Zi aF@zk|P4!Q$I"\$>w 0mHK]%iP5’SO O01QF1 (m I4zzec ᤈYbYBņ}OCk.%(hMe7B/0pHBeI$EyigŘd֡qxFIX'vd.Ir\{+%fIrK%G xU+:-F{~x$!1yN j{`,\kU:_%?SL&\w>SL&\v*%N| (hL[ElE@ e#+nSqm~_ǀ/כq ()+\ui4qaQaUM-x`r5ҊQ!TJ_Z8 % JBI`G$C8FI5M%b)LQ%gRJRaR즚PJSU  M&PePP%4D"TJB኱) XL! $ 5"Ñ( &At!XDT M30 I(X !! bSTTKA-IL2 # 4 DTA,HL!3,2Bڳ~@9_\@ȦD`@ "iv@b ΔG ֎)E"u8:SȇҞDWޔ/x.DiR>..44޸R= Fo]վbԓhlb\P>,k|oАQO脈($*2I̅ p*!L}coԆrh\l(S M|i.i 4Pec aI4JWxSמ>2COFIOБE!(2F."Ҝ #\⠾ ҔЅ4 |i> džMf56e.I.椾^̥%ԗK,P[<."'ـnا͸b)G#cwEtogZ\9GЇKSe)|_SBpE)©Th5|*b- PV4$Ut `yB"AAF$%ڑAD*,*BEAR#QQi$P1"" BBB&UbP()50 ij; BjD,fi 4bP )I%CXL R*$id:LO*Lh0BCXJABd HKXDl6 &u1!*1X:l)yxH=҈M] c{ӊ@OR7:r ]i9{fP~h\\ ^x) )}G'зOrgZmD҉0DP0CFV,N鶚 J{ ,D'At6JAObch]HOduqF B36cme|O:S\aL>4iM tR11ИI 4[o>Xr\qy̾'-$Bmqt|dw6$ D#aFLȱK*f\sR'9%3.HzCo8 D֭ߋM|it>X6[֐ oba KK" ۿ[BA|]..>4#JR(Bik`%`24PTNP#B$QEA$I*AT&K51)D1I PEXhiII NJI`I&d4J`LdAHd ڰTn_h"R$eCB[TA`-BPtAbA ޤ *DEnAXw%ޖӽa&n?"zAD=gM7dY7= Şiy<&~EZalOxJ9/8Nn1'E6}S3)\=yR,6Ro&Ȝ h!Q%ņw\)yKP)bM1 0IJBL,hi!1l.P]J QBH1o,|BM. (F5ĄƢ> D i&BmP(\LdCm4Ɠoc# H*S)F! "M<,|xq"w=UJj]jmrIw5.Z3%@׼ )&{-V q;j|!Xn/ ZV-зXʱ*[h?xFc|g_ҐSotԁ"heiH8}Q:Jz8|vAiɴM8Τn*= 4H-. A _:PS<ScG_BAZsȭiDҍ0B…wRyĹĂ]y &E6S=HQ_(Ms+P$R}SDJ 9ҎЄ8E8E^zƅ&Xe=hn+k 2xbe=I!&!a#(M4..e 6%1$Ѿ&MIY(K89VP.ξu2&Q%!җ؆(ꊐQ6ILE8]UB|dmq6_XH>'r؛GyBE)t_8P M$U!d}l&clCh9a)Km&41&68b\!%İ)}c,@lci0<1$΢TXO"\CaFƆ1pI`BEޏR(I1! ۀI$ pBA?K%r@!}b}r-[eI-w.pX!E n!# B_U'>Q8ϑOK{e*Kݞ7EM4! hLSItXC#0J/$qa߭RPIB PDoLVhIÒ])uT&!!gT@)@B$ 2La U d 5R*LHDe0& !*_*$Aa P $4! L* Q{k* 6$-, L,!.0D0"I` 1`=G"EQ.aESbѮ>4cC\ I I Me-AA11e $y 4I/1)e rŠŔWLb&]'xid&R%4(YW4rMRw&MRw&jY[O+gk9JP;r+6Zf}nZntB )%[H-IXIڄ))@!M 'BqA zABP$ —m@  SM4X!!>4JV`P$f[RX ;V pJSQ)jL:Y@!@4 T@IL!ia`52  J$ @B0`5HaU$!fHI* 0 A!-ٖ-Z钁  #}br@tr.t oҋ :ᵜ)bv#. <MkWEs$P'}\](닥QěuW\D8t ->/RN৯SF&RIi DC\URcaD"$Q8czH1 |o\aDP)@I(КWe w Fcm2q5ĂBM hm&,!bcyli "mY )KFcb 4]`)c(Un>/>M \]ܗ;|"Mjj(y[?V0Z@l O?K)vJE hM)CDjIvXSM M)CH$ "-!j HH5%4R*B@@D@00TP ȔJ &FdH&jBPB0&'aj"J hHJ'FD $*LI%LPLAY ]s9z E,6A@CeUɝD7%A7KV5OdQZfߚEhSn}!@kN"`?5P9CO"HD~i|iMG߲OxɃp-!T4Ӻ 4P$@JCBQEJ*4%X1XL%1@FFE :0"TdU0Th I$lLBIa"b%DM@`VB 22dY@&,3u2X Ip4D(@#9xUw)WƮa!?q wBO'lVK^l"|-Z-3x_Sj١!!U XQK,@AB MnfJ@}B**"?y$ o%$`IbLU!! @`. Hh@ai3Y$2BAљ&` a MXdj%D2HQ,2A]kIH(. (kU-D*R 6 D$\Iu@U$A#ѐUBnW T"FOM*iBM2~rKgɔi>oGכM0V.)t_$ &$4,$PB¢jծPB_%5@" 2ڬca ,T 70eI 7TҔ(,H$2&3)`0fdEP@"bP H2D1p1MD0n; Ll!; HXkXcw !-;uPd4;0nlQ/&T]ɩ>RNVKK KFŸ$m {:D\."e T&q֐۸llX*8?VFPƚJ([*%4XOPS+@eե%h-訐_@HIi #Z@e Q`#u]dPQR)8$ RBh!$Cj () 0HLd Jm@H0C A $VE0$I&JeEDU)ԑ 0 n*nHYʢbB$OwHAVjD _$ ZԈ P@$\$aahBXN*a!L4vi$+=(Wbtojcv3[e뼫 ʻ]˹|BVI"jeh#>UKZt n|Mg*Ge)|-i?T+ ]Hj*aJfdktH X%HH j kAB L# ilApY$%(@ TIDABST 2LR/,)$Y KQVhib$'c21 RD" 6 .-$[܅@nͱETzJ>",?o KDV> 2ȣ(~%cX75k"OսiE)XR 4A Q }Y"%$*ab)IZªU|OEY@ Q T* 8B(DJ&J_$ ha:dITH""T$ LZIHX& -j! %jNౄw,;\ֆ &74 Ң6 !:ct[͇ +q.ܝ׊J&dz]B\>AnBm\4 OMSlwCb]k(A/`qJ_TZ1)"AAA`jHVmDPȃ0ZF ɤU0ۚ!E@k- "Ymutagen-1.22/tests/data/ooming-header.flac0000644000175000017500000000012612157371172020751 0ustar lazkalazka00000000000000fLaC"B B]{&VC1( reference libFLAC 1.2.0 20070715%nmutagen-1.22/tests/data/brokentag.apev20000644000175000017500000004013412146763412020322 0ustar lazkalazka00000000000000APETAGEX0000000000000000APETAGEXxTrack07ArtistAnArtist TitleSome Music AlbumA test caseAPETAGEXxmutagen-1.22/tests/data/mac-396.ape0000644000175000017500000000015012146763412017147 0ustar lazkalazka00000000000000MAC xD,zRIFF$ WAVEfmt Ddata Xmutagen-1.22/tests/data/apev2-lyricsv2.mp30000644000175000017500000014135212146763412020624 0ustar lazkalazka00000000000000ID3 vTIT2 A song PRIV'WM/MediaClassPrimaryID}`#KH*(DPRIV)WM/MediaClassSecondaryIDTCON35PRIVPeakValuePRIVAverageLevel?TPE1AuthlcCInfoMDg !$&)+.0358:=?BEHKMPRUWZ\_adfiknpsuxz}^f2uqյZHQ9~;?~ʖJ󚪾kWlY^f[Uj_-[TI b>J%7]0\kJi6!LP:S 1ad`*b"q`~4₁C D} bZo^a l7~ME@bhhbj94.A+LL5]^zDdSk@#%jPP^:7 (O#۝Qmߓx?_ 㙽wul{g/'Ue=M3"K٠!A`01|b01k:>,8F2B00 0 %C#B`/^Nt)<0Q>f-#@Ҩ CÏ8◵&hOVptZw, #bQ,AhT^&g7ּabn>V6Q]!*20 񨈄(Ra80BUF)Y9.G*a q ^&#~N"NG/8Bdh ɔTSb{Le7!Jfn5k'$)j4*sd ~VZ۟;[O6{/u K7VW66 $e7dqAc59s-i*է=gyI3/C)-sa_wFuL֋iGɺ2odF'ާ{[/ی`2 hh p`d@cN8aVF@P$P2~IUqFbRHD Q D/k)1ZO{&F]}d5];ʚSQp[W8I-I6#sw2k%[^s/ ovېul+nMKB_hX2a,A`Yʝ>wY?~^ack6, LU-\ܯ(O7Çp& [ry|M#ZcJ`얂\BEĿ{sr8;)8L02Ň! #2izq(0y@0K !N1!@p ?wobx~ʹ;JF 'wnB-gWȻ0_fKNgݕ{2L1af @0 xxf Ac!F% 23n d2x!ߴn ]qxƥ2ý %H"JW鸞rrk3{E\;dK|42Z.]UdsJ>4)wsꤎh\lW1xM Dd쨐h#jX2{#@x꺋4QMkGTJ Y);!5}I)!U چ"ȉضɴY潣تj*Z؋Lw,L*Dthő dF֫lqJ# SA;R@ @H$<&8L0j `H\Sk$V1a$H[0jhҤ($]BțF\[ #>ɤhMkkup5ESz5$6Sb^'4/@= ld5[m>.EK5@T{T8 EK>Q,Hrƣ=Pm4?^Fki2OP[\Mx7r^"ҨR schn8QS :fov2@Y50Sh Q0>rfY<"SNrWBc02Hy"X2}qfeE7_&?K0$M8VV0E DzAN)BzS2mnslU,NqjJ+˶81MITwn{'-Cml%|M5r`hOX=4-) EL r pIxlw zh D.fK3R$|#J߰M%u띤9,w<1s`N82mx^Ȯ–[P\I*:$F1f/@&d k6dgL#hp'Ņ77y*vܓ(tTǛT)*>FOpU單SX5sVVՁ;NGe*vNLPrY+oQǛZűƺ{aOhtKx덋B7झPST ?UA 2nj~ỵ́QL(u9u@pE ;~IkwoǾ$%e 5-HTKXoٖ@q}B $Jm~#d|8m Vj`>B#52Nl& dԹUZ3bL2tj"ٳc{5vzyt<-28 1pf h,ЀjyȈ;:lM%^~̛Te=gw5gϯzz{_)'kqߴc[*ViAAA΂2<2l2G6`3>2p00/zjIlMs'nEg٭9M8|:yT6*Ѧ@dMUOUԒ~7Rr'NcԂU)Z5KUN+e^RIڪvuWhY~z۝{ʒKs]l? v y E_hX2,'ɒbG)_zvLE4Z# mC܏-#ᙄc%JZMw?o(2Z x~5Bywg'[?Q'O.ϋ|\$cJތm[lI7\gD`F@ꀏBA( ;3 PZTv]^ELj9vөY xN  ÓLjY '5AC x<). "|V㝍8U38W'8Dp<|4B!Er.ݵͽVPKAuۥ^}2H Vs^4hdj1Uj>[~B."|fU(W/Oݯ8MU~6[gSi5pyYFt*Ry͞r$t*2p0C0+0@$0$$ `L0N5I.5_ ÏԸ!) (QXh5j*@aC2.^_ 5`w bcR5Hܢ'%L>|*wz۳DI1?mڲkۏ{\ûlbqM ^Bra!PxX2.Չ{"AQ=m랉kxqqUlwQ*LEf.??jMSj36=S^I"WPyٛ$f4/ _cIrU^]Jv>5;ud~a~[ h{\<-#aKBfKQйiTޟb1Д+')Yd8/3rA^t=&ޖI#aF15lfr2Ix:ڠP] 5 ~ .P:kP`M"#HyNO0PeZ xP  /wd?tp [-˪ՆrhouP*Oh6ONj()߮Vi~9~Cya*{emΚSKxƷ5=% !B$+̵" x;Q2cڧlF3n̋}JRd,P#y4.ɆE@yw>{җsV,Jj,_Vu]r|uurە :KdB%#Ua-J~˺9D(yiG//;9؁4BrqՓLܠœ8xEFZX۞R"3(ٮSa0=*6Wr ޯD@p`82: O͎\ 0p[ #dH8`<)}-4@ƶ) 6#'Ns!;0bqJ|YėBmwzG̒55#%ْͨHW=8Z: "/GE:$|ՍuҲf. = 3ClFs 4e P#nX2ArAc'4uuwqh#!;JJ*_kp+g_/t3 ڔf}u}{^#v]"w?}?3J;QJU&8ap1`yB2 F:\t!& g2;l$zݧuɞh$c`F'6  Hp*1\H/g;kK% 5W}u.@@@؜BoD5׬) Ԑ02V&[^oc}ًzIf=>tChz _a֥iVbbEeHIګݦS¿FYlW"wLDf,i  41ɇbE)NQ>&P* vu]hZ%C>)*EB]b\1֭ko +4N> A!!a:R*=ZS}:z!Arдr HeQ@X$.)(Bbz`&dKqrld1$8A VFb\U6ː,L9LᄥŭK:ii9)1$$.m&]z佶XMHNc;Vw$]tewl/(w FRa爡P#]4.ՙ'@8&N H@nEe Czj^EneDEe X&m[e8?dRĚ..jm<\@ȱp\Oʧ@DUIbˁf-d:I6yQ:i HiL1ωdz0/$ENZI )`V 0@ڥHtXHIuZ}>}r$=iCc͡gzndQ'G *(fbxu4AK =W9}Xq ,X .- `a `يa@a4s+beJ,f@\0ۮʢi6nsFsY%Z(j.wҟi|(Y6YLu!q9*.Xő[ 0鷚aekqEclW>Y{e~ebjl</x̃}JBeJȐQ4W4.@xe{HAgbtUJ)8疞e6S܁Mw!0<"g}pptU7W"#[̌I>#2l~ .Ǧ ^%ykXi0$:c` C!@p}:G33~ɠ8p4 56D܄M2SH.e#"s,+aj4l.E="dH}"m**F >JJ^BK\h%RJӭ!|rN*IY$ػ) EGqOkl}YqoV0$Xgzʲ{xtiSV8#gO7[<%2UQfEHX&=Q3 ND}iO#>s3e#".d܈%VY:H/%*4O/W>oե1LSW>gewz-/ )>7%Ba8+A0 #]Pa,S4-Ɖ.:x&wÒquC-LrǨ֠nVE&mi4)dE3N02(h2ZmwE~5, K<$b%$AC.Hq;e4Ecyޅ/y[3r:) ۡ#8eee\vK߰Ȥ}8P9Dn7?nsF,J  geh^b .a0 ˼_1`rC>_*p(8@EW6*If^¢# 2arYʍYf'i.kжSj$4@n@:HUdkzϭYO,=Y~92H9`Po=Q49Y}ly yM4\ h/X2AEGzew4>ͧy }3t͡CBO DxYrӳ\{29%KN v<5"vB׀+2޳1/#x@U}a_?< ̼) S £#b'oEfHL#ӖO?G=ৗE^Bg E(@FVY1])[fgpb pXTG ZgG gh!dh;Np=}2)1xy?5F:0 ,3טHz 6"*RIy( u'*E@mB3 {O]QiDBiB%q5kLOuC IkSqMg6K 7BiZ)Zf~ۄ C1\G4pK=\s`Clf 5_AhoXٿ2Ɋ"EyK@BtJT-qR}_cХ;n{\);%y2;wM0˙Sm)t2}9ׂӼ"7~FS9:/}N1ϝ'$@A2YR@ @0E[Nv#L ! p@ҨT }%6oHjՎLdځJ(bFMJ$PRf )4-4;YҒZuROx$‘.{ (*Y D RFH((CYX =qk78Ff|i;odّb顛# 8#^EQfC6l'JTXON\T*̢Er ::l| E`xP#Y!4/m IE_5-piٔVLWJIa*}j- -bp{)cY'/t,kTWC~",\5yBb@C~:(bpbfj`B|.Rڰ1OoGK!:&E]Q1J,kB#>k LSS%E)EG"dUfՃ}Rm3Nvօ_l$* ƊveDAkJ-Qdx`IU3+ȉJ7kh@ݛ=/&AmoMv>˶?{KدޖR]^Sl5ŎνCC_jW[rB.jco{ `16 !@q9٫9)bA(_Z yXR*R^ܸPԂi͜V\I7\ĉ)T+-,;'~^qk)s9E'эF2 ݳ*W锝lZli .'-qj+~qosH1 S R0}1=leW>xcfzi}Blv̋]JBe-HP#~XY4AYs)[ʆhH<8T -#SdU.]PԃY)JSEoFx. J&BU=H8 P]ږuNڷỉeF;+- #1 0p7Z"rfQB! ٧D `DS=&Dk$}ЕM4&I8u_e0͡MSळ}@/m7J'(5bP]v/ݭrzPN@0 :ҮLj J٨Zg',<9+GG~D6S83@Ct~1> c9!FDHr1׌ld2t4" +*ȪB IDfJϋN{yִ]f<Ӱx"0iMcv NFt˦q*VIK]E/-]cjD^kSbuIe%IQ6i4֌2C qԷ||%hJlx}2bcʸP#4.- $Eg?Kqb#_Xc-FW@2Te*m;_Lv[1g"=⓵= C ,O`f`p(RppDn1\0pDaKo"MG09J(f w7.«Yn+gаRduRT-Ժ JKlXi a+J0{ Yf+):ۮ? 7-kCEL̼F['܍H=E"v|Df"̙/zl͘h$u8NIR VDW,a 1YY0V4 BǑa*.Jâ:0a  1WHEPDh\\ J8ݸ*tc79LaiMt,2h%n.M4px}6eaJxt9 A#Nmi,] 0fQ.= ؜<> D%XG$\FU 4lb)yK2aJP" 4- ɐ"AC IIU|ETxkP_}4PSLeh"9=l]푦HNŕ드Ϟ)!z!z1VAJwL$|}(&몦cv (Aa tP45du$`vX@2j`RMA+URM8fۋS-38檴M b&LJݔٹTcɦj, ˆb6 t$јsE[7饴c$?{ܿY/;vvL@F=e\+8  7,2,Zs`9cIA@c] m!(K 1jRVW\J47RHJ[qp.9es"bǮ!aͱr▉QwJ(v*nʧX޻6 en[l-|̋4aH8P#X)4.0I"E (71HrSG>KH[};|5IPv҄_6NYLc5"ɦ]2V/ӋWUކLhf™.oXhJEj:q65h`_T( =0@:Ie 0x.w(IGly,6DyVQd3b'h erm \D_&ɳؕ=r`ܠ$vH1yj)M9ǔ0{ƍc9U"V̻6*&@ 'ï]cjϱZĕU얬T'daL:rK}33} ߕs,y1YM|9Zף)Xowϫowyap%7uL B(a"5-O\S 0=0@}0i#>00H0dm a XCku{+6\VЕ@m1=$ӊmt䛚VrL qb}Bvpg]Tm'7 SgJ^g]un9lh,{LEbci X4 y)bAo݃bۄ^͓ǶXv~Hefjmp,)1 ngl' c=C?5n:律Yn5r2?x.QXD(200D30 0082V5/2p1wH39LlKjAj'`KˢDdOkk ZO azJnj?/rCXbtS&OqEHTjL0X|A$5H8.dt`C2]ڨvfDzFJznnix훌l.[Ru]|^$W) [.S}:#sBVVgz@~G'Rkk?zѱouZG?I3%βL*ֽ@\^R G?KA0XR -!.mk LV1bgH(W'`t\QBu sg@Lur1=oii[M#$VOk{ 3V+j?myZ^sN3y?51iQ-]ٓWtV6t<_{^l w Dcixh#~Xu4ɉA)RV+{+dC V>zDnMve"O_߇zY2kb}~'&I.[:};9._뿻0H{" Y{?)6&{17ZQ&gx88C7Č14P` P s„Y4 /ԐLsIE&ÆWU& `3H dlPMieF<&V"Ion0Rهui.t(m;YR%GVfq26{$IXbB$MU-Y[U-b=Ǿ.|޽w/QOۚ-@pROOi}z~~S0 嗢ai9gM~-~pq3pB[Өoz绺9^bkgxμNGP-}"s\OQfaEB^}(pӄIs*rSLz2o=留e7Zւ!ˌNKeYd!<Dr ] hk 5V!@©:{!Z#:upaySzҵ*ql\>v K%N؈R5 rC=#VO + ,fA+qHuk35j%j-$*绱bYHk7m}m,k@uͼ_eV0wQrBqSZz5h5eˊSa9JMB(q˕$,-|f}=\"K)oMws8{p#iL $@ $ *\\NkS驇b ,<20#%H7F``XAe;e~ J-K̯v}quJNޓr0(eIT4*tؒ8om{NTe(E ٜN/KcNˆ`^>z"ŵ}J9S.Xk?>͍)78W"R6Ow6_iRO͹,yӴQ ̓:D`q( uiG>_&F`0!(  Iuil&"@&Ga|I1feS)&O@`52:)mq#DM!XIgmO ez*Ci˺hXiyFp~r*<  g3lY4vL\JBeQ4 u)I("@x nܪpF{ho|o[ ZLEP,O1)xWf-3dgtÕ(2c#dk}q*tN2.2TpӦ2ΔRd@d@F5&&9F*.Op̖{n2j!@#9P~ZǭAfUE*T:)VFaoQ&&Ҿ6ΌfqP*KPf`[h%^9b.V[3#ǏX"gK7 ^Ee1(9Fl.7dMN(⑲1CDlG,lާ8z)6K20WdMd"+A]ڊgmVF(1 /*аLNrHQmCEA0ؕY3MىM=1DU:X>éfcjLw1WVtdgNEƔnh_zFk&eeY֜OvS͞8IxBJjzpk/lY1"|LDeP"2-)I"AG7Te^o9|P^!7C5_֢S rv)Ldvio}CCFLe#~fѳHϝbPnW@3.1< M :eF/0 e [Ǖ;>qVw\!4 I, L-K2ǒ(&Hө[-:Ii2F+VFȰ8bH3Iw؂a-9bvid4!À+ p0tC2ZBk nmƀВ3e]F]ՇjڋWNJ/yu Җ`< FR9#âRNܒfU5!O?i$E;L h):[J8KMe2<`բ 1ʑjo,Etߥl)v̋EbJhP#)4 u ɄE@iL浞uJxij[ V'4ZW *Lu!`h]Dpʫ% gb3v~ͱ:SٸSl#6C?R>s|W1e@a8*/c2yBH;2%Q˱[8'8,`}Gb-Ik*LGrGxSF4zMf*8ID39 >P 8*h'4o$h;:NQT Xe1W55\ZEBTig8.AL>0 jkM ~ LiJRN>\r7/" )!v;nVs+:SH{=W#seҥ8s̼{ {]YȭSG:,A@P3o1B{ >0 $0mU VLAE{^A}HΡSrFAGAeeVڦf8壋Q|aOS>349*E]%)w*{"] V78%(V^{l~%rMDe,Q&4-u)I#A=^PM]qnpY$~E4u^i\Gb&4\6z֕*OzٞFDEFovChB޽-K*fv,zArn@qp4et  hg"xrB$^ @I@ [FvFL bD)0red bU "'+(I*UGb&e9!$(F.M(4]%˹XLpa<|RT&13='xeM$]E##Wb'YR=~}S=eڇit]L~xl "?/oF8˱ޜ[H.zQk>]H ZЖHf 5ƹԧ&3"'fv^U krv,t$ dB HVZm,ۛ&k2$WM[PeD@Z(js E(cb[NY;n3MGI]ܴ:`I\7۞N-M9z"ElJsM+Eba(QBX4.- +bEit]tW>!v kZCM@d)0yu?9}&DkI|S';|=jxu[L϶>[%"W~l8fI@``( ` ɈA!pX9ÜܧpL J0 $ X1TUED0DI!6!LŒnE%*] _GAGhCtABTs͒Jn] ˍiAǕ@pqvS J LACb%MtA]DICWHU!XvoF)x%6G߫2|Ile-"B=}YM,e#5/Nݕ N>[WHǛW}]w,/wn[_]v&oB/sC @LUOd qfDS3ADU g rċÂ"YOI?Ny qA PZ.~w}7A39`'j2usD "/Cnn7vMxrReEl͙mM CbhQ&X2bAG n=dd Ǝd\!Qwg~52nj79ܲN)Ho'?=L|/k/f{/کmսb#xw{ 3i{͂T* 5$@A$tX0d0@3 Q0 S4BeVVelV񛀣μ&X ) LN)uEW MficϣM)'+sNI$!]8XM~z? ,E$l5nh ^zVlӅ{([o=}zG6Lgߙ]|(>Zxrq|n*ny/}N5hw[W-L.g೮-$}oA{݇H 5~NP$DnI|7kxeQg @aѰ<12okE]ri\{H~0 ijȅÌZg2aa$v1/ ];G6ܥ5a(\݇k&Hmwn(T0`O|2|$IASC֨] ۋ`,Dli z̋5^!(PoX4.%~"E<)2'يQrSDl@{FJQ^oޖwCK;+/hnyJ"S콐2 {t9kD[6Ľ gK$kQ8 n oJNt(L ".fhNzf, 7*2A12fr,k F9JڛcʹjM '+Cj9Vwb uF%(1sVHޔo^Lzwparb35.S#eVVWKMkAlwR#Ut67oʧqt7#JH9kQr5_^u5}%w2WOO)}O=qnմև[9,YΗWZRx_d1 D`0& (0B m3n̆1`*D F 90+hOAfʰ "+Z7 mT?$%1)TM`b(/n2zkâtՠQ;!:oVD!Tar)Stk2@Ho !y5 j *eǧ__.Z.Ω0cqkA]ũb|rCO*} RQIcxW kIJil":zLDe8P#XE4.I"Aȡ2R"fiJo3IaT) wp.QyoE4m4|*~~ z*=?=ƆP2XG g|~[W{on{;SbC`W, h hd*Ta4YJ%SN<..& `t$Sݭ\ERDĴ%(2Tv=1nm'bP$ګᎬn ՒIPlh `ݱ0qigoh5k zYS`T?Q}J'_?c?7 0S, " 4(bsF$tg)b@XTX(iEJJ:a)Rq*"lRZS%XLq3YNqߘ^Ys^r*=dWaEDx\ɲɝ+J6#FQZJGF j+H8@f{@ist- s6Z`B 82<=UЙT"Pe4M3<` `iSs5CNӤ/ĕڻrM&hhY)S^eəHıtb˲B@.yr_)9T Y|lJfzMiEra*P#X2-ɋ%U@9m#T\S3ˊE h-5#ւZqijb̔NoRΏS;g"<|kepV>C 潼0:ݿb:m}I3~V]O n1T!U%6.p0*xQ<ω3 `40(ɦXPG'NKb`Ĉld0'qmE`CH!L 9&Qb)uw52Q1Tgbs`,gdIA Ac1oJ(}kw)/YNɻ,R_=]*$?']i*Ȩ-d32,ϙM3!= U&$ubjt; [rD1S{ͭO74h.qf` iEa)A$Dq}mehP n`"l( HTPM;k2L* qO'&"޿5K'e0fv4q5ǁY(Iu=5E;jݪ#)=[wZ8+: "lQ'|LEc蘑P#~XE2ɁA@4qwh)L6Ub{*Β TAQ MLKji*}H؂u#ءVL)i}|&emsCVRm̓C72n'/}ʮk4[m,[F?ꔽV(F\".8 Y.ꩣ@!T{~3(T@2̷4m 2 C݋oK)q%8ANSL4i{^vzn # ӓ1n|"F-5"P֯޳sJȦ-nw[@ ?Aj'g8<"O ,ڃQ wu& ^8žΟ}/Zw\۵-JX&ch=t TyKq vp[0FvnADdc_H5:6j0t!m868ò8i&G.3쪢 Q!-.jPo `BebTӰb@?,q^UyM'g0Um eJՊq *LS xeUXj\l?x̋Er`!hoX8Nm+ "A} 'ʐ=76+!`C7M"z&z˪̡Zޑo #mpZq/WlŞi_:`j$ncl1 f 41xG=``@aaa:i?KEF Ph /@k LpQ؛P$~bLC^ ,eI=Ǘޡ9RxzH5frK2yb L:BM.1:uqc]!%6iŤe&S5 {BFM#>"/Z. fVS x,}v]:%Ǝ pb>AWBak"`fO:SR)tћqfb|gΥ9|0 ˵<,2W!(2uDXe"a$DGަXSJM. /$nI %YWsDqD B7;TBIBgro1S@´z9X>&سӶbܓ^-W*B}в|ZYm^oybzʮsUl2~̃D_8P#XA2,'bA)s-Tuþ3 2YK!I[d7ȩP3)@:v½Rv9_oɦރcpgkVjFhB߱Ύ*1Sx4` T*dpi&wl1F\K LFoYc\G<IBAT2q8`VSК r4egAi7t{m.ͱVS:]* !qhq'#TFCHb+Zz؝UDN'bcߺb>AM?>VJ#,!N0Zt+/=mrP ZhZdPv0Bm8͏jRQkMXhGO{2o9JnCCDcP 4 y+_42J@>lm yLEbaGi X}6Mq)}"A{o}xCxfml'ؿ M y{;wewwh)CR\"ef'&}N^u윎v,af57zꂧeo߭L=aU`WʘFH`DS #Dad00t0D F 4J M)"HP|F.H謞d au׼*&gWs$)hiO±-'EH\F7IkMzlP0?ZMcjM%WzU_BtTry>OlC7  fM6dLHq%P[g2.0fayTQ !Eva:#yf(IKq;d#6;u?F˃gD3#mzM{^5\O0H"*) fF 3 3.T:mme$ZfSV} ҹlff8 U_qo%d% =*%bI] ͵uu*-ėI0-+^jr*:U _4[rzmQ"M0RMc92 nbH#wlaF?n̓]JRf,P#Y4ɖ.OZݪdu{|F~*}I[ye*OfhH"5P+|\X)ڒ:wXSn֝s-U3H*TxXQSKjG XAlVMt${B6DE),,* o Lӈ D 3VCcŘk+W]amD\V d&@73[ (HEQЬk3D3IV&]u>PSY^UK.tl_+c2t&͓5. Dwj0|LZ{}S^K} 3ꡫTT- ,`G>Ǿ/ͽӿNGǹ"F~LXj 8a鏈'ZaDAی^Mv-5ZfJ/hjMqX;HJD.a+DɤmWGl:fm~O"- ;ktR@,|M ײYPډ&3 fƫk$dp67XQ5V>yF=үl̥ ^v5ބղSZ#'ʪL;}˟[K[x~[TSܧmѾo]ozV[/4vǾ.Ց@TW",ÐlSAq@rh2D8q"81 1# Aa}\2;N QN(QU& F`F o*raaUۂlBRNma wέn1:;ew6~KQ-EwB5ԱSiQOW!-ҺݵA)19l&yME\h,X2Ʌ"A)KmVď܆hA|ָj!5"?1v!ܬ {BvcFc'&l;ry^jcY?ud&PzT盟K{x*IiGKT-ZjVȩ:k<̛.]H$Y3^ZDA8,e&L\ E(j!+2*0;o ^0h`@QLyIAEBH07pfl :a3rԝJ)^EvHY )tWKa^dhi$MXۑSUTVʤd5X҇L:,,bSax@aʬTؙ"IA ٚu]ۮ49i\|ܠ'4YR˯GUY6c_EH&V;nA--%yJlJyG`Aa)htԣ"KxEiȃ%fs/Q.`e ptLq #? l Nv ErbhP#Xy2.1+"A@0GC PμKK]$ejJQdVEؖ q2Pb3p6JH(HEdMSTğ0" ڴVrė+jٱ۝:Z]}7?Nj8t,&J$4$Lͮ d$D2 D}T^N;?L8k.Iܱ5"MdD& *qLH t'If C;Fz9ZZ |{B(: 0%]JL=DpXA0( G1TljB{s+Ľ-bQ> wsӤ+]Apq}ҲU;A2T2SgZZH0#aMj v -,ygX( ,z4EI-B.iP@MԐAY4!0~5OACL: 9Ƞ@@@HIHVeغΐ ݮDPԾx(Zx6&(=HD$rRZ84;-'fb4K+E-IOc@RBpzqɥ=MvzD "rK=gY-L8ml xM iE_ȑP#4N%"E@f[ywggZqJεRdOdAdF g ,gȵBD֪MM*vDZNk4Q&ƦOXu%'7hӜ1d𢳃ќ58|Rpq;85*-gm^6/Q .dլr(! d9Mu_;XI*CzTZ>ؖtEsߛ2W5Ԕ~jG/YgWt;~ÉQp0a$<N4L2#M3BP„ Ya\.N*XV:<.@BfE$'L1P5XM6eh&8;m(*e ^l#`h7n{m? agյ,ݥROs!ݕupBpl1 FP` %7XkDAL҄Bu>{qX d\NUpW\ S ,sMIJEM=-Б'J 4t$*=Jl*QF@Q \|LJ/2% q86ҬG6>K3=[{alt$sM F_Hؐi 2N%ɑ*Etܿr94WMiX&Okk|3+kw(37sf&5T#J9':ezƚܜޕt:rg'6 x9 N\ާgsXAiqK5"&R+1a(=Is A$0s{  5/!s?򷙖b-QBRiM6`ePPL(6:6fh̍la4GRյsj*@s>I'%T$ .Oֹ#LRjR'۟SNO))awkW׻T3ZnTyZW)DŽs7Y!nkc=Z5f}=!Ls_{;.ૡ^0u'M~Ok׮LZ8}1e`d(08ÝMPNna9Ww,1x䡈9acmƓ6dG41d.dI2 f$bTfd iPfOf>J+(7(B:*QP*]ZR !l&2=2b*@bk-_X"G7*fgF[; 2*FO(_֜^,|D([>hvjj *JC= Vb1H(0V1q1 gDąFh22( F@M3.Frz&re)9M#"oTU9=rݧ&ב@cOZ7G{=hs0=ISui4v?+z9l|.ỹ5cii# XE2Mu(xE®+rwn]W*o34Bfϧk)1χ?WdSisk.2GNӴi}:T=o,ww|w>jLv5 z&A (  ͙ L&.0rX3R3迯;ֱU҄MMj[?Jyb]k):7 dlNxcvUD^ h. >}\֨8ē~ %Rd3rN6gus+нimwID^1OX$mh9@JuZyA&mGC_?Pτyׯ/ >g'v[y,dg6I %߇5?#=snGp[3Z~'tM2/ $ P T r jH:"h`4R:; a-?BqZj=`$uiIUdcdMI0@œ*6IA 4=2e2媮ԶJNt(tpN jS'6詷pN /꺿S@g׌{l@ >z̃GBei u01/IA@?=;WQVk d]c"PJt(;<e4M:|Xɕr{sXyxq{1ZdnR1Ӽw1e 1+L xXLcy& 5AXIna@QR0Y[Tq DdL^9o[y))k6ʴs{ʠVړ?F >@t]&GNRG鬼4"̘޺1fC fhIDI/4,PSRJ^v)bй7s2sn} n|"1Ցc Hb/ [}A[33ؐ#1P|4XHkPY7 P*Yq$'S!n:9\!V6#8,喼7ᖉXQ^BpCdRB& &GYLՏ$_j>6>{*V+sziFZQd=ĸȒ;C]U9'/u:wnĜ?sdb_6Qfa"ZBtX3D]?6Գe% Hϱ$=nϼcA4bS(zɁ+y.@#Ud:maPXt[Pu'Dqqx0:$ gI,c2L3 CŮSMn_.}#e {YJCSƧrBT\i $3Wlf6y̋JdP#~2.%~(bU@id̓vIȳ+)$QARF)L'm>'$3>E0TdNt(^9ehO0nU |}žcvP?ptvokcB )X:6@A`9 P`G.;*\` dZ+ É9IvRǬNJ1yp&‘"2.teE#YriB2~\ iZKu3E? q͊aj>(u1 _ q_P/9֚'YA^RynSIKeN(6["r4e\Q;H8 W}ȹܽ6zk%K8 dF ȡ) {i 6ҟeg Ds7( +h!z+n^Bdk& Xd(##,ܾ@1T)Dk &6Y-J4 RVZ)ȚR $׽BަLdj*LjLTS9Yw]Alr+E}̋rd (i X0.12)E@>5kW<ێHMk[vE!M> WfƮNExbqW\+.\:9bS8hX"6?C_ Ea@ E!S*MO< 2xā`;|%.hlD F+ly#RziU&y+o#ֹfZd%^Z=;PRY9BiU9sD $:g?6>&o&QaUԭhoŴ%[v%sCD΍^n\jaW[[45aЉ2}c<ߐۿ6?qNu7ᮨ[^wvǛW'; 﩯/z{b?Yӗ}t61 a*:K%jJP3cApŜ?I}u@"L ״4!DI`y+c [$@4zۛUG/K1~'%~;q* 8]ݥgqӎiJZjtl>_K2ۭObƟKӈwf:C]o1@.@P$Rz4qسj -v/O/lċBg\T}9a҄%lٰF0yL i^be'P"nXm01ɋb ))ԻBQ'%e͊^&r<-k+,iaQq}O.g=,NTx :/Nj͆=K .#acGY#ݶ_W|| 7u ԗh"@ ғGx̍pRF@ѡrنTlNjMF0Pt%ҙ"@Ȣە)Q\Nb"t['4j0 (:+'$J6 IaKn[ <$aj[=˓a$v){GE۠ӫ"SJ]#PqJ4VwhS2ԏt6o*{V{YZ9>ʙqn2ړ_8L%*p{%I,>U.mϸpn938(3 Q! wqȝu/HP(24.؅&M[V406L(Һ۲BpyROZNKZ sQɯЦFiLӤmka0Gx+1RzQj+¡M@_*lk%sLiEdPCn2.%ɇE )iBc: /DbsL$Imç/ .ϙ-$;ޑG:_>gSPcOdAJ;}_[-V B KĽDL @&3Oԥ vMr a}됹lP  q-РXJ)>x+*]̠.]gT9M7+!WYZSzRXY4kuK?#VIfɺM0s߶jvOܲF5|aR~+;jSd"&Y|]}~˴-.yB#(]/ I7a v ڗ-/kmx7Yq5zKL  3eBxTP fhgよa{/.!\|u Ll:L$ .״WiBLZb3a~L%`b31BJ?)s9Ho0b3^pHg4~yS':SeXd觿VvlORi0\lf#{Ird)8hX4MubU#hǢ񦔎BRI3bGo#`F:g\y|k1rZ{Gz0]_35[}zc'?HA&lM-'ק׃\{e.*vXāK~ť x*#%.:0p刴B|@T): a ;)$I q"*z$:0izXJN#UJl+u}0m"|$覺[9Fb6@a9A!IRɍdYbeaZT4aHF߻%G+M@0}f,h=Ihf9dfVB+/Wdur<̯}QR;#;°6Se4Lax|8fFi*%←} @Xu޶ףoovdD]ye9@vD`D Ȳ أ39v#Y04xph YgE }')Fn]K]C0UJ ǽVVjcZaBTaA^tI 5$B&[*9AVcPCbL2i;@qMj^/A!TG6egH͗~h?:6)c4R9UJl8y̋5eP#~XU2.%rAu2Z]eե g#>rW6l@ ,&1}UZu׶Qܱm.kӦݶBK܄i"Kw}c3ܕ:6Ck]#Z8ǀ1(y2P kM݁H^p; Em`~C';\g#%?S3VF|b`0D3 U08pTȣ(eRsZR8 |ig>-1 AYdl*v0x~M"$ b'X4e짧48cItwЍ]K}nu#kpvYq[A/|]Eo,#4Barmi\8ԽXYRui>4 +KiK&bpک.ȔᰮHbXjG\Hp&[l@d[ sjQI|3 ҈Ϲ a&a J2vq̞(`RaFDRsBYwE-3e-fQDv&F8' nz%Elb`:Vrk<Jt+V2ȳ8YWz]祰'EݔF28X$"8O{LOrigG6CȂ"( 1i0&E1 #\tܘvCde1Fէ#0X*j_IZ$78";Ylէi}/{eWB\LZk!~'Lu(:ߑ:cȧ ,C k#`V(_."lfRq̋Fbdh# X10.ɇ0U tzUZޜji?44c"U#d)JkRƇ~srK~ho:r>jJZv~|RND-ߚ涭__ַۢ*L@ߧπZYl7J9Rh"D @ffE{Cn F(>ABؗ*"pjVo p >RMM-k5%S5.}D$S輦ax̏Z!,D6(-"0'[k+.ubjc7k@';I:ZnW0>[ROفxߦSl 1_[f0_p%ֺObbg=]{=v|sqy/Q 1(\q  `,+%Ss RlKcT%]zd ~ł\ZOxpf嶊$ I"S:i!Nz/WH_֭t`96!Ke /([|k{ n5$mh ݧ87V鎮lAoMu^@ؐ e}*9$! 6m|%t2Ǚ3vfͪ½OLe2ñÝHCsԁGszXXU26!;Am㫸5]/0ܗ9_RCpKߧv,82 v?dz?'af'N"Mt !Ю& H=H/5&blU!(q̉tsfx]*Վ,cb\g+~?.k(=+w,Pjg%k7)C63y+ZnZW/ T9^9Vtw eUB@ϒA^N  )HqzT &*T2X&*RTfT1X.D6.&N\:.&6l^ ± #\3a6h.CqgUDW $;68@F| K)M@$K =*@˂L2f ,  y-RjcH+YL!q4(GE`EBwEЦ͋V"dp+l&L )21 E&V$@4SSQLˎLUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.93UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUAPETAGEXMP3GAIN_MINMAX000,179 REPLAYGAIN_TRACK_GAIN-4.080000 dBREPLAYGAIN_TRACK_PEAK1.008101APETAGEXLYRICSBEGININD0000200EAL00012A song EPEAR00004AuthETT00009A song 000070LYRICS200TAGA song Auth0#mutagen-1.22/tests/data/almostempty.mpc0000644000175000017500000000002112157371172020455 0ustar lazkalazka00000000000000ID3 dsfdsfds MP+ mutagen-1.22/tests/data/sv4_header.mpc0000644000175000017500000000020012146763412020122 0ustar lazkalazka00000000000000' .'RK߷S1fYLbeYe1B ,hn ^ٕh3隩*\˳K|9vqZr+dՏ؉ZG,܋LvkJÜmutagen-1.22/tests/data/mac-399.ape0000644000175000017500000000025412146763412017157 0ustar lazkalazka00000000000000MAC 4 ,(fu3 qi :DRIFF$ WAVEfmt Ddata bbmutagen-1.22/tests/data/bad-TYER-frame.mp30000644000175000017500000011400012146763412020421 0ustar lazkalazka00000000000000ID3 TYERTIT2LThis track has an invalid TYER frame, that used to be able to break Mutagen@ Ukx{j*om]WUMjiaDn;~j+z, Dalt/F/]Hy!D>ګ4'4P%0KZP?n:R:aƊkF/ñFڑVW;3330"t!Jb|ć!#郵R vIr%jtLP#1iϤQƑrNޟ7+e*rW(O\n\Ł@PBK)4WjMہJ̗r6! XΧR(hLUʿ`VFT"L2ʁTzs iJhh0Lb4E)S-Y2:2Q)|HP 3(-US+M*!),W\21a53"J&Uo[u13+SZY)VJW4"1@w 1K&|I RdFUw1)SIkö(UܭG9s @) 0R>hq슥rKR 2zƙ T_(eP+H`dYDI=DI"fm /f$AHa!XHC0Y%J51~JmVnlym̏lj\+fzz-Y^,OuUe)$hsTӌx`φЕHŅT9Ej8Ӭz`Vm?Jrz q֨ AgGiΰ/\:K&(`yO+(8:5(pĎCBMIK+i܍R.Ctb%צX 4 4/)sAlY<0 0w5a;.. \&Uj,R֣1jˡ(A{覄X*mB\j9Pٯ#С)(< ;U,3v})E#XtT,B{ e::3kk,aj+ZDfXj[#qi-3y${WyVUjF;CPb Hɺ S,+Kˀ d)tI !j%*uDSI ;G GlUi2eO2×9*-weg+6'Rdilk Ib 尹n"O6ĝ8\┎8-_'zާVM#ҏϳȃ2բrv?m)gv3aV*kg\®7󺪫MFF4rse11 B@K :MѠG*JƭstWኽ*l0\Jg+Թ'[Ȇ u^ZxHQ0o!$pf\%wC/iP14n`OFDdӸz`(j})bJ꬟Iv8QDFTr'hI<x}68|rqTkQYMQy1ea7Ͻ!?B7C!#VBtd'5G^Wt++@<'Oy:V6ǍZ}IW7)oT=iRAm~ys0NILxHc4폲@'X{Bկqpe"ua!}8!Ƚ!p MC aƂñfiadܩȝki|BV+,}%z+ޯ\?ZzV+۩zIuӱ TNSY#(괥K Uȃ8#3{([tl;z2ԤKj>fULJ:}W >~6 ٽG['U ȪA{iA,`TQ> q~G8ƞjQ&`"k4mgY|R6mM*HN7C L $.al"4ƀ&rv9.@,)EMz)1εXc{T0atbUF(2}%eWXyvB)> Dx$dӎ;Q0Т9 44GSLZG(ɚ25d H_!ōqIڒ2ڱ}|zS+d9AJ{w(tVǫCjVd VaE"BܮfF+%~ woR# %%T:DGd[ z<)6ٱ^c7j̾]c7Ő9\+h81+è %=&ųvyJA=2)v$b/ V!J4U]cΎ(ܱ&UB]E ls6JIfQzлJ0 YJ^} j` 02W҈?')*ӺFΪ=f&(^YFMT!xV6j%HMR`RLXCQ'j$T au*3dWU)bjẩS!a_$(!dx.m"&$Ň(R8s8y2qnU*RdR Ύ7)0vڄ'*F(S]Sg]$QAH--*%ddEz*Dp-Sx VKC/sJOVO'S.shy:SX=mzd>[LzAXSKPUJD!1GSitUc -5BP21Q>? d):iȶBXh͊jDOBIs2vexc6˶C\syK+RG5mj;+YWg *fui@ hSL{h im`qW#jF)A? $G&\CzU!DY%zh}WP@4Od>B9frˆGfַRZL &%`z=-jVbĵJ7AW74922(i.Vc:S+ДDx[NU&:u<.ʲZ\xfҙlљ\ީ]L{%*ީOVFhjaRm'*kSS{/hWI6JcQ,"եZr3f »pV'&CfT?6sp5ubt׫ 72KzAU'6BD DLKh(5j`dE&JIMRBPZbUHB*Ti.a"y+.BF" 6&e'[YrW+dS%Knc< /`*X%]OM&/鵍jʱH=Y9KI =셽l/@x9ɛ1%.Xܒ90e-3h |݇b*.^90Vn+CY J1nUL/47S@5mXkKCL71풥<&4iLifw)e]|)Wܪ$ +ND)nVe[ҲqAՈMJ8\e$VU3$[%%ܔ*B޹D*chn)+NNCirjeÜu+,0R)(j=ђ uDtq?5ӡ͍ 5A<&J*N5b0ܞҳWd6J?W+ݯOݞ-"n:^3 ˄]L;9 9WGT(>8Ly\4ɒ[٨B Π9ζWӔOMv[|Wv  *[XHIbQa5LfB7 {NεZ5dC 6UQ([HER%i 4&C X1TdI< @I22 ɛX2d^ڨoZm,wO^INh1]cJ sΤ܊懿Tb X8 ,Ț|:jB}mc?ՉXt}jPڼ1(J'*!p]q+;O!T*Й#4m-~5n.Sm_vƞ[rZ^n.5賩v{-$%$]wGHbaƢ>Y3jY4QKJ¯C%N\xMz/q*ـ_(yhʧ߸g f! Q ~Նy7.wewJܹrOâ9cљ^5k3VZVvJMMJb$zI@;AS88Hϔ~"!Pmʱ832s,@ fx{jJom^şY-+aQmRC&1@a0hȪ$:Rt\W-\fU#W ĮjJQ kob8ԸZDz|ДazSJ6uR!T/Z`PH$/HBƳwcL tmd,ΖKufD';8awEL!c:BEy65m1<[RK肉1- (:a&` /qTި*vHvhŝgd1{sۻ9SCr`Z֦G#ׅ3%TY஑vPQW Xֆk}yYnaB%#)Q'D>V^+ cPȾI%kG+Os$'ԧj5<~Z1}XmBcFyњ{ö|~38ŐP@;]ZkP _0| I݉*rhL .F*/_!C a΍5IZՖ;F(.RH'3.C BJ(J!(h }6 mjzX;!܇َLʣn25#YPSw`D`?#&[R8֔I(Q%:iI&U6LeiT/Em4SQmx<ǭ-KR4يYkJC3`Ŵ4P0JpQiGQŠLH!:yFժ9 F]NV|G)xXJSlÆeRMjWMqR$tHX}c7wʊ7gjMZ9źw655R30ĝSd6߆Hٹy7V}7Xd?ryL4#krKҙۙ6LFGcos .l05;g8jWfQMTaՇ(EܥRcNo37>Hh-ĩ6VJ%U"I$t, GLJuC2 \'*R ԺXQKڝr Ȉ= qbJVV(YOZ~h HM%d+ la`@nZaϕ㚱T>2@UV=WWt n}+9d~+/+ cv 2 SDB˂;hC|# %tAw'6W%2hLWiӉӦ2ꄆ'0Ck tov $8 d Ef*KIvöcZt,0b` JV閕Z1TÀۻSo}O:XVhU bVaܗ^s`"VqXV=*[43 i,ExbMiD,Xp.EhU& 6#3Z2wEZ\<6˥p)I'?U aKWyK?צ 7iκO+y?:xss[wƯsm wϗrUU7m AЄ!a1ȭU7Ox՘1{m:ͥ kU^kP278v]0'*YSx>h}K~ e0rK˷$}tYq92+%,CY)5$:i1(`e.ꣴx\ PR 7{KB˙R*yL~ໜN!1bAI-,e@ c[x{j*om^їQa*̽$˿doiL&t]h!kE3IHzmFeQ@#4 R3n)y09߷6$QaM$%VT= M,b!D PYѓ2ïE}{@!(T 1cP%D08 z{iyIߋ-Vʑe^v]~4f9ؚk<ףi(ejA0,ZD)oC*rhm㠆(VD"[bh<s"H{+&fڥ79"d 3@ l.D ,(\6ޮP1#YK z'跥Ҏ8, &:CӬHLw`#*UD)TkLSͭ_͇Z7mN0t,`@A@m14PAE!G[5YA.-FzlWBKng䶎ӑ"8)2 F^~4!-rN er4B\q(H.0[<n sqN<"PJСΥQ/K.42<OZ TຩJHȨ)O-1)Lq@ hx{lժo/mݕO-2jia@Eo|8 9%ZBYM p"ըp1{cLX͕n)?R.ANY5ð|M=ZxK%MaSp6H-4m싰8΁iL.&/Kө捋yYVY֥~$NRx=EՁ<؂ڮn9 ^Uv>Ii&RH*(Hȥuzr^TiU-`)시8v4 yZ} _] SMװ,Wv2.ighpYNh$(XUD$$Ffs}'Ev$mٍ)»\:Dm{-P"fY(wXL_" JT5z T-hM$4Ȓf*a}B}J?= ' QاKe^Zg%؜֎R@&: ?- FF@UyܟordsnybSg>o@֏k;&)y/J6XJ&㍴X5.2BS 0Ơ`  X?BG]6i \b~~c*fAY,"⺖Rp0HtMc!}[NHZXhn n*vOG%N#>^0;ak;PCrЉGF5Gˑu][*/~%Oզ'c"@U<@ UTKx{njom!YSM )@@j3zhekivR9ښ0SWqťWv ,GsVcVA/Ab_(KΎ#y`,. z2,+)us,OU$V\V*#|H"R]2-)m+/0ZڵWWjW+6<1c@r>yI{?y/_䍴9 c€`CYf @y,"O*j>àfe %J)#.AMw]HSAv r{ܐ҆)/ah5l+O7c[O5Bu \eyViĤpG!*(f4pݬ,X\R-6$+74agoagWR7*95388yE+ܑƌ ?t=)ݲ;`rFI4!BSv}k`J@A6X慯5KTTߪҨjf@a2;+нE:%dStkNtқ Sl`GqP;+q?ok"Rz2%Ó2\i4H%ܘ<.PԱKg-J#3Y, \/"n!K5ZqL 0@.DK{kYAaqmI2pc$Fv,+Ivs*!(FKjy\nv^/ 6J[,,3Mڦ^HZLnÐE, LKԕiM%RvZlJY+Қ ql6R8^^ԵK_Ov 9F90^=MjIdSSfMމn]KόJ&0@ `Kx{nJsmYcjUUg /1CAC0 „2c8R%5%Uv"Brni6ZUv|=A|ӯ'P!HYV$@(eBpfGF';"S eatRy$U4ZWImurU u1KNvB,`m((Y&jx<\Y˜|5/|VtFy-_mFC!$tt f!26x;fͳ+o$b$MǑϣ4xBi=n? bEVM##{pp2nnb;(sNFNyV%Ck J;3lsn[dUQoNU%ZY_|jq2.lu瑝^!˴Q)G%CG3@5dUFR M6˒PMTdgv[aX&/G|,EmLILLuEQH@mdm9p+ګsmpN)pÊp98rBdAMӈQ ,? CCI;p'̆Jj#跣=Tܠe'g<1'Yc-优 ì9{8ož+8ؠGp˙0B/#ȠցJ]%}ݟ3dxz$RNFܺ- nfM< ~Fԉ]K$=TC^\84觗TAxaT8MO8RH- J5  lTh^MEq|C 0ZR FN󹐷os17(zƑc(z.\ x3igsʇNos׎EcM^H>Mkҗ;@ $hko{hzim[sj鷽hRn7$a͇(>2zV6mq=XS*A:3##c>[[NĄ5)7 V-򛳮]pv_ʪyDa(Sj]'Pb+ Ѽ Q/Q[&u4,!qa.G+-ZF%+WHQ}lQaaZʪX/XTWcC,m \2i"z0Ύr@6*z%i%\e `|sncA8-Nuim*@r;]3ר-k ѹc=W\+GP"S+//+@R%i rd4зe7C1uԳQaU-X> V!ؒW%N5^k4rG 2V-JqKƌj`bqB^@e!Nz#=,A9Qw1Zg9 *Tj0qNR;gao*aK?nyBnBܯYBޘjI8L gSIuq,p󬴼1H6 hoFP"P~0-:ni[m3aK*Y'#2٢H'ή-tL%IVW%$luKC-,F h;Dѵ'tAՔ@: u,Ob]MczutDz`61 L\PSJ&?\)XXUSqT:E+zeCYž,vl! Q(#bGk,h4xu@ {cU{jqm^1U=ju=hS;`u`2l)S UcNbq(E*o#^fjToTAՠrھ3E]K6H t*R9JEn߹ i4_ 3,UjACy: v}Fz-L 'y\ dz! C"VQdHcJOj<Cq7/L9H Mt0`A2ՑjpXaq.a0@(X<%ǡEX-S#ӕ׌bzM*R,`1)pW *JKL:7;gt\)/Č'Ë @3*ق;Jҝ?yv&60ib31L ^:%1 D,F!? .B 0vu 'd:ĄʏLy&$PBBXj*'җUʞ%Gijl%$<5)ҕ9T&*9ӤRIi!eG+I qQ!Źx4gI%&pW c)r_Ku8@O!P+\uQ 5Vֱ q708BbH1T^ ^Nr2P'Uh 6u|#(ðV'ip2S? pB!TĊ] _cQJ1G vu; uH1Qc#; %t336 rZ/fӕ4\QeQrC<%$drzI'i 2ԥ~Tg 3+Pp{[uSB`@ g˘{nӌomaUNa=PY E;`6 DZ!+KzCݒۍJSi'(X0 ;Xosċ0SrlDUĸڷes)ѵ'/>ԒF sgL2|@6::/U Q6P3"(Qw3/J>nLRMO˅Zn2Efr[|VxVn"5\g}_W]zmZkmto$-Q 5рK"cAEVxTiAXD j1aO"STT &tpW] Fh=_ZT ;ѴujZ!2.,-qo9T+²%YXxHc`U7 n\+^'`V+$EGgL7TJdv%[x{Y$ 7n4&n}VԲww*Qn8Ff "``08( _* RObv1ESCSd74PDZІE/DX9AxԌNKtZ5Tǀ= bQ=hYKRe\NLRF nQN!^6zԎK%; #<\:6 ĢE>YbAQr %z2UN郍m_w>q s2QN9#@VDF$rnHH& +Qd\%RN,P5Rtv*JG3[')HwFE6TLEEZM 6Ӛ,9#gG0bR6FGh'SEGY,grzO1DygSh?{p % ;F5TliTs=GV\;ӧ|VKDp-0G9]*mV+ޟ2j+#4O;<4l YPyeL>ywozhVԛymSZ͵IĘE6mӀLjGK֟ Tb '%$SqTKq4o[_$nt&#/:M<!E8vg6ƅ󄝪Y'dF*BT!H8aY 4S}`TEttYysrZTK+@ݎ^X]^hpai|MMo?6c|M4Qn8AnQ X"cDb(uS6>q3;,:sKY/Q$ԉأHY2uXeʺ1$8(j)r:hR39ή2!k!iE CjD|T-r:YGKy@h5WoU2] @/lX*60;ЇOUxW"#qࡊ [ezkq|bKG5<@ UӘ{nӬ:omޑQNa4j=Vm NX 0P$-SbK&v7R𤬐7FzL乲He7FɅivSx,lR)vn*ýTR52Wl&d`\㳝tfu3 $E)9zaeD7+0ZD69lJ9P1uq߶{sd]ôKw&8(/$xuKy3͵ؠ-E&mA^t\,/p- K۔cM@P&4F>e<< T.dXa3kEi޸bj,fLI.\ʭHل| <*a@p*LYA i_YA`ӓŠPa?TQi{*^HUSՃ%I*V ajh-ʃEᬙG0a+] -k+"TH\N eKc>dCpg8V+T}HDTKVr V⸝Msߘ77_4)QސT(ə_~E>3=fYG!j-XI.\fr@ yeT{j̪mmQNaj=Da1 B%jN̉.&PlԂ_IJ~FL% I_N bdxYw$QT$Pppʶ'G>G%碾!r5]d`/jUXG̥U'XTUFײ83s ncL~'t nK.pQj,u>5xkKm昖7-ETSpT!eȫbگȡ<J@s+E pSJDB؅3.1 /}b~޴nR[9G~Ô-,q|8*Y$ ߄֥\QHVB}.^ApbgTAfqO3{&_%u8ǁ XmGzk|g1MKfQd QM@&K\a I~Jf'QuaJM%nySYR@ Jq %֪, 2ݙ.>p?V)m9t21j^ d3gT{5tjzū8ճ}c*nHb:qRF Tqи%MQ\-y>aD,/TVj/J<#2A[tEV^'WB0Q4v6d7)Pf5jM]؏&IN΅OsfJ+ȰUt*e'l1ʲr4d'TJ2Qzn7Hh2ɖC8GNfޠ}UWSoQ`V{rk ]$㍴Kf "Qi实k5#TdDYBSQFRΓQ2LhܕEBhj hcmA98/A8>bv4R˧:drQQk+lx%2Ljqi*ĕ8T˒Pvt+J%TS )4L\Y"2 Tjb!1L(ިf6j4j/@ hSx{lmmMW#n3j귀P M Oei BGC$5>mĦ*O{X$"\$+Hye_)z0S@U'>b|:\~,Qu6[GD|usmN>|Q!, ,ɘ ֣1c8Kbʬ\L(Y*UB'q*ԙrY[O3xW\5f6k[˛A7}8]t6^Af$,2-uFPהA`0^AF'S#!H6LʝS"(/ )B، fw6j[K)\v!E1srxs \",W@Q"֯阴脜ӯ")*ɧWjuUoLyK24{_3l{sk} 'N۶N#.&15_'.+KÖ!灥֚s,;qu¼Ԓg2ԈL^-vn6ﳶ&)Z;&"l=bI>D?ȤbL˾m Q.rՖM "M{!r>/b[H,"ib `R'cpܹAńHD]䲁1rW)tr)~1#rY6f3XC a`ސ۝Iy;f_f!?&wyzVT T2v/Ä*K3%bikuʜϫ{|9[znT4 or)"p3riK ' CRٞ傦p( c^3]7 qq­tP:ϳduNJI HaE%,wZ:Ė5,P:Vf]_^%s}"˦EmQ Ro_y׎TUW3:=u46xV1%+<[#Xo0<Ĭ܃ c eK-j3=yw S4f&u[Nzf(k0 vS*^-k enzzg1Au5AnbgnYmʋ`z`'[-Tn %5S}3WD>yŒ[ L"qU}O,Awo[< Raq2"@'(s[| D\%79ZewG]9@N3OfuCbUOc3$FK*l/CQc3D;>Ujaw`6)Ý"Zls1Nm(Q>3u JCNUk[X_"I(ItKv\~@J)}Fmx ڕ+ W/E5)(0.L":x^%~,RN8fW0E"S8.TJ*U"[R% e*iHG P>xjŵAXX#u$pgXvȳHmdraHRLDgUEDKo]ԧ1z_,+.rr5\Z"nWH :y1Nu ÝʝPޓi6BdúQY|8M*CTk $%:O[VP{,iEwn4B)cҸ|ش+2%sHo'xmjt8W@ hx{hZmmɣYMW1kh[c\`g40L.Pʾ9$_meně!Y}Eަq\`FM/lW_\Ktu`ӫvs@PREI&"a-~R-(v3HS&8.@ԍ4MBuQp>΂wBur`%aH OQx10a8p60̮ph:>δڼm*0ܮMgr[UekY&o?9 "4ߒ^FpRף6)v"api:jD"+rTU5'(5Rqs.x,kI2 %\Mn#A&V` ?WLz4JeSr rJExlVw\rN\Z}N;:ZȨMr,lgʟ[S(xs.e퀹zٸ"Ksd%:L 1׈: y/B@U+  KBhTk؃3 ejD!ޅ h2'зifT [<HFGj)FܶY.#B`Pjf֠]i|_ GTA b6ZMO.g8WKHԸT*B"zUF,T$q |`fK-3!\<$zC;#X,'T `[H" LHHJIl2چ+G*enQGJ2 ;4$Fek,=b|M]¿$n[$Е49M v^Qfd-ϊ2U i) )I1մ6->.lF٩CY9&դ P>J"ʕ$h+MTvJjd|q A95FJ H|r\q$E8b %> PT.%u1֕]jZ#BX%ɢIJI.m,=BA:K(mbD ~աȬʕJuC/uh9aEp+UWŕ ?\VѢ 9 °g-.PA[DoH8s9٦g„)z#)a )үʤ!rQF:s]HD">6㴒7K'2- o]bև{K/0ePBUjobPi(H@@ Q=(saV `Xz̲"3;]sHo()Z3A&hnM/rݪ'S>9 8` 5PD9">K `Uy2yrWazN31(DiX S*wS:$d< +˥RD0OJDX]-\k7VؙN҅\ Q+Q\K-l@ u^S{j̊o/mAwU- ꥼ=IeBXq-ScY:.Ò}_;:Vq͢dONɷ~G4%} (reVVܯ H(UdU2슉f6͏Y-͌WU38ޱu]\^\9|@ugZ(R[s-H_`K4u~3G.2%jpq]y+^ʊp-$$(P]U Gqs`2?l!6ۖk;on άfȥYlTZz(5\Ar (Ek qKjƱlV 4&)ѡ>ҦxM6i^9}~5=K3cOAnJ[#/PX5<)Ko{cVEfӱWz|Ev#M턞u[̒gש4{HH3.߸/ Oag7<O)/NX6&рCq\I!o{?.wyкdL·Fl(ar&]']!- `J<g2NȫTu_u'c6}eOSKiз f*x ߂U 7jHP׎ ap%B:а6HAv5p=t~h/<19cB`&|&i7\}Z^D`C7,{'RfT*Wߴ÷= @ PVVS{n+ omߡQ.c =Y7I *Y *"KYKy]JL\㤪R 0JÎR9h aN0^g^7JAQʐK .QFIГŌ_ͥJ]Tk?@u lPhXBWRvF)Iou;MګcxslQDx3?ZuWcZ}{Ar.uԡdi+^6~2(K5"1OTNJoZ?JK__^A7YB#ݔz˸:5\۸s.]]oe{ ʵ~6ц ! D c/n&zZ#!mfev%!M>@%4K@)3 < GHk*̈́9j-s]U 0BXP$X9ڂx) ,I/܋n_cXO$*/I7;蛀~:]LjDlU9Z]sdad artO k. @ WUK{nʊomYNaꩼ=@$98%W).s5WCemZ-s$l*[r:Fs"yAx΋a,Ә@euL2^ P"J廗@n22@t*GV pUдqdsMa8hZ^cg4CzeۓBD1_by0<Z-TU!LYg4rN">`c,HU# ׃L }.[iަ\$k[_I}ŐvnH*l^%2PlSD\悑tP!,Ĩe#}(8.E3zT&JIL)z}jB[W*gku|p|'ަͱY$uK_3Uqg^GREfRs(oҜ*FWTMI(" -RdG#-EH` 䱌e̵]i\+&ik-+<b,gHjBi.e]+c &;*$ܧ](6iP=NY!*3Gb]S4L@ [˘{n sm]QNg iPE+S¤SoG% /rmMRQ8}l;+H?Hb!L3}K4sRٍNS=/Ri'CYsth9b L Eulڝ8 H$kLVKΧ%:H)efx(W(aXaB+K!eDV7(,]Mjø0"Alj[ޫzkPs 6{DP? 5&pA+xtd=5FTI6LVBqfyh(7re^K=$a|qi;hx1 )Paf$ȹ Ʋf+6!FtŠ!M0q:'KoH]B%0;k0\n獘lrwh $㍴UÄBS=&#9yR}3*\Q-G Muڛ lDhUcDwyl6If-U%ݕDf(N"Ou܊ui'QԳ9˸yo}W$г8 \E9#"ZkPp!4]GA 8$!&*tKBApP4ƃA@p\VB :è8d#1-r.%YZx'fPHyu nR4TjpXR*PnΣxn·[P1LD\QS$b'r(Cs -XBTRLx΋sRqUc yNB `sO"n\7X?1M3+8Oje8xO&R3y|ATuk@gH*8#ajU&|7reJ5OhvrBL\>ݚJcYv@fL24T'xY) #q՜:g|z|? Y9LpBKDEsbZq4X/ߥjʅׯ'ݵ8p !H7,m><#!`q G[Pu nT8& 5½F|.IܥS0vDBi"-g^[a7𖛢>^d%u:)Q $j`fB1 09;I*:@~y,x\Kl0O.k \_:ذ4Uܹ9'z+a}c$J}"W_~}xoV,vo}q?x.7b;H$v:ba1"ijȉ^:͎e-ɜH~2bg?-vvO."UWk;kbz;o,u -P& ({fN]\h JwxXZ-VaUQ}R)l\&ҁG;%Sjͬ*frʝ +FkL۟vćW=>Q{᭲H@ hkx{ho m]ѡWM4*u=l,L!j:2XƖ?j8jG$Z7GK Xb)Jg)_'OTBleiyšfڷPs:9[6ȧCТ]KT>HW,;i2SPs[v~wxOkֻ}7 TAGbad-TYER-frame.mp3From 1.01 To 1.02Splitted by Mp3Splt v. 2.1http://mp3splt.sf.netmutagen-1.22/tests/data/oldtag.apev20000644000175000017500000004010412146763412017615 0ustar lazkalazka00000000000000APETAGEXxTrack07ArtistAnArtist TitleSome Music AlbumA test caseAPETAGEXxmutagen-1.22/tests/data/multipage-setup.ogg0000644000175000017500000022626712146763412021247 0ustar lazkalazka00000000000000OggSokt7&vorbisDqOggSokfIvorbisXiph.Org libVorbis I 20050304 comment=SRCL-6240 date=2006 tracknumber=7transcoded=mp3;241album=Timelessreplaygain_album_gain=-10.29 dB title=Burst replaygain_album_peak=1.50579047 genre=JRockartist=UVERworld replaygain_track_peak=1.17979193replaygain_track_gain=-10.02 dBvorbis)BCV1L ŀАU`$)fI)(yHI)0c1c1c 4d( Ij9g'r9iN8 Q9 &cnkn)% Y@H!RH!b!b!r!r * 2 L2餓N:騣:(B -JL1Vc]|s9s9s BCV BdB!R)r 2ȀАU GI˱$O,Q53ESTMUUUUu]Wvevuv}Y[}Y[؅]aaaa}}} 4d #9)"9d ")Ifjihm˲,˲ iiiiiiifYeYeYeYeYeYeYeYeYeYeYeYeY@h*@@qq$ER$r, Y@R,r4Gs4s@BDFHJLNP@OggS@*ok<Rqnp{olts|jMRHq'qfr%ors+E)|hOׯ_f> s:{s[ېy-e|=}s5ӯ gsͯ]_s)6ڽ{s]go؇ӛ @sν7IهsCϞC+ Csή7'ɗ._l;_2PtΡso4S g}!cZ?f?~:A@*;Xww|gw6/x__3syHGhJ"_ .$T~E&A-4A*e<.I@񼔻)HU4bv0 Uh8hO;fLǙy橾a7ot~q߿9o4O<ڜs/`o:>879{?sg.P???YoN3̿nVL7b|8ӹl>'s8|Oס sr٧`gP"9?Z~+I6g3ŗua~?P? gs=zs-f/M=T6| ލ;\6=|F@f? (l oЪz gTO)eo d[C\ z5P5,ӓoNNO{ia=<y~i` }>{fWf|ko?Y_6399so9$-x>ï uvt_wu6=NP?vQL3~6̩{~2lNo|jۀ?Ϗ1wΜ:9~?]?9s꧁iO)I>i x99pmsmNAřO"D *G  MQI_z(Ru޵9?C@cJy"uOS4_񘣦}>$}s}~_oϧ6/ {صCω6={n&|jn䁜/NgkxgM8IzwCb'cݹáNu<> `?Z{س9?YA/бM@PKPA0lA 8h%4 TuY9l>77|0of<͡7هkw?{u8 g8}|N~IOCp>;O7dBv]?Ϝ}}k v5u:>Y s:=yzֿ<(ٿaL8՛ؿ_e/6f#|)S?IAά}ݚ5-7`V0/ŀe($u(MCzȤdP%> e<. dKC ޟc )FFTUJ,"3"m 7'0s?7[ox=4p} t?ONss6W {*`|sz쿠i?o~8@ vEs9ӿ*2Ac*`שs|M ޓl6 {~ro`vrׁ w ?P?4߳ls ^ϬY|}JͿمY~~c}wݬ?? C͚@9`@QPfMQTJlFe |S6S@>φl6>f_gS@pLO7p~r{Cu_?`'ZӸfM3А! @h @8@:V3 e< "uop<|>8n]>'l_Ywy?lV?}ݚg\V5~V9?<Ct~h6PԷ@?6٬ ~~!?$Xb*_MJ, Ui,(H6 4@!eofyL3N:?_k~dٳɽ7|6{{?l>;f/l9}>9ITÜsA*7f;Ϭ?Ys~Up꧁ӝ~o&0qy3}m} l<) {N:Sg\":~v>qafip SPTP=|Qη] W5? 72 1BP<< d!kCsY9ơQtV#B)ZJֺSk}ɖEu{b_.7_<03733O49/pٵ>?Su={>8sgojnatOMB_y NtA/_ꜟN:?}=~6`3sZߏ PawPgNgŮܻ9Zײo·a}a_w|]P}Nă6u*"因@/$?!4 POggS@Vok &.vnjvudqu$]?z}~`f䞩b9E:nls g`u`w_wA'?iaor&̟iJ0um]ŎYw~q 5 ZwN.0UtP3  e l{{C'(:rALmCU%K)I v$4쟶'|ӎ ] iyׯל7_>Ά>a}h_s8aΦ)~{~ {Ϝ8? }@5 g8s?f_Hc9_Lɽ'/pߏ/[|{'7gl@~vvu5>8{p~N:pMIs{6L}~?|7sw/HAzQTLtɒ`[Z  e< l{{CP z㑣5,ڨF54dHI; ]ew⻯kOO9N>}6=6䮆9}8cn6.~=N}?g<ﺩw?? ~o]g6͚|ybt (4 T}īP7~+$B~ uy!j;f哀QIyH:P((8i Ky TAe\bKz# T gyCyť,8_皿?l `8~b_VɀRʙ:c0`I 8d S e\o6HY.u(а6jL)pҶ=]kx~WCNmI 043??_ Gyz`'`zmp6w8}5 }8{s Üa'gNo 05O~s(fZW9]|zo`~ ls`wWo]?'&پN:7{;U+>zٛdgӹ<~_ϗ"?TEp_ Py99~ٰ|8>l_$ ;m6{6l@Rp>s~|]ݔ})zoYͮ͆!Gڿ7@9]4$|MP3C=u:.5s>9xL?tN鬟?o$8 49?9NÜֿ}W= A{@ߒVJwi2MݑqNSPe<dkC73Y?;|CkmfP$ٮ;jOf|??3|}{&~013<vs>kN>~}_M{~Æo{sfsTnׁ܇:;?i`?'U;+k_4'j}W_ggׁ9пORo֟z'`utMw&Mg% P?ސɯjt^\\Gw׹:sӜtN%=1MHJ0 Hh:( ?Br#Jh |% Ťwo<.d!KC8; V)p(C8wmۈ6LT)$ Sܝ|ծɾ3aˮ~ѝ.}?CO|}Lo fhN&{|?k&i fρso:`0m~m~s6 l}~:{j||`go_rC$vo 6guܰv/ّ{PubS Sgϗr$s>$0.zrw~w|g8? ?+?~]pS/v?_F(Q^AJP $٨! hYZz=} yuo d[CCut9kkm6TUh׾ﮡm#|շ_]K۶4O|oya._s=?9u{o9go}u~朳o8{:~ ٕ0u8qC9=s{>$o:'w dh{6sr]?< 晀=|Ϧή:י*`N@5'_'= ?ͧ`OPg\ bfYc/kYb% ¯7v;0 FA "r /u9_ I@Q'lMn|wU9K:k8*tGӂ7?տ'-Sߩ]T]Yt9M_9Pl:ߓ!pN_PE0Cv".97*w?;ۃMJ f ^.{Ws`8޹\pwsn`׬ok!@;}ԤHtkAU#!@ "aX*Y/(#OggS{okN܄!_[USTaxHKG\XWWT]pKHMKXUELG<uA_qm~u*bv~W/2㲼܅'юv 5SPJJe5Οa֞C{DeY߼rٿC_'qzaH-O^Vyy~n/2q=DQ\ Y]=z9@=Kvg2܏;>,rO<݉'m2;պTsV+1+.u?YL_$K mO/KN啓pE'Hy1L*rӝyzIwu3r/: o h{ؽe׿qa}Ns&>e7tsVN૯7ؾX2h(py>\BÍK=E׸|ek*d,>&0_ؼ#9ի?x#<v~bi\o׻-6kp!(B8(UV"j׮%Ą{}\b~,%׼_&9ă|V e/ ՟Ԃn7B#B*a%*؊Q"Za t$uGqNaMs vͿ\yʿ\XVB[IT lXJ"X#hD#Xe%vDQNWG[>zwMb/7/5eO$g"᱊,(|5ԛuR7dn'pprZ۶VUZfP.:ⴳ1%fgngULkV .:TQXVmXzm$ A84EuK[q$U֤bA(kmBhm"[@)(XR\Hb,wv$A&\^ 5lWW:&zbD+Ms!fAVΛ N݄ ѕuQ /4găi^!adi)xz&,`s\3/MEwVS-,5S;VK9Idt bd䪕 ["0A STp_Pht&Dw0};^uF7yWnCn~83)p٭S$SST\̚=Դ~r+ޙIh4@msр0춸\WQ--' !G[,qU *1:%\JB221=Z49#P Xx@( wujȞgoladyT>7!vMM/(>@S"Q]qYK XWAEbκsz̅;[xK^@"Nq:8 h ;D"⪈^Sɩ΅~ԯ$֠k*<wNQ5j4R&s5v|}?{7GOl"=oHacevpq4+=(zZUNuv=W\gYNѾ"8 j9aշ >K~ b[|>K ۔a{, -, 1SVCL0mKD:5c5镈\WJFLƺ#,ޅX ^?n -R5GpWi?H%@Ȩ0FP 0b8DB|譎|vHp]T/C#U@\RTjQ4(F/6R Zu>,88kn9"M%:(zDG5Vh <:=џIj:Z4fJȈZDTBLtL6bZ@Qh:a[.͉MaUTUUU*СEAAը6WbAN-*b`yS"  m*A,T XRY,pb`,t0aiqw2hLrW?BLw[Tg~9삽wo|^c!K5{L"K:D+pޱTJ@߁hNSɎ{X(n{ n5Gl(#'?"=VNj%1 A Ʀdzd> 6oZTދEmh#Q i22mҤaoh2n,NLKĹi,$Vnht0Vhb&l *A$*.4XU ϼ-LRd *ZbP|TbB;( vfٔWf(cJvg7OP'<3y4fQ&?;OVUKe׹=]G'z{kv \z}&N'Y:;ivf%$a[Nٗo;]yT @ 1KV~ „4L:#*i=l!Ƹ(E\P2h'B(6EF-KcTLWemV+%ݫf\tϐ_B)?9 J=?$$ސO鱗*nQS"I06DYSPQUUjIJ4 %u\GQq?B7| z9K\77!6EJ|&g;7&2LNzZ5תVA vko䳮gY%%rz܃ܳmf53k3&~; $iO05F+\m[eTt`S;YY*gP]C!;s续-WEB@"B ,DA G*@"h$*I# 6 D| \ )&*Hh!mߴsy3njP e}EO] KmtLGW:Lu(qs9J UA}N r00 "`pTu)ŖEaR zS{rĉ( _Ӡe[n OdRgxV4h-jٻ\[F?a٧,1<{l&:^ޓ5V5f_͛Tbz*54BNܙ M}upt$T6嘂e|{h0=hU A=.F ! S}+PI$/`n!]~`l/,`.YIMKiB\ Zckڝs"2ӪOڧFNj/ujj3]}0)?H2+_Yy?='DvY`.GG5=`9ljr޸_-Y QW4^<ӡf/J.y!=: Q*9 6wdQ{TCEY1.,v7өmԀ؎B?+绣z^.DƬqNa*|ȗ,nzۑ/㍻̲@!n{7W feϸ|g ?҄7DQ0܀)DLJ@Za" Ve@ˆ 2c9͂SYKø#}i켖WFkx=+d;b~UUUUC ZKW @M+b\;^FE'&BlD忤b^baC  @IJ'& S)uU\]<]had! fx3X0.~_{&,7|sZMitD)r)ٌN!) ='$73u孊юr-V_&ViVqeAG{WˇϭϠ!xx>;k7,B x1PV0*Ϊ@2 1ޕ";Bq˷È̂^I64OCeܝ],_F1[5UU6Uac[[1W6ꧯVs׃wىt2d7ŀwxGhD]+:;iRXZ+Nb$*räPZh!(#W's7*^Zhrf͖nELJT ~OWP:NPJ~c= ? {wc4X5Q#%s*˷p dcz_Q㥭qtgnrM.oi[7c^'˜poo 0fIq$3`fn~"y(z$"7{XQX/H.Vnn 4wF ֮*GHNY(I@Ƈ.Y6x2cs>iUUU5 Ixp6D=p_i꘻g?Y/o[cPDggB7] 2ΌQ-i QMzIY(ƚHdnA*Ž3C ˘C!zYH]^:0lL.'zg_$XVݳ%r}RS<\sIS,{pHsaXXgpF4V3a;e)PV) AxfNI!m֟>vclArW|X`l1e6kq>qEﳳ9v/?q [W*0NVD J 7 G& ~@tJ6>IMP( Q6@<ws )w<-_m9jth1mxwv&5$zY w^{aKշ"w;fco>O/0Lw|r qO3"Kt7yh4:.Μ,C0;wx4գ7+]TfNc%YsDJl=I.{6jW#VtQ,k0xRH(Kxtg,A(I!il&Z1N&a^tgF1 r ͠a"Iݚ,j32XoY1sUl[bv]2!cCrOggSok'_n{7d4@ndUiM=xӟNMYdĕvSS]$Su&OR]W7$κMr(U&i,e`݅PZS{qt_ x0͠Sy -w vnÏvvj5Te$JbrgO}r0YFR744II4ګxcK靊m5Dp(Yh+.nX)^]Hq%;&(fF(g0̄d {X;``*-{OIƖCC)_"٘[ "/  o`uF|F6mC,HG octa;Iݡm2X]B>eZ-&,bPr(O{2'"U9C@T](#O(ZYT+ 3dMbuM"L* P"&/tَXg1ee1Q\K\*++ V2J:?.k95{gU}Hxm]E&%u/[ ɹY s ˽Go@.0OݻPٝ_@䮂A " G0 FvB1( مȂbxXP6 Asme8/1( !},@*96A#P33} U~4Ü2>w8'$Ӹг3.yM]ȀcSѽx%"RtG86}*lkn( :UzkN)PSjkYh5 n"2K/٧cq}JI屍ū+Y*Q,Bd01te}erƍ]QdkihY RLghWeDᵈZ_yJf;2w&GOqhѥl8!M̟,,˾9>ϲe8 wv b y@12wュwi6@w 4w9K%<8lWy50i^O\2 {Hn4 $ed_ٲ4–FhsS QvR$7 nw 8)9$ Y кB4XE A۳'#)fi~mUUMa"^؞1lo\3@_ 1JY#0<1{Hk#smܳL*f|Į;A=Ki/0hd{Yi[~'Zh Z 3W^"H_,oyws ?"+y"UFZ M4x'>*ku>@ft=WvUJ }Zܜ$;_n nsnߗ?~[vjq_P33w̠֪If*[FE@$00P>4)`#.HDe,J,]FaUGD6HLJ?pZ<-GlTUM- EFƻװ޻݆i5K,G}kfS6GI%.#!nIY[ߤ? vtẗ5ZgRF:7/mimDE譇?T&C%/u.uQ2A./X؋í94䏗@ooo"$x饫f ->- kz׿U7MCҐcǬz=E>mTg ÝKp?c~3V`_p$V<!# 2(^'x пV-DAFX)lAn|H !' rb(s+mV /5ͽ] {! cj?X:NJd3vÏc? s{hQ?ḕ65yU,dSqo \.\ :4~I2XovNr]rשD;2Cݪr[^*&wK}@e25u%,J鐝*n:_g.3!rjrRWg ԅDNle4[];x5Wup[5eKQz,;)J}:7?۳"KzafJnm`i2{|.45s dϻo^W3\-3Až<)zO$ w?P|ax=P^.Ig4}7LL+1i~[@`Bf /Y5) eae$He ީc;/9͓(bH$Aѡtg `1찪i!.g9tH*1=kg "g;m-d(nwkE7šݷ95?/upmǪk+@dPNK?3r ;"dSFf7[ 6>@aUWW6s9-)qA3&'bmυSYT7MQTN=^9U7s/C,Dk_fwY3NF3tH)?8gy\bMan;n)sfNh`؊6;otv(*+f8?gsgrjpI l? (w9b5Pn!pnگI 1Lh e#8 $ψm3]Cg}گ 0;GgYmcffq{w}d>Ϯ,4qZ^(I'+M=g9rs)Y̔C{*دUTuUkA!Gୢr56LzODau*anVtҊ۪AːIѻ\2Z AJv{r:)>rX[[θeCWo>P>7E=fS܌|"{P&rRκ6) `UǮUǍjq~pa_ v»q sN% ._|- m!fJzn1OggSok%OxXg) Nm@ݤыbTP6oQ5Ն;45E ̧vvj9~ª$;OXAڋHʪJ,C5\JB$"B366%R5"^ݚ0l)di^22{ђlnЀRBC&_lUUkLPY1ev\&G4smQ\'BH,G C%KJ)\S X0L1Lh)*8,)Q@EXU o2HsC>77Cu0BD *fIڵ}e5,[fßw~Lfy5PΤŀ]`5p ˮ-%l)p:(Axqի W$$ k 7Mh!}YU5S*""U.iI'8ƪ{M#9$ 'S΋l(AVQ+d!6j0O2eʌ !HjY+#q/fft7C`T&MfU YĊYf Yhq7 u,,apxIU,,d6C.UFE-+Y>0. #NZ+`-s~J1F`Vأ_$q#gS20yU_6~li+~K}|p;\C[CcY8Pd?@T0rs˫eUMAW&kE!2f~ fAGNFPn_Qz#P9[z7N" 9͐hca ވbbLr4y\9T۶A_aiSi ;p}fS%)_+d[VEl1 /`HU8?גL{G!D4&Nr֟SKB6{ ;9E5 Ď5e0आuݯ40{ngKʾ.*;׳~dˉ2kW/ rz)&2;$}=?|t?LӪt7dVA'_{`&6JF1=}%dYS.[e/99䞚~h.mƹL6S#Swͦ͌0B&Ep P@$tpy;N$ Kc,}m>,toZI͑ųY Y g\Gh| y?F# @{y]hS\ѻ[^ɵ}g6>xT޿ Wˆ^c? pWo*[tǬdpY;20]'dh/^֨J\,x%se2[ʘҕ4*Ʌ^Βd99ۈ>le\,דN4T&9KkٰdWNΜW`;3g,*Þat~v;.o\9wwL]6 $]~%@nؕJGaJb2Cy06;!m~0,` :GllvI_,ȗ*lI Y@/bt\3dI}) o}V[$y\mokq0Of9_9.)ie|I뢉Kt=)9 -n mZ="x<):\^`y'ʠ.OUB| 8apDXa$;Ԩ`2$I?sHEtCigT7Dȹ_\M h1Fw?ǡWv&g8vMN{KީRO`1qdh^0y1$)uu>K?~C#µf6 Y/#;PO™b6w 7ۺu9u-ɽwkO X3n&~X6tHY[|0utVZzF"Զ27VՏ:m&ʮt^[7s 8DWwJgxNa ct?ڮ.)3, U}OЙF{]wȴTK 秓oq2~)w|hfp3JNVkgrCgTjNw;᪪i6-zg6Y$9wpS =7*BM1hgAWfn"^@@ n% 0 J6~uVe?{˘; IlJCdƋR7> B.( Zְ|.`9Hjmլu*q3xM1&}3-~iDCizkNyxəq+r-mK( q߹HMyWhq b}={m}eW,:,Ƈ%&sx'R3& ^Xe̬n<˦Y~~&<V]?ao.L2 !dfH3F{T7ɪ,J4OggSok  t]Uyc|ޚ'W]L04էԊ,j &H֗I01=ǿlsa=Ubj&ʐSu]k7;W(R"AzbT+- $-+koR(ob12yLh *َ 6Of9` TP3š׍ݥ 0i~[uD-Dp *c+1%q Al4renzdh)ۆvء×ם-Ð_Hd Ĭb ŲM&fH5|`%2;6TxZ2FV!2*~Ew'Rd{br~sdPVJ^7b>" 8z9,^N3tn;ti>oyd3.kޭ4wTNI2 *}Q)jj1f  !t>Cg)BN] 9Eb&$]wdAuc/ݝH8B1b366YidO"pt!+ pA[ڠJxJuJ?,6?v} jXR" DJj6ԋ}s{xԊ9s9 Qjw/鈙 )p=QL]T솘(H -mZa(bE( (NaE KeE&Ȣa3HH{r( &1 c+WQ$1b(a2 JNS9eYD (ݓ̐V ߝٝȤzzSMjLڟcG/YFm=6nNCgV6}S3t;ܥߥňw6Mjh*A f 1 .ͅaW Ck)Y^ &s3&hK`pl~ΘRlxo ZZ`ֆVZ)v̜2Z*2H`citih^|"'/Wוh̩<.)"&WȀt@M^p8u g:C̺ƴGi?j(.Pz sw6nu- >:wBtEøp =}A=A䁶$u;wc{S4U$TF[pꮯ w;cxWvg['x!)}S~ݍ0ՐCIE语etݒA\:0>/]Rݘ46 Y-"^}h7CMhTT!AU$?S^yeDuΘ]k05mt P ~M3j*)5&o\i Z҃u5|WZ*GwLJފ +={t@,T="g:#%NTV#jt AC=LXrP@ ;2g=$tcAþ$١4 cwc1 7UkAm h5XD7%!%v"X/ReQiA E M*zXِԊ"!LӶt<AL 3c:{YZ(rdK>gsr2zz?Lag_kj[.gQYYHz7_UM(lQd mPC546\\Ԍ'4dlSh67}㑫RnWK斾kq]` Xh!pcHSRlm>T1`Ӿ7f9&hm^g싼YE+ܶٙFnu1 |ZW_tu`DԶ~E> wlOmLeo?ܾ̜;9#u-ZÄ alպÌucϦ>W"Fo|#*RgfTƔ@e\ uDLV)A4x⢢&cuX9BNLLV; c{fFDYLVV8T^Tw+ &IVc#ηn?0w NM1W'[4HCeoΞ:?0Q6 GR[@f3;L1g 1۶g?O&O3bM 0Ifѿ7̋%JZ*&vvq1[7{C WftVd)9گE>sY#T$?: NNyf=;'aZ}wgLaY^t̟O _-Ii2@lf)0[qI2+ui3Ի0Mn(HCnAzQ11Qf4^z`dޮݿ2t`m.K^iicy baY9ajC5#ALRC8ДJ*aUrIiSV(idRV+Xpw * ,($Ѓ&[;*4W,TkHet*e?֒]4#㘺ҽYYU&m{ g[時R C͛qYaԏ5.܎y&yq Ss6@_9$+W/ nm6oy;k9rX;K#EFVIE%%%m(8-`K^U'0*UFnCMg c$"?FZxLt:c4:8vFfI4jq_oi!3+6guzqct{T.X TheC%[P*gQ;k_ܣ!͔iT (RU 1s",:j^7UVks,sS "< ^VqQeW~Vw\gr(P1M` xoԒU93WɻYWOggSIok 7!LNMIKbbYWj8uT1g$ 1tɺOb館@e]|=?ثcjJdEb7$} 2m.##=( 1@U?7XD浢°3FT|MI6|#o-, @V' , V#LFъH9J6gꮯUQVԙ$&dw(X\uz! ƮѭaE# DYV1.[XD(z N),p͊iFaHT` (q[iPܻ q̞檘Gmqd 8VVpqO/SyQ00H/ɻ>M%$fؿM{b<i+9$TC4/%}))r Sɧ<|^ԽާOT P 36Qub$5I1n`y o,(Ƕ ,K"tո/7߻ူ-~7"'!_(Xoi.OBԄ4FG6*jn$5 v̶,s:v*ݺ=Xm"SRt^tߏm_9>Ύ+dF_hb0 ]%1}O]PCr@p4( ,sm1͜,:OƧ g͂N>սU'It{بK DXe"0$6(` #o)ŗK@|BK>f60*ln4=B "5Y/s5՚!Ք%$% dF'tC"cauQCCu܅H,d2 0 `x}X/h'KkM"a)KZPr"jRtL`Y8{̘=QWKͮf{S&rߪzXqJcHBeKHj,\50hP¬;Ehkw'5sfD1g&]MnӥR :SE^p!c֊%g8 u&H1MAlȍ |ٝ=hNoC_  f , Db#-$0ld_Զi[&`{1J"C@LOͻ2ܵ>f'H9dc1@*|E D.O}$ [稨5QcHOvvhx^R&zw&eSKˀnGЂ5k DJ+5,z *rE "^8f)pE vD’%y.]NE-+qԘc#=wΏP4S3#2z;N)|\"{>#dOb Ppg߼#+ɪdX7|aꪪ8sexfNcanjqT:ߙQP73 z1`Whj4IKXb\[+u[E`@F/ > :RBq$$(Gl=HgNoȭ"yj^ɈX ԟl9dqqM[?j1LZݭV?e~v]5Jc=.)GU][r[Řil!M.MZ$'lhAig *`qfcFA-[˶u|g33C~[tLЁօ 0Ttp:k{si5+Njrc}Sw TءPĸK Õpܹ0[׎7M<1;$kЕoR܀ P8XQ)+U XIސF@Niը)0Z|p EJEnYH{E Po )~7e\/&4o[}gW6mseؿk=MJƦ^{B C:x3+G} ?|L݂+ޥIB46l}TTq:QQť0b!4 [Ḛ̑vat J(\f )@jü̐cj _<pl;Б1SMSuV's5Udg;{Ļ;H.Iv~հT-)~5vn")v̝ZDzci>$k'׮Yǘ̚,Ry$ v|l p `j-ـ+ʒH 2aIv XQ薉oKVX ߬ tJ4Fw6?9`uT89oc]qD*mWPqqdL\PO;uF9.,s!0,&wI4[T鶯oĩY0So^)xY/9ײJ7-ZOӬA]T_eoeqq%,NK3˸{}',F{<>?QW`sfo#NwG|w9~m Q 2DRK{""~'#o<ʞJ)\D+=A3w|IVĝxdE,>K0xA|ئ >aN.䦎Q ~&15:S]]w3<9e9l_i2E_ؼt;jLv;cOG6P'~&f2Ou8<ٿT1-ϥ75&DO}Bfb50 ֱYG2VYeN]O,6RGm<| /(ZbE%I~#iU?*5OYP ѧx88ɨ\.(]O:mV"TC 2̐z_vW3}EGi|ÔRw/Ͼ̖Y_eW2hC#TDcD{r8g\Xb{e襧 R@&p'Uy}LP]UG {ZI`"P̠?w{#^L=ݲ5^DI0S,}3; }OOggSrok DuuLTNXTWdk|8٨S5Nqܞ+T: y ;pkjp3i0O snC=M?5/r￿c}ljʺD+~$8 O/uz 2l]DKP6" A$aH01O63'x'e&֑VVm:餱y7#c:>69q|"|\y[4lQϨĦ(c^"D4vFUfiZU,c,%sc&v: *FankV*PMK^.}Jz(P8Аik]I詮~I:= |j[an$VT@:OE>ў`:*(H ;\mpjjbjH`'6]D] M]EAV?1F*I"Lq`#\K vAK_Ö| eEPU 5q n&j53FWy Wt *m WFn;%U啽t2xv%ċXNI&Tren7]f УtAMbZīݹq\ɎYyo0!-?&/ 5"7;wsXxiݵi?Wve/;zҺ.3t]@Q7ˏE}n7f.:n̘wo7ϗ3kM3u͍-XcbS4p+ ɷJ hzK?\@" FϿm>.oDɉ/h _cbAL!IH: CpހḾs>ϼ_zՆ*;%?gk0cydjtw6Gą:c"SI'ݍ.njPvޜNOۈkSN([Wc=1E b;pLn샔:6IsƏ{<S\Wx'42񝷆ƂW2BA6i77eRg'q6'弶}q񳱵 غۜN? @ޛN C2sSar}WQ7`/w͜6U_:*UJ)\0ɉDEE[ bĜ/0k5qL|{fg%NZ]qY0{@_}ؠ *:62SMW544{肜A@MnH׮l1 9w>3ۏ"35]a%fj~tC3U95qd߹}w5#9ea_q/ƏQ復.PĸMreO8$/N$p<*~Den,a\~cp5Ƭke#tPR3izj|˭W'?i6vIr?mdLCD]1 ;!Y -MU^sʎ|_ *`? ~c JI)Ma'#<*{bTХ?\)U/o1iOO9eEU:ٝ NDN=$ B ƶ`ruDd1%n}跈a>DRbqU,B ʀ*ZlbK~`d8&ۀ]SnsShvQ`E zJ)EujW71ܮ@u0Tf}?m)G|0 M8<5,@+/&*(^!TZ1ҊWJMoNgo}|u #^j~!" "?4P\ʴyɱ:kիj)@zHt7߶77đwۊBRy?`ca,50FB ! ɱ$)q)v@QтP&XB##͈fF!]1Qd5å2 `lV),[P(+TWŲN^A=tF)I1RJ 嬨]5_[PD|:5 ` 3~+}@]Kk!w TSc ly0X`~-U.Ń-Sf#X4(7?+h"|a0".B0,I ;PJ JȎ;$4mj ,Yd^ [Z&y SR՗ŠsPT:&YJL HWu H|ZdE(њ̄XDFQA0;A,x2:Dh53fd@m\Vɀ5λCr ꬬdIvzIV#_eHΰ4 }+<;dѪ 9o4홟>2]wH X^o`fʝL'df&x =ݝaϫ5yEK л`y!G`q@S҆4,*ZcIeHL 7$% `3ZXb P.hVvNo0 Z◥jHˊ ceMg33~pY%;DՊ/l8'}Yt SJ \_ JC @xuQ4L:zbP)LnzP˃VӛDT(5ϤP,J[UD6z׏rQJY:OZ S0jRL qt $puzW{50Z5Jg3 s~]a?`Wpd 헁;3pnɱ_AgJL-wH z/#ˤor[ˀLoEIuw^BoXR. *(Dr ȳ V8(a~Ǣ\,>?SV)sYV 3cjܡi+RA!Y¤it4SU(ҫtT7k+:T<ߏ])r %ZDq(gD&R:J4duRZSŴT.mY̒εi*z3H M4r?ײA "[!Vc̎H&Q}g,sy>!LC.l\S#< sgVZARIwPTfMp=t4;L!Ɇ \^Q%'@ ?@$@B @ћ MWhsl>:HX~Y$X\ q hT`bOyˆ⵫fFpc*b>'KRPBr͉a!o)ԬnFG2X s/wϥ?@Rv}x!u-䪨҈f"s7E9z( d!:]mB@^sf>dwChR宀P;v6d'RG 1^y3=)Φ"+#r.T φ{PE.47H>wa_B+g|fL th[}MCD{yThpX,$M`*.uf%p2pr4Ϯ`W\Cm&v-w3(jۘwiG㠰 X_xDE+pRK>  `!37KH8㧦Eog7 QW%*#Ttۋ1ZxOz~ "oضms[ۈE4q swհL(<h߽j?-e3z% ȕRb37kZAG-Ǻ(Gl - r$,fK[0Tm#5 ^_6bO^`CP+tt8JB&6b^>-UIh`pf;WW'F$W9Ga =ħV&!!b~>Mns1M䥮ǟ>'ѳ:LO'gf&/C 0E9Xwtä<PSIwPBPoϻo(k!: %=Me].'dbX-8w[s %N4Aw 2y#+?7{5网L5~{ؖ{Q&ݐUTiT]I&lP! jÕ(fhbߝ6@əpE )g~z:VNsjd. p 1tCa\  P m)UФ!d"p;)^ ߈R;g~@c6sxɡ7_ ]sȇjC1rZy?ج8nD^?;&&Gk2bBp5Vr03ճD =Dޗ0=k/ l0 e:5?щ2ޒ2Tb#m>xչ:" {29y熮i*i/֓{Ra֟dN_0ib~m_;qfzWi:'ފSpRkUq/E>0= ƇUӏk3Oi~r9/E.Mlq%n RuVhX^( h# #e%~bm|fY?~5ȋ@F7"h p}Vt6o5kV6'mp(Dvltv5zs}s<1f=ZESbTP܊TbȎ+UQ(]0,I$LJj"L&"QЩx5ݔLQ{ZT8v5T4Hβ R)*޹{eg/OךxY4ʲࡕn *(i @)l;s6#6{rP Νۘe#1/Ddg(` ]*I:OF 0tmS8J?eV.U7[ }Ke`pVYhg̻Fb_E-d!zoW4x8 (R*Ȇ˕-aj=y[quZ}F5B-BiB2dy}\1oeK+RuOU{c)tVܦ7IBl,m`F*\Q{Xgu4zB!̬ dГaqs ͒^ B_Wddo7_r{W@!#I9yA0\BrLBnJ#:cs1 5EPT >F!9>?d7jn7$w+T~Fg+3vjoTOP `Z@0#KFJJmPAUo#h/o M!F\8H >d5EN#4^<_w z:϶a EcPi|46:qE:-+-8 *T^n,d([R&(],u[Cqq SI}/2:ƫX^Y1Bʸ Y)3 1ル)Nluoy P|aCq)RMge b1΂d cZ 1ݜR=5Yԙm9f YLxMj]pEg u{7OQ5eȩg\9WR ?FVJFY,3u÷^(X3M({}"rlŅ-L&7~T̋`Kfm5YS=E~c utvx 5o+NɻB91$##WL@?gqwBڌZP$[#ܮx`oFyOAuܬrBD@tמ HWT!o/r'prpl##kR1)[X UߓITor#dчqH-M0foLHH'dly, `{fY ƢNV=]uqoTuv@ހp2jؘY9|u ӕ)P!$vc1c4ᡝF` !Ӎ$G \~+Э"KcnwݽuǼP KyQ~dZàqP

GJ;؆k7ߜr857Ujp`k#x X/VBa#uyl$2!+quWzJX"{Q~_ ^ZRpl!٭^WZdm&S"f7cqdS2u7tSfC{5ݲ,HesY@wӼOggSok Omyxvz? cJ:/Sqg|͒UL­}7]dnmzv60U@I|!N+Oot,syע.w3m$zY"͞a(Ս@ٞ[H1sd_p+8|UF_ȎW4 h _bX!;Bi/U9Ptq˹|c{a|#´t ףc+;QmSB{ӭG#?w׊~R?g =}R<Zw֐Qj1 c3yS1t/a d>2c -\]-i#>'G#8_wy܈6|t >j+hSјfw%2ן߲qʥҐYPrn ;2PLb~ ;낼]hqsLms:79^3btW>&aHUm^A*v|2~<`9x_.AFAr 1ȯˀPce]] G:4.+:@Թ6I  !7!"1z3fc:kM0mhϞ]NsU4:@U%S[6af.iH2+c`Kh-ݰ ׬\e-aiJʮbi˻RVDcrz 5ӎ襋NK)LRҵz̪Y#ș={ЧC3 E|=z&ߩY_ 3W1]tN/`7Ik<ygkjh]b3]_o T5-s6e$ͧt 2KVخzw @`hhBI".C/ k Z}g|06CJܤ G:u9ascXfaUZۻ`E5bIZzDFnF8zotL(*1cdbVAA$ظ-4IqIl[?\d_hYR-l@Iq5=n[KB2D20ttYǀ$.5tx[6w_XޗeAb3m: rġF2+C Ծ[:.ku-8M6CWw|њ5͛)*^ VPmH* ?(B, T`:J$'M0+6 gJ~ fĘxԐ efM0>ܦOy]CTcsn0Z[9i%E?w¾:G;?98xcC9}}t>K+ [v׊2kB֪B 8Ju:$M$-M*RLSQN±)ЃԒ1 l&Q0'&*MD#_-{KK.w jD^.ǥN0P,X3rg{WJ8>0& EӍIH䦢D0@B3Le潯uVU;9yo |kcj!ɸ!v ڢl}=n2O Hb2_4W1CoMv0l ў㔎*YZ5gs97(z H\\X`fS6 ;@ 55\AGΓg^<8im$#T9WBiBqf-A_h/,6 Lȣxt/uDžE ,PKq?|>KK&i+ܜeP0;a&U$ mMOZe fdIK`(qyU{)֖+%ZeNE"Gv2A1{\YƁB5UVvjkJkB :˩n$Uhh55Q@ϊ~: *s*{)uUHY#Z0HGӁ"gª=Y~03Dqa/mǫؓ) jeߴM2^d Z _j"V>PVA)#Mӻw 3,އpU[Fy M6۾̽LȞ;J}ݭ":]ʈ>;4X+(؂&fG}HqI\]rG8ĝ* q\U"UeZie?a bR@hzΐYΓ.N s _ EE:l{*7v LJ3{c f (f1PS}e0YHt4Lͳ6ݗ9>p!.n@S !/@ g]H__vr*! EP7 %ԶB z. QN/'\rixooxex?Uu n_ĖۄCB3wNyE%/O-.JeD=)V+ɵ h HfXBk ZO:83/E>6s$l @X9,u19L5FTƐ΄Ԏ`a1!3 A(\ =եÍwS`hN{SvRJ dη/az9ʚ@>ŧwcQ|g?hw^)7Y0+S+Cmٽ!y[e,ȁSB{+Y҃~uѵ3*zU>9[r늽c9VmQWU[qfX)E`JLvoNG}MNG;G|(SR_m넧,KW(AB<4PX+HrfZU՞b.}ҞFdL1OWh.q؞'idDנt6yfR!tl.$L("6MǏ|En P.pow:*F8Py6OggSokYHSUWfP{LGO]XXUYWVZ5w7M&1M zڵO{y~sXgDGfx.h]g OɢzZ4j+-]hrNv5z^gf6 yPBo :B0@UD z@0 u_t>LƀL-Μz/iɟԕtU&?9ܯЉΈ,};+Ow~.)uoE `z$Ơ2s<+qZsI7WK׎$C6#6lc%'Pvfp(\(JλbmE;^?@a7ZTB6!SKOECacdCwٸ.F-29=!:E xkt,Wid#܎}:HxLs*';)I eQ~\X~Aoawy ;$LNr*ɜeycY l.99%##B@Ju-_H 2l1'MN؀O~I\"5!ݱd˗avG^ĊC7Pb^ʚnxC},:9!I|TyrcxkaMnou뼷lh6~kߖsV_Rk[VeID_EMn00}o 1( ζZ^PVIhGhOz*">1u1֝*٘&W(A+()Ƶ<ԝݴ< lb7wɤh'=t`VIȬH8 V@_ -73kvUՍ ccmn\⍎K?|l OsqjLMQP=L@Fxsz*F3 ʤev}~Z|6xl"paO cSI9_Bu7~)q+^)2\*ʪZ͆'Z )>`}c%|bHàĖ@Mm j4*VlEa=NUk2 QUkS.qʛWE"$1Cj}?!iUOf:ڢ*9(ZV5Q[JRAJaiwDJ]jr8NrPU5f1!{]ʃb_va>8*';{óRD VW@ kU֭:4VS {5KB l?H*S]??ߐt&EZ͡mi'Q YmA - J;R#`Xjbf"v-/L*3B~C|~Y kMD2J&!tY@@Jl@Yj؀AXlyڱ'VL +r/J )r KQ%Sr#,=ez\ E%ʮxPw@L~9wvIDbnDԈRR  K*0+WU! VUYxڝk_9_W{mYn1Υ1ue>oq`Y? >ww \aR4P"TE $K,a?0y /!X&Pca7m WMĘ"R-R.& Ӱ.EwVDkA) ڱ"Ea,eKѺDR$kBflQ(b,bZe% c)gN"2A@MILkF/jJѺGsy9eB-L׳N1i) rH-`迲̨+ԛS< Ib#?F@Apk^;dWP +4d0Zn`XS~OA^Wc%Qt>L4(jne Zy) ,1@co vV ӏZ"Ka4ߦhUy3410RUV|Mbjm6BbQQ:TZHVJS / 2H 8p04H*lκUHD+Q .2,CR$USB=mZTsMIT$+PJ=/WZajQX &,`}֣OwN\2̻vgA>Ji>ޭ2k)A\? jHH^pbM9W캳sߞd&iE 1-PFx@PIg'[69wGP(:i)6jj#ͪE DBa7Vimjv:] 9a^rJu~g $_8̤_6g$b JUHcrZX-DewH! h$L$2ƒhaPr"MHIw%$ݹ,n C,NsM(Yf@R颺+6D~8 Y KMz \\nqm~~쁤i>td?f6kDem6su0n6I'ھ-j%nVKg0?m#ԴEm`'w}hgu(o[ޤD&}"=BJoqvxJ>@YI Ip]=N=HeJC.+NYa?y,A=s4&}ّ4&F#Lě#$#J%!@pY3J{$6/R,}F0/0p/=e)syR{_,)uS]NcOKOof_$UvFL,1B˕> 蘿ri˄O)Ua/kZ?~Jgٳ_kyw ￐kqtF ;w -wot Xy ߷Ȣ  ֮D.jBZ j 9YX }A-X3;eroNuqJ0bhKgʓ#C \=W?tXˎNDt4*_n>d,qkv4z.4 ܷNQ?K]:ϞQd,ѺTJ\xBͶc-;jO ?-`avIb಩]ݏr7dZeG^]㍥dUʑҵQ5 ]'@.ǭwGI/,܂s`cf%w|l@~UVB4rYOE[q9SSOqן51(*3_(Y~<|EA%~jKqJA`jَ0aO[#٫*vP]q{dD?*덑5z`륊`g?rgf\hǾYhstTϧ{G;ȃo'_}  _rOggS@ok[_LJLNNXUWndtnO7Qjt21h81g5kh^]Jh+\CDkXb;=}y'so.^ U\]5ZlbPsF;+e@әtm8c~Q'Y*2Y[Pdr\RK\ux}\/Q-_e;\JRZ ؿYgFmE7Kv:0؆35 FTk>Z"g|EFq|){qa|_ywܖҬ$a!Fl1dj(e(4Uɮ,K#eVL]$ԑ!V(HMu$Y4ν([pWѸ?/Գ]Lt,Eƙl0l &gK.WmO_}' dƭؚ 3ٟ:f/3\.Y${'TQ$sVyO3? G%,]|$m6iF:1l 58we#NY!{Ie T1ۼ|.ղ ~$̿Y`TI37"Cm{%ڇ\JrwFbh fHOK>L9J~fz᳏q\&itγe]W(H YN`c#l0/#V2 gkYWIrsvZGHE-Tl Ҳȷp[ u4i(z& K]<%v2rz[uEr{N7SW%Wgjs'w\TF YSPNd[w`SB a/L`nߤx% 4cV >3@ FoL~Ry^~oj(N:% M d1;pZZcZQb?ڨN/$__ojA3X%N<=lR/(dE,Hf QBX&aeLhZ,h r$ t6 d7ŘWؠwNk;S6q9yZ67SYq*gO-{;2=&[&{7)| `P\ueRL ÷Μ]Ym񛝝yպÚ 7@{tm[zoUGkS`.V{RbUh? ,U {0`A@߶v@2 G?iѾ1/FSxYDXm+dF;Ej߾cvr4(ˉYr57O_GS][Щ2.x**1q Ŕ- D&;Q^Lu9(s0o> j:9i_s(K\ݭʄ8[/62 ZAkb3y=5YNpnXZE=l d *]IHȾa:⸚[pp6gAI\;v+jW Mu¼g"/vGdOn_n :alWZEi0TExuF=GxOJlKCϺ6*XT6 6a%Y&.`j([3{:AM[QY#!lB, f͋ jp\.EjqnePa:{=֐b טK*%RxnCU{;!\P,.1C7&c097!!5NeMy5K9CW9vTb΅~:S8c6T. ~1AP]}:=-AVN]Vp  gu؞5*`{8gywmu]5uvSK&@2`WXT>ɲ%?LlEK/UL m=?,PW I)Iz \{}p.\Buߚ"V5dDӏX$"NPfe .CQF5W/ߖ?fQn*;Y:?ykgzq99)G?<|ZT)Fuxlvf?f *kfm*.KaUee"'WZs}Ӭ5ޟ~{]$矋ufu?i߲جd_Bߨ_TaAUj]*^B- Z5 z)4*nFz1?5y.:͓ .@L̬[9ц[)Rdo'J0۪C}ۜcu,;lV:δ2V iE(%d(2j"A chW2C!%AhIEN5az-XCGDVl[)%ۊSE8["\妦je8 KGQi 2,kvJpqEIH0:( 1 c6ܕ}qlֹAe{`hh~4s|v[g|] =kjPә@:.SI 6RQH޷aBh I(3 x]>fG^$}uT Q;ojRUhJT)(&퀳bg}M̘jU:܎TȈdiZM3!BڴQH& #D>"8LYtۆq4K +n 0VYn h4 1I [ZoDd \tI°ҳ9xǂYqx✢IAiՌX;կC5e-&=TjeLMѢwUƿ9{ _Ϲ~L3E57yU=LR.o/kz쏷r\GNl5GZOI`ԯJr K.zV|(7J8^U߰]Я j)TF^=v;LH|f6,TU&3^3[aʚ(8Ƣ;.f^;xVK]S/dYԌȫ p5޸6"9YtZ1+.)DF#m0!@ٖ 1ⴔK}`=P@* @Ԕ i]dTQ!"\]JVI DҙـZ n\, {ƭ[,)=d}v8a{FMu95m7|||~:ell{[l9$%1&#[ 0^_)B/K-8qズ$uЌy|S[)r#7SxԶamB~|2Y\%NO'ڴ+q>= C*01h])"{G =TRTե{Tm"f8uɈQX֊&hڤպX IQE@Q  <,0 AfZyVE VՒQUB-Z<5 F#6L nX/G#Sd>ʒ a>lxcrs7b - EnRѩmOؒVMU1|!71kQZ' Vb` !DUJt[PTmֲdfiNZR9o[5UH# ,0(/h *+k5) UBBAA퓑eq0.Vm@8d6"4ن, W"d4@Ar*i@L" Mh”)y0CFya:\Jf^T .ܽ=v`n)e/~*{L?^wn.]wϜ3}gڿ)t1gT0&%BQ2 dMl@ lQx[el%]/F#(JqrI7Mnix.#. t=9Յ8$XE*梠g D CfRy`w^[&Q7`]i5@F.sSo>`z9N#eۥGЎшA+! ~ > _kI@$EG`a<\t9Gf4LU#Bavʺ~9)X2slSgQNg0S+ֳZ<$m1mڪE!Vi]`&4v*dQͤL]0`VV-,e( D8P iѭC0FUXletAifrƭ,2{Xk`! < 1aVH9rWWd/\g]o/iüWnHaEX 5CMCyߗ=8wu/z]u1y~I@c C3ngLBC tc.cAlբ 0 `I &:R,w B8>i D~)`sΞfd F+J!ZM$YuMO3#ݵn uy*~ze/^dV2hJ r`+FvѝD`6ÂĈ6+8ZIq3$*dZ\5P4KDYP&E+J1D)Մ̐tF#U9ОfAf95@}1G9Ib^4s?/ac`z a[[|c6R" `j,M5`d%L hj܂rͱ",K2Z ?|< xB@W}ɁijT?SPVd|sSa vw W>O߯{:݀ΐuZX%\YYd# c Uժc$Da*xU$ƻ *QS@/D=Za'fEAbTDYѮ@(f'K;,dN b e*kͤv\?3Qy.DYagqtdh.aaAޚS/Ww8&)?59L;ӓv1 VOZ9{K<?@oIS ' XR b@:_by ĿѡAs. 5Yt7aDXZ3 "*;C·jZm8`Kɽ]IQQ$q1bzJ.݈ ~6f R]w̍ԑ8cq2łTAip!C^B.nTiQ@r0G[2WUE Ym)R8n|7^SASOf`+. 򐕠 OggS@ookMX\_Xblpos@=]:X[|4.hM|l.\F-\[뗛bPo>l~x;p& `4 .@/RS'i)_!o}jƋEX,-n1O_Ue.|r|n1ut~66/qA6}3PQFXMcSwU]etw{[% %Eb'B0Qe=r>53g:ǎX05Q#ڕH&=FAk"c0ZEc[ݫbD]D0& -U.mBRa坍2ZXa&KayJ4UzYU\o/gwm7M0fN(c2&f7So1ۦ70Te]^&NQL]a?S3;;V}9f d0NdR/Ơ9*QsNx!?TIxm^نy,*eyrU$^ U_bڲǼDzԦ8@~p(P J͉Ȍ?G۪jUYmHYl[Zښ Vc!Ia{uLWdj8F,- P()ze))d7Laq(- 5bAHUlRk*FB6r1 . 9B`p j{XY+ DF`04Qh<(R/)KO=Oy_5]9lHwuw˗ 7`Z^,Pݜ f  VAC @",3?M}V4$ưgj!C) %A?:L`D~Hitb[5ǒ04S$jMk*)}Rb%Gъb-BU!mj4r5$&da1 .bh-B  e&d$TrfR"A"H.dd>LH(%nYg"&i)UR Uu&{\tvw&q{ef$+,?r̘O盦",.J|ְ4Y1]6|vпX$yD6"_Qbɭj~VI i^ɿ}w)6_6|cζCM-,V)Fc}kvFd:-(:C b@F}l}vUѡ؊+lk),E5XH4KXө%\( .5M8ͪl.u@WGcdJ M61KHi " hzy+#vaXe5#V#[kRm&SਅYV0N X)E"+\2RZ)6 BR!4SD[XAR2v DEF0Zv5؉#x mh%hlV#٬eI!5.}tHt{x}q.kus7Ugpx}Lٶ k; n 0օAY+E+$![\4$ۘ|h^Ovv9IN?i ͒.MP?Ʉ##ɏ" <:T[3U3AQdT 3m [R*CQpΘv;5Ɣm }Zju]]:[іX ,G[vH+rq}B5k4 SYAr!2ٔk!Eer%@5B :ek)B-%G*R 0 JC.b '1(hMD@47$m.wn+Ϻδ-fإhf3=&yN7C 2[ޚ1cb@.A, y[(ޗxr2BdMR@~BNx~9 ߑkKmjGl Z |ئh_stTUMMPd$TQͯLt+22 + :b{ 1P,EPHօ^uחkШ6 %Խ?u 6ix D 0-`Á䦌HjR"Z:4 E,Mj;TK#8;Ă@w&j !&^ ]@^#VmTq6fHB2c!bb>4j{\^758d \ޱcvKL|0g`@7H,( AJkT:ю*l2΅dzK$0",he~i͚͡4K3B俩ue@`ŁHSaxF<0 vj<4C 70}F)|i7 )iI0AD 4{&',9%ůȫu)H7<L\ &Jn y~źS5 RU@G;Zg?ʪ\1,IPF8f h5BF~Q7ˡ.S\35aRbhH(pCE##@o-JV%3T8B,1 mtf"H  hDǽ HbA65Pduͤ򢤑bLsA{W/y7sXIë xpgAu'o@ϙŎ:K+0 htξr7/ в4fP8U ohEzvCL3VOggS@ok qz]tZXp) n NyMY;D|,k>fs~TUU\wYB,:@o`39CFe*3$,jdaa{V]CL]5}Ziuíx :lXULZ  Q Nq"0BX#]aNG N`lEFB\bejB. @VIIJHĆ6.HxtlTZ$0$|es. H1#1?!@) L%*FL'bNs"ͬ(qԚ6Rs&WHNơ=f]iZD4sc!gҹc]>4we$o|bx<^zέeiH&V3jGJmsEmd\D(EJ#$hs!RFAUhQ#VŲ8 z ҵJ@ LUmTd-8ypA(hf-$)U:M -E̐Ws?(#qCg{`zce EAgrj vv᳁ɓ;s7k iր:mA*.@RR (dg{-e"׊H͝1aދu]p!],j&Yo8`44ҟ@bn@' ö y] m&n֠V<`Ŷ.̄D^jLe,+1^(#TQ@tBkW׊RO]ǒT 4D.@,ԣj-#, OZieke,Me`޼L ֪2jGyLqiI|g@BOO1 Nܹ @ /jhMzA@IQa?T0* h4D5͟guFg" Y^ԇ'i.M]Kd5;kiP$ʭUaB;7<1iQ D1AF7n Rj!`-xՓ'UY4@![.-6:7 OTUER0T{3鞷#P%VF $nl9q.x ? [k֥_fMK~$E -w1j93@a!vzE %B~Q`!|A p2S.k7_|l<!@Dkx):4?O K ₐEvsZDjD}Y7cXF~m~='Є8b`! F1:bʢթ4K 8K?ipV]-A $,kƢ8NC6:D$ iuhkcE#J(0(Z)D{A7~#-ja+ehzn+BuIZilZ=ɓw/ݧ$M$xw{}t&)'蠐󆍿w?ldEcV.âޟ_8K LշmCUR Т ؆D籖8U%/e,`kXZvّ݈m#۪Z*&Y*M @A@ ) ɚ=PQE"1$AW  iY%52pXt`*ED )%@F$pk.-lcERH&B$AjzpYTjNP"0-yԚfG2H} ˰9O=i>9ۃrq:W|2cYdq*޶-@L};rU>LYllְYЭ mR6^ $y#_qmeg4$rMՈ15}?[ہt@{sֵ nELXve00SLJ6eXvUd7-.p #2*u`6Ych.xr0 "X``EJ(#`m3`tZju FdXQE,&&jEiA@\qL1ei& BPeW% rd`E;f͆Xc]9?ǭmL$P.8{bs'Ywd'P ,P |oZ]D J!>ږ9΅fK~\|Gd ^7~E\,]̫ T">\s_TMC1 A^z <5"fjf+CPYaG%g3Z\579cE 9+ʂEģ*n+*zl:Z( i*6*qR,qVKC$5&A14 %hq@c~dH)"2+Hnjs\XZew)4̬17@Zc<\cf)/8ʴ}xw4c(g~*1ЕiU #WV|*86 XЀ P$QlxP"#j)qnkxKwE>A+P!-Я@3&r!pַ%ئ(?BL+ɇoZU kc!p!6l.#u{(bEV3ѰiTu.ȓz{} =x*H(햏[ EEaK(z7-e]R[lW#E[PC $NAtH/IY BڰT)ŸdO8rHka) Ǥ L-olzhu5}bϚrm 0 `vM`wM]`Vsi BnL#O* i8X.aldxVnVqK2yq9_ T:@G.Af%~ )ja߈@)+Wl99#0sVU[F!gayv0F՚M7kg8|,4QCMbnZ( ݕRHfeMMg ҈ڮctX D@P hy=N_:U2@XP1 BrMFt$qΉ6iBi aIRTt"(1Tݒ3I;^=Jҟu,2yd燃{'#䓭΄ie4$]Sl 0W5l'lS ]UGaF i8a|rkjT6;mP7qQfp1Q?gaOiQժ5,#Ƴ;wZoF17 M>9R0.#ְymQ]alcYf -ti&gNRX$F @W3ٙA.a U@BPa VRT2ZHM (C5# 4V%!@zmT =EL'+SEa993D!= )^5m UOY0/oH{̋]j^^M4J*3\gl8 8T7.P;y@(bE' d.-ڥ K((e^(˶`$N5.^>l'1l5,o+!.6li`Xm#T42~xs ƛA-fAТ.މj,FAY}Y 6El7&G6Pvcmͺ:ONXDAMIzLv`piBrbSTP˪y*ڶ,3SjXVP hw;P8LaLRv4qzJw>=O:TE~+G:1UP2ݨýƗ~Auyݝϔ-t'ۺg,1 2IN;#Y\HE! yl@_n BƑJQnf˜HO*HI09,yhJP81{2L+hH&<|j= }gsf͌+b㌘aFVt]۠F[1%[:2! t^hYh DЏSM+kH(DAF74@ah "`T#bEJX*X`Vֶ[!7(Z+,RmK+@WP)Lmmkyiddq6 )ihi9' &SErY_YC9\7k򻪇JZQLFaJ@&-wW:"4)Rr{.a4nLqԍ^Ml4ж-(XbRw+X_DQcDžQPAQt(@!) AX4PԒGC0%jY)堥l1ZWpH{?ef=fXܴM׬o/],鶭M-$%n`OrMT *kBenaG$bt L#GD@bQ|;ۋ ?w >{&6^Z,XFgW\P5Lb:}k[ۆQƮR mkϩMΥ85XYxR[-DrK=/ę{`MF7u,p@IR XX `xGeՄ&-ɬ%@:9Er[j4kD-Ve i$*)*"qSVn5ّ8ʇlܫ&'Vc%.!v69@;s~0gzf{cownk3`k S_}z?4$A,d 4eG*R0(TDE+J>,D濈Po~{ FfsE'l+ikmObCUM<3KA+b`Snvj*W li>;iSZg,z*bBȠA2Ga9.bɊP8"dLDL]bXV aV_@Dthԍdp#w2qC `@8-Z"`{;&vS438Mfq`vĕ3 uT@C`9 ₝Y|wUtr+e~`! ,7$$`ŽW_+bsl,0y;24HzKOZs]{D 4'2|u,vbvA]&LXL*p6I\¶猚io-w `YmgFw5:o3E6N-IsE ͢` =1>9[VKVfXM$u٢ H P4-3XH5 ,Ph R;*4 @`DEf)T  h^d]9δ&4,^amutagen-1.22/tests/data/silence-44-s-mpeg25.mp30000644000175000017500000001056012157371172021234 0ustar lazkalazka00000000000000DdXingPp "%(++.0399wiCud+0#8F}0?I{id  $D0Fjd #` FzQ}d#nX8Fed \ DoIOOAZd T" Ko_Od  ,F ?Pd"(8F?|Ad%B(D 6dL"(D}xho(d,x#8F(Z((ed""DzoӾiQd` #(F?֕jd #miGBVJ$d0cԏIRUڐoj5vc?d,"85PZ٫rd'tC (D*<_ߠHdF$DF[n0D#dcp F*OPں1$d#<D-J~jG,skVJqd!"8,FCx#%~Ì?d+"^F]7d # FiRWOd"(jU$d @wЩ?GU Ohqd,X4D|_Gd)(#X(FGo<d%8c$Ft}s d"" D5SmdD"<DNj0~s_ԏbӻd"($FHQNJ$dX " 4D޿{ͺ*0}4n@]d,\c0(D& ?c*<d'@"H$]j5d#FOGAAWd,DOtd "h$F/Qd"@g5dxFW) LAME3.98.d H2UUUUUUUUUUUUUUUUUUUUUUUUUUmutagen-1.22/tests/data/silence-44-s.flac0000644000175000017500000014333012146763412020347 0ustar lazkalazka00000000000000fLaC"y+ BzbܷH2ĺJl.L8VavL reference libFLAC 1.1.0 20030126album=Quod Libet Test Data artist=piman artist=jzig genre=Silencetracknumber=02/10 date=2004 title=SilenceL1234567890123X123456789012DLXz image/pngA pixel.PNG  IHDRwS pHYs  tIME  6D=2tEXtCommentCreated with The GIMPd%n IDATc?YIENDB` Yk?O?s???????9>?3y?̟>?O????ϟy??ϟ??'<3??O>I??Ny||??C3'?????????????g|????s?|g?yg????|ϟ???'?'????9????sϟ>?'Oy?4uYl??93??>?ϟ?y3???O?y?????9?93???<<3O???<9ϓ???9?~O??g?~?ϞNO'Cɟϓsϟ9?g'??|?ϙ?'?3|?'?????yϟ?g?9?|?gg??>|~sy?|?Cs|??N̟?3g???>sg?L??3O?3y???>3ϟ?'33?~||g???O?|gg~gg?ϟ?g>sϓ9?L><??g??<~?<?'3??g?XYw??gϟ?|?s??y??2gg???????ɟ??s>|?9???3?? ?s|???93???????<O?fss??>?~g??'g???~g???3g?3??ϟ??s Yy???s?9ϟϟ9<?|???9??|>~Ϝ?yg?3???f3>??''???|?'????$??'>'?'9??9?9|y'|??|@O?<|??>3s??&~3???????<~r'??s~???'???s3ϟ??ϙ?9?|'?N<??Y~???O??'<?????O???9?????|''~?y???3>????s?????9??93?ϟ?y3???O?y????????<<3O???<9ϓ???9?~O??Cg?<3'?9gy?s?~'?y??9y>?>O???3?9?y|'s?<9O?Ϟ|9?~?g?3???zY T?y??<>yIg9<3??>|?gg??>|~s?'???f??'O>?>s3<?L??3O???s???ɟ???y<????y?9g??s<=Y ]&gO??|ys??g9?>~gO?~????|gg~gg??ϟ?>sϓ9?L><??g??<~?<?'&Y Z???y??r??gϟ?|?s??y??2g~?9~y?yO?g?'O<s's????O̓?$??9~?>???|||9??=Y O33y?99ϟy???3s<'?????~<'s???????g??ϟ????9?O?ɟ??>?9??'??'?y??y??9??39gsϟ'??|?ssO<3'??3???yϟd~?yϟ??3???~??O@1Y H?'?????>Ny?<N??~?Oys3?3ϟg???>s?>9?ssy|~???9?s'???~O9????'??|ϟ?|?II?D<<O?<|??>3s??&~3???9??<~r'??s~???'???s3ϟ??ϙ?9*YA???|'???3?sO?s???????9>??s????9<'??$?????ϟy??ϟ??'<3@?????~?|9>s~?9ϟϟs??????3?g?'?????9?????g>s|~|???~sO3kYF?g9?<???~g?|y?>3?'g?OO??3??ϓs?>g?y???'??|<3????gyϟ?|'Oɟϓsϟ9?g'??|?ϙ?'?3|?'???ϟsO'gL?Oy????ϟ???>yϟ?g?9?O??|y??<>yIg9<3??>|?gg??>|~s?>s3<9?s~??$3y???>3ϟ?'3Y'y3?~~gO?~????|?3ϓ?9??>L??93????'??s?C~?<?'3??g?ϟN???????f????g?????$s???y?~?93>y?ϟ????$???9?f~?????'~????'|?'3yy>??ϟ??><?3<'y<~>g?>3|>|?????|???|?fg???|?ssO?9???3????s?9ϟϟ9<?|???9??|>~?|ϟϟ~'?3??g?<~O???|?'????$??'???<<O?<|??>332g>????9??<~r'??s~???'???sY?|?II???|'???3?sO?s???????'O??>?s????9<'??$?????ϟy?93ϟ??ϙ?9?|'?N<???????3OI????O???3?g?'|?yO9ϟy~'?O?D|~|???~sO3?33O9>?s?9???r??s~rs?rs??'|?'?????|?Y?9?~||?>s$??????3>I????s~~~~s?|9?~?g?3??????s|???>?'???f??'O>?>s3<9?s~??$3y???>i Y#'~yy?????g??O?9??g?ϙ?y???>y?|?9O|?????????>?'?<>?~?ϟ????'??O@3ϟ?'33?~||g???O?|gsϓ9?L|Y$33??ɟy??????y??r??gϟ?|?s~s?9?>~y&>|?O??~|?y??~?'??????ɟ??~?9~y?yO?g??~???O?NY-?s??9?f~??y332?'????'|?'3yy>?y??3'??ϟ9?????9?ϟ???ϟ?'?ϟ?pY*???ϟ?rg&|??Or>??Oys3?3ϟg???>s?>9?ssyOϟ|?????|ϟ??>O9?s?|??O????9??~O39???gg3Y?9???ϟ????39???9?????<?9???????9>?3y?̟>?Oϟs???'???s3ϟ??ϙ?9?|'?N<???????3Oϟs??????3?g?'|?yO9ϟy~'?????3?>s9O̜??'?3~gO?y9?'?9??3~ϟ???O?s?g?sLy?????<|@y?'??9??y?y9?????39??<g?<3'?9gy?sg'??>gO?3??9?y|'s?<9SY1??~>O?9?~||?>s$??????3>I??C>?9ɟϟ??y?yϟ'??>?~?3????|0|??N̟?3g???>sg?L??3O???s???Y6'??s??|ϟg~gg?ϟ?g3??>OY ?Oy????d3??ɟy??????y??r??gϟ????|3?~s?9?>~y&>|?O??~|?y??~?'???@?9??>L??93?~?9~y?<~g???~???O????|||9???'??'?y??y??9?|>|?????|???|?fg????????9>3'??3???yY"?9???3????sϟ??'|sgg3|ϟϟ~'?3??g?<~O???|?Bg??'g???~g???3g?3??ϟ??s???????????????ϟ?|ɟ??<?y?y#jUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUPTUU*UUUtTUWҪUU]UUURUUU]UU]RꪪuҪWUUUUU]UUUUWUUUUUUwUUtUUjUUUUtUUrҪuUUUUUUURUUUT]UU\uUʾUWʪUUUUrꪪUU]U)URRUUUUU%UTUUUUUWUUUUU*.UUUUVUU]UjUUUU]U*UU*UW*uUU]URUW]UTUUUJUU]jU*U.ꪪUU]UUUUPmutagen-1.22/tests/test_mp4.py0000644000175000017500000005726712211635644016624 0ustar lazkalazka00000000000000import os import shutil import struct from cStringIO import StringIO from tempfile import mkstemp from tests import TestCase, add from mutagen.mp4 import MP4, Atom, Atoms, MP4Tags, MP4Info, \ delete, MP4Cover, MP4MetadataError, MP4FreeForm, error from mutagen._util import cdata try: from os.path import devnull except ImportError: devnull = "/dev/null" class TAtom(TestCase): def test_no_children(self): fileobj = StringIO("\x00\x00\x00\x08atom") atom = Atom(fileobj) self.failUnlessRaises(KeyError, atom.__getitem__, "test") def test_length_1(self): fileobj = StringIO("\x00\x00\x00\x01atom" "\x00\x00\x00\x00\x00\x00\x00\x10" + "\x00" * 16) self.failUnlessEqual(Atom(fileobj).length, 16) def test_length_64bit_less_than_16(self): fileobj = StringIO("\x00\x00\x00\x01atom" "\x00\x00\x00\x00\x00\x00\x00\x08" + "\x00" * 8) self.assertRaises(error, Atom, fileobj) def test_length_less_than_8(self): fileobj = StringIO("\x00\x00\x00\x02atom") self.assertRaises(MP4MetadataError, Atom, fileobj) def test_render_too_big(self): class TooBig(str): def __len__(self): return 1L << 32 data = TooBig("test") try: len(data) except OverflowError: # Py_ssize_t is still only 32 bits on this system. self.failUnlessRaises(OverflowError, Atom.render, "data", data) else: data = Atom.render("data", data) self.failUnlessEqual(len(data), 4 + 4 + 8 + 4) def test_non_top_level_length_0_is_invalid(self): data = StringIO(struct.pack(">I4s", 0, "whee")) self.assertRaises(MP4MetadataError, Atom, data, level=1) def test_length_0(self): fileobj = StringIO("\x00\x00\x00\x00atom" + 40 * "\x00") atom = Atom(fileobj) self.failUnlessEqual(fileobj.tell(), 48) self.failUnlessEqual(atom.length, 48) def test_length_0_container(self): data = StringIO(struct.pack(">I4s", 0, "moov") + Atom.render("data", "whee")) atom = Atom(data) self.failUnlessEqual(len(atom.children), 1) self.failUnlessEqual(atom.length, 20) self.failUnlessEqual(atom.children[-1].length, 12) add(TAtom) class TAtoms(TestCase): filename = os.path.join("tests", "data", "has-tags.m4a") def setUp(self): self.atoms = Atoms(open(self.filename, "rb")) def test_getitem(self): self.failUnless(self.atoms["moov"]) self.failUnless(self.atoms["moov.udta"]) self.failUnlessRaises(KeyError, self.atoms.__getitem__, "whee") def test_contains(self): self.failUnless("moov" in self.atoms) self.failUnless("moov.udta" in self.atoms) self.failUnless("whee" not in self.atoms) def test_name(self): self.failUnlessEqual(self.atoms.atoms[0].name, "ftyp") def test_children(self): self.failUnless(self.atoms.atoms[2].children) def test_no_children(self): self.failUnless(self.atoms.atoms[0].children is None) def test_extra_trailing_data(self): data = StringIO(Atom.render("data", "whee") + "\x00\x00") self.failUnless(Atoms(data)) def test_repr(self): repr(self.atoms) add(TAtoms) class TMP4Info(TestCase): def test_no_soun(self): self.failUnlessRaises( IOError, self.test_mdhd_version_1, "vide") def test_mdhd_version_1(self, soun="soun"): mdhd = Atom.render("mdhd", ("\x01\x00\x00\x00" + "\x00" * 16 + "\x00\x00\x00\x02" + # 2 Hz "\x00\x00\x00\x00\x00\x00\x00\x10")) hdlr = Atom.render("hdlr", "\x00" * 8 + soun) mdia = Atom.render("mdia", mdhd + hdlr) trak = Atom.render("trak", mdia) moov = Atom.render("moov", trak) fileobj = StringIO(moov) atoms = Atoms(fileobj) info = MP4Info(atoms, fileobj) self.failUnlessEqual(info.length, 8) def test_multiple_tracks(self): hdlr = Atom.render("hdlr", "\x00" * 8 + "whee") mdia = Atom.render("mdia", hdlr) trak1 = Atom.render("trak", mdia) mdhd = Atom.render("mdhd", ("\x01\x00\x00\x00" + "\x00" * 16 + "\x00\x00\x00\x02" + # 2 Hz "\x00\x00\x00\x00\x00\x00\x00\x10")) hdlr = Atom.render("hdlr", "\x00" * 8 + "soun") mdia = Atom.render("mdia", mdhd + hdlr) trak2 = Atom.render("trak", mdia) moov = Atom.render("moov", trak1 + trak2) fileobj = StringIO(moov) atoms = Atoms(fileobj) info = MP4Info(atoms, fileobj) self.failUnlessEqual(info.length, 8) add(TMP4Info) class TMP4Tags(TestCase): def wrap_ilst(self, data): ilst = Atom.render("ilst", data) meta = Atom.render("meta", "\x00" * 4 + ilst) data = Atom.render("moov", Atom.render("udta", meta)) fileobj = StringIO(data) return MP4Tags(Atoms(fileobj), fileobj) def test_genre(self): data = Atom.render("data", "\x00" * 8 + "\x00\x01") genre = Atom.render("gnre", data) tags = self.wrap_ilst(genre) self.failIf("gnre" in tags) self.failUnlessEqual(tags["\xa9gen"], ["Blues"]) def test_empty_cpil(self): cpil = Atom.render("cpil", Atom.render("data", "\x00" * 8)) tags = self.wrap_ilst(cpil) self.failUnless("cpil" in tags) self.failIf(tags["cpil"]) def test_genre_too_big(self): data = Atom.render("data", "\x00" * 8 + "\x01\x00") genre = Atom.render("gnre", data) tags = self.wrap_ilst(genre) self.failIf("gnre" in tags) self.failIf("\xa9gen" in tags) def test_strips_unknown_types(self): data = Atom.render("data", "\x00" * 8 + "whee") foob = Atom.render("foob", data) tags = self.wrap_ilst(foob) self.failIf(tags) def test_strips_bad_unknown_types(self): data = Atom.render("datA", "\x00" * 8 + "whee") foob = Atom.render("foob", data) tags = self.wrap_ilst(foob) self.failIf(tags) def test_bad_covr(self): data = Atom.render("foob", "\x00\x00\x00\x0E" + "\x00" * 4 + "whee") covr = Atom.render("covr", data) self.failUnlessRaises(MP4MetadataError, self.wrap_ilst, covr) def test_covr_blank_format(self): data = Atom.render("data", "\x00\x00\x00\x00" + "\x00" * 4 + "whee") covr = Atom.render("covr", data) tags = self.wrap_ilst(covr) self.failUnlessEqual(MP4Cover.FORMAT_JPEG, tags["covr"][0].imageformat) def test_render_bool(self): self.failUnlessEqual(MP4Tags()._MP4Tags__render_bool('pgap', True), "\x00\x00\x00\x19pgap\x00\x00\x00\x11data" "\x00\x00\x00\x15\x00\x00\x00\x00\x01") self.failUnlessEqual(MP4Tags()._MP4Tags__render_bool('pgap', False), "\x00\x00\x00\x19pgap\x00\x00\x00\x11data" "\x00\x00\x00\x15\x00\x00\x00\x00\x00") def test_render_text(self): self.failUnlessEqual( MP4Tags()._MP4Tags__render_text('purl', ['http://foo/bar.xml'], 0), "\x00\x00\x00*purl\x00\x00\x00\"data\x00\x00\x00\x00\x00\x00" "\x00\x00http://foo/bar.xml") self.failUnlessEqual( MP4Tags()._MP4Tags__render_text('aART', [u'\u0041lbum Artist']), "\x00\x00\x00$aART\x00\x00\x00\x1cdata\x00\x00\x00\x01\x00\x00" "\x00\x00\x41lbum Artist") self.failUnlessEqual( MP4Tags()._MP4Tags__render_text('aART', [u'Album Artist', u'Whee']), "\x00\x00\x008aART\x00\x00\x00\x1cdata\x00\x00\x00\x01\x00\x00" "\x00\x00Album Artist\x00\x00\x00\x14data\x00\x00\x00\x01\x00" "\x00\x00\x00Whee") def test_render_data(self): self.failUnlessEqual( MP4Tags()._MP4Tags__render_data('aART', 1, ['whee']), "\x00\x00\x00\x1caART" "\x00\x00\x00\x14data\x00\x00\x00\x01\x00\x00\x00\x00whee") self.failUnlessEqual( MP4Tags()._MP4Tags__render_data('aART', 2, ['whee', 'wee']), "\x00\x00\x00/aART" "\x00\x00\x00\x14data\x00\x00\x00\x02\x00\x00\x00\x00whee" "\x00\x00\x00\x13data\x00\x00\x00\x02\x00\x00\x00\x00wee") def test_bad_text_data(self): data = Atom.render("datA", "\x00\x00\x00\x01\x00\x00\x00\x00whee") data = Atom.render("aART", data) self.failUnlessRaises(MP4MetadataError, self.wrap_ilst, data) def test_render_freeform(self): self.failUnlessEqual( MP4Tags()._MP4Tags__render_freeform( '----:net.sacredchao.Mutagen:test', ['whee', 'wee']), "\x00\x00\x00a----" "\x00\x00\x00\"mean\x00\x00\x00\x00net.sacredchao.Mutagen" "\x00\x00\x00\x10name\x00\x00\x00\x00test" "\x00\x00\x00\x14data\x00\x00\x00\x01\x00\x00\x00\x00whee" "\x00\x00\x00\x13data\x00\x00\x00\x01\x00\x00\x00\x00wee") def test_bad_freeform(self): mean = Atom.render("mean", "net.sacredchao.Mutagen") name = Atom.render("name", "empty test key") bad_freeform = Atom.render("----", "\x00" * 4 + mean + name) self.failUnlessRaises(MP4MetadataError, self.wrap_ilst, bad_freeform) def test_pprint_non_text_list(self): tags = MP4Tags() tags["tmpo"] = [120, 121] tags["trck"] = [(1, 2), (3, 4)] tags.pprint() def test_freeform_data(self): # http://code.google.com/p/mutagen/issues/detail?id=103 key = "----:com.apple.iTunes:Encoding Params" value = ("vers\x00\x00\x00\x01acbf\x00\x00\x00\x01brat\x00\x01\xf4" "\x00cdcv\x00\x01\x05\x04") data = ("\x00\x00\x00\x1cmean\x00\x00\x00\x00com.apple.iTunes\x00\x00" "\x00\x1bname\x00\x00\x00\x00Encoding Params\x00\x00\x000data" "\x00\x00\x00\x00\x00\x00\x00\x00vers\x00\x00\x00\x01acbf\x00" "\x00\x00\x01brat\x00\x01\xf4\x00cdcv\x00\x01\x05\x04") tags = self.wrap_ilst(Atom.render("----", data)) v = tags[key][0] self.failUnlessEqual(v, value) self.failUnlessEqual(v.dataformat, MP4FreeForm.FORMAT_DATA) data = MP4Tags()._MP4Tags__render_freeform(key, v) v = self.wrap_ilst(data)[key][0] self.failUnlessEqual(v.dataformat, MP4FreeForm.FORMAT_DATA) data = MP4Tags()._MP4Tags__render_freeform(key, value) v = self.wrap_ilst(data)[key][0] self.failUnlessEqual(v.dataformat, MP4FreeForm.FORMAT_TEXT) add(TMP4Tags) class TMP4(TestCase): def setUp(self): fd, self.filename = mkstemp(suffix='.m4a') os.close(fd) shutil.copy(self.original, self.filename) self.audio = MP4(self.filename) def faad(self): if not have_faad: return value = os.system("faad %s -o %s > %s 2> %s" % ( self.filename, devnull, devnull, devnull)) self.failIf(value and value != NOTFOUND) def test_score(self): fileobj = open(self.filename) header = fileobj.read(128) self.failUnless(MP4.score(self.filename, fileobj, header)) def test_channels(self): self.failUnlessEqual(self.audio.info.channels, 2) def test_sample_rate(self): self.failUnlessEqual(self.audio.info.sample_rate, 44100) def test_bits_per_sample(self): self.failUnlessEqual(self.audio.info.bits_per_sample, 16) def test_bitrate(self): self.failUnlessEqual(self.audio.info.bitrate, 2914) def test_length(self): self.failUnlessAlmostEqual(3.7, self.audio.info.length, 1) def test_padding(self): self.audio["\xa9nam"] = u"wheeee" * 10 self.audio.save() size1 = os.path.getsize(self.audio.filename) self.audio["\xa9nam"] = u"wheeee" * 11 self.audio.save() size2 = os.path.getsize(self.audio.filename) self.failUnless(size1, size2) def test_padding_2(self): self.audio["\xa9nam"] = u"wheeee" * 10 self.audio.save() # Reorder "free" and "ilst" atoms fileobj = open(self.audio.filename, "rb+") atoms = Atoms(fileobj) meta = atoms["moov", "udta", "meta"] meta_length1 = meta.length ilst = meta["ilst",] free = meta["free",] self.failUnlessEqual(ilst.offset + ilst.length, free.offset) fileobj.seek(ilst.offset) ilst_data = fileobj.read(ilst.length) fileobj.seek(free.offset) free_data = fileobj.read(free.length) fileobj.seek(ilst.offset) fileobj.write(free_data + ilst_data) fileobj.close() fileobj = open(self.audio.filename, "rb+") atoms = Atoms(fileobj) meta = atoms["moov", "udta", "meta"] ilst = meta["ilst",] free = meta["free",] self.failUnlessEqual(free.offset + free.length, ilst.offset) fileobj.close() # Save the file self.audio["\xa9nam"] = u"wheeee" * 11 self.audio.save() # Check the order of "free" and "ilst" atoms fileobj = open(self.audio.filename, "rb+") atoms = Atoms(fileobj) fileobj.close() meta = atoms["moov", "udta", "meta"] ilst = meta["ilst",] free = meta["free",] self.failUnlessEqual(meta.length, meta_length1) self.failUnlessEqual(ilst.offset + ilst.length, free.offset) def set_key(self, key, value, result=None, faad=True): self.audio[key] = value self.audio.save() audio = MP4(self.audio.filename) self.failUnless(key in audio) self.failUnlessEqual(audio[key], result or value) if faad: self.faad() def test_unicode(self): self.set_key('\xa9nam', ['\xe3\x82\x8a\xe3\x81\x8b'], result=[u'\u308a\u304b']) def test_save_text(self): self.set_key('\xa9nam', [u"Some test name"]) def test_save_texts(self): self.set_key('\xa9nam', [u"Some test name", u"One more name"]) def test_freeform(self): self.set_key('----:net.sacredchao.Mutagen:test key', ["whee"]) def test_freeform_2(self): self.set_key('----:net.sacredchao.Mutagen:test key', "whee", ["whee"]) def test_freeforms(self): self.set_key('----:net.sacredchao.Mutagen:test key', ["whee", "uhh"]) def test_freeform_bin(self): self.set_key('----:net.sacredchao.Mutagen:test key', [ MP4FreeForm('woooo', MP4FreeForm.FORMAT_TEXT), MP4FreeForm('hoooo', MP4FreeForm.FORMAT_DATA), MP4FreeForm('boooo'), ]) def test_tracknumber(self): self.set_key('trkn', [(1, 10)]) self.set_key('trkn', [(1, 10), (5, 20)], faad=False) self.set_key('trkn', []) def test_disk(self): self.set_key('disk', [(18, 0)]) self.set_key('disk', [(1, 10), (5, 20)], faad=False) self.set_key('disk', []) def test_tracknumber_too_small(self): self.failUnlessRaises(ValueError, self.set_key, 'trkn', [(-1, 0)]) self.failUnlessRaises(ValueError, self.set_key, 'trkn', [(2**18, 1)]) def test_disk_too_small(self): self.failUnlessRaises(ValueError, self.set_key, 'disk', [(-1, 0)]) self.failUnlessRaises(ValueError, self.set_key, 'disk', [(2**18, 1)]) def test_tracknumber_wrong_size(self): self.failUnlessRaises(ValueError, self.set_key, 'trkn', (1,)) self.failUnlessRaises(ValueError, self.set_key, 'trkn', (1, 2, 3,)) self.failUnlessRaises(ValueError, self.set_key, 'trkn', [(1,)]) self.failUnlessRaises(ValueError, self.set_key, 'trkn', [(1, 2, 3,)]) def test_disk_wrong_size(self): self.failUnlessRaises(ValueError, self.set_key, 'disk', [(1,)]) self.failUnlessRaises(ValueError, self.set_key, 'disk', [(1, 2, 3,)]) def test_tempo(self): self.set_key('tmpo', [150]) self.set_key('tmpo', []) def test_tempos(self): self.set_key('tmpo', [160, 200], faad=False) def test_tempo_invalid(self): for badvalue in [[10000000], [-1], 10, "foo"]: self.failUnlessRaises(ValueError, self.set_key, 'tmpo', badvalue) def test_compilation(self): self.set_key('cpil', True) def test_compilation_false(self): self.set_key('cpil', False) def test_gapless(self): self.set_key('pgap', True) def test_gapless_false(self): self.set_key('pgap', False) def test_podcast(self): self.set_key('pcst', True) def test_podcast_false(self): self.set_key('pcst', False) def test_cover(self): self.set_key('covr', ['woooo']) def test_cover_png(self): self.set_key('covr', [ MP4Cover('woooo', MP4Cover.FORMAT_PNG), MP4Cover('hoooo', MP4Cover.FORMAT_JPEG), ]) def test_podcast_url(self): self.set_key('purl', ['http://pdl.warnerbros.com/wbie/justiceleagueheroes/audio/JLH_EA.xml']) def test_episode_guid(self): self.set_key('catg', ['falling-star-episode-1']) def test_pprint(self): self.failUnless(self.audio.pprint()) def test_pprint_binary(self): self.audio["covr"] = "\x00\xa9\garbage" self.failUnless(self.audio.pprint()) def test_pprint_pair(self): self.audio["cpil"] = (1, 10) self.failUnless("cpil=(1, 10)" in self.audio.pprint()) def test_delete(self): self.audio.delete() audio = MP4(self.audio.filename) self.failIf(audio.tags) self.faad() def test_module_delete(self): delete(self.filename) audio = MP4(self.audio.filename) self.failIf(audio.tags) self.faad() def test_reads_unknown_text(self): self.set_key("foob", [u"A test"]) def __read_offsets(self, filename): fileobj = open(filename, 'rb') atoms = Atoms(fileobj) moov = atoms['moov'] samples = [] for atom in moov.findall('stco', True): fileobj.seek(atom.offset + 12) data = fileobj.read(atom.length - 12) fmt = ">%dI" % cdata.uint_be(data[:4]) offsets = struct.unpack(fmt, data[4:]) for offset in offsets: fileobj.seek(offset) samples.append(fileobj.read(8)) for atom in moov.findall('co64', True): fileobj.seek(atom.offset + 12) data = fileobj.read(atom.length - 12) fmt = ">%dQ" % cdata.uint_be(data[:4]) offsets = struct.unpack(fmt, data[4:]) for offset in offsets: fileobj.seek(offset) samples.append(fileobj.read(8)) try: for atom in atoms["moof"].findall('tfhd', True): data = fileobj.read(atom.length - 9) flags = cdata.uint_be("\x00" + data[:3]) if flags & 1: offset = cdata.ulonglong_be(data[7:15]) fileobj.seek(offset) samples.append(fileobj.read(8)) except KeyError: pass fileobj.close() return samples def test_update_offsets(self): aa = self.__read_offsets(self.original) self.audio["\xa9nam"] = "wheeeeeeee" self.audio.save() bb = self.__read_offsets(self.filename) for a, b in zip(aa, bb): self.failUnlessEqual(a, b) def test_mime(self): self.failUnless("audio/mp4" in self.audio.mime) def tearDown(self): os.unlink(self.filename) class TMP4HasTags(TMP4): def test_save_simple(self): self.audio.save() self.faad() def test_shrink(self): map(self.audio.__delitem__, self.audio.keys()) self.audio.save() audio = MP4(self.audio.filename) self.failIf(audio.tags) def test_too_short(self): fileobj = open(self.audio.filename, "rb") try: atoms = Atoms(fileobj) ilst = atoms["moov.udta.meta.ilst"] # fake a too long atom length ilst.children[0].length += 10000000 self.failUnlessRaises(MP4MetadataError, MP4Tags, atoms, fileobj) finally: fileobj.close() def test_has_tags(self): self.failUnless(self.audio.tags) def test_not_my_file(self): # should raise something like "Not a MP4 file" self.failUnlessRaisesRegexp( error, "MP4", MP4, os.path.join("tests", "data", "empty.ogg")) class TMP4Datatypes(TMP4HasTags): original = os.path.join("tests", "data", "has-tags.m4a") def test_has_freeform(self): key = "----:com.apple.iTunes:iTunNORM" self.failUnless(key in self.audio.tags) ff = self.audio.tags[key] self.failUnlessEqual(ff[0].dataformat, MP4FreeForm.FORMAT_TEXT) def test_has_covr(self): self.failUnless('covr' in self.audio.tags) covr = self.audio.tags['covr'] self.failUnlessEqual(len(covr), 2) self.failUnlessEqual(covr[0].imageformat, MP4Cover.FORMAT_PNG) self.failUnlessEqual(covr[1].imageformat, MP4Cover.FORMAT_JPEG) add(TMP4Datatypes) class TMP4CovrWithName(TMP4): # http://bugs.musicbrainz.org/ticket/5894 original = os.path.join("tests", "data", "covr-with-name.m4a") def test_has_covr(self): self.failUnless('covr' in self.audio.tags) covr = self.audio.tags['covr'] self.failUnlessEqual(len(covr), 2) self.failUnlessEqual(covr[0].imageformat, MP4Cover.FORMAT_PNG) self.failUnlessEqual(covr[1].imageformat, MP4Cover.FORMAT_JPEG) add(TMP4CovrWithName) class TMP4HasTags64Bit(TMP4HasTags): original = os.path.join("tests", "data", "truncated-64bit.mp4") def test_has_covr(self): pass def test_bitrate(self): self.failUnlessEqual(self.audio.info.bitrate, 128000) def test_length(self): self.failUnlessAlmostEqual(0.325, self.audio.info.length, 3) def faad(self): # This is only half a file, so FAAD segfaults. Can't test. :( pass add(TMP4HasTags64Bit) class TMP4NoTagsM4A(TMP4): original = os.path.join("tests", "data", "no-tags.m4a") def test_no_tags(self): self.failUnless(self.audio.tags is None) def test_add_tags(self): self.audio.add_tags() self.failUnlessRaises(error, self.audio.add_tags) add(TMP4NoTagsM4A) class TMP4NoTags3G2(TMP4): original = os.path.join("tests", "data", "no-tags.3g2") def test_no_tags(self): self.failUnless(self.audio.tags is None) def test_sample_rate(self): self.failUnlessEqual(self.audio.info.sample_rate, 22050) def test_bitrate(self): self.failUnlessEqual(self.audio.info.bitrate, 32000) def test_length(self): self.failUnlessAlmostEqual(15, self.audio.info.length, 1) add(TMP4NoTags3G2) class TMP4UpdateParents64Bit(TestCase): original = os.path.join("tests", "data", "64bit.mp4") def setUp(self): fd, self.filename = mkstemp(suffix='.mp4') os.close(fd) shutil.copy(self.original, self.filename) def test_update_parents(self): file = open(self.filename) atoms = Atoms(file) self.assertEqual(77, atoms.atoms[0].length) self.assertEqual(61, atoms.atoms[0].children[0].length) tags = MP4Tags(atoms, file) tags['pgap'] = True tags.save(self.filename) file = open(self.filename) atoms = Atoms(file) # original size + 'pgap' size + padding self.assertEqual(77 + 25 + 974, atoms.atoms[0].length) self.assertEqual(61 + 25 + 974, atoms.atoms[0].children[0].length) def tearDown(self): os.unlink(self.filename) add(TMP4UpdateParents64Bit) NOTFOUND = os.system("tools/notarealprogram 2> %s" % devnull) have_faad = True if os.system("faad 2> %s > %s" % (devnull, devnull)) == NOTFOUND: have_faad = False print "WARNING: Skipping FAAD reference tests." mutagen-1.22/tests/test_easyid3.py0000644000175000017500000002576512212321247017453 0ustar lazkalazka00000000000000import os import shutil import pickle from tests import add, TestCase from mutagen.id3 import ID3FileType from mutagen.easyid3 import EasyID3, error as ID3Error from tempfile import mkstemp class TEasyID3(TestCase): def setUp(self): fd, self.filename = mkstemp('.mp3') os.close(fd) empty = os.path.join('tests', 'data', 'emptyfile.mp3') shutil.copy(empty, self.filename) self.id3 = EasyID3() def test_remember_ctr(self): empty = os.path.join('tests', 'data', 'emptyfile.mp3') mp3 = ID3FileType(empty, ID3=EasyID3) self.failIf(mp3.tags) mp3["artist"] = ["testing"] self.failUnless(mp3.tags) mp3.pprint() self.failUnless(isinstance(mp3.tags, EasyID3)) def test_delete(self): self.id3["artist"] = "foobar" self.id3.save(self.filename) self.failUnless(os.path.getsize(self.filename)) self.id3.delete(self.filename) self.failIf(os.path.getsize(self.filename)) self.failIf(self.id3) def test_pprint(self): self.id3["artist"] = "baz" self.id3.pprint() def test_has_key(self): self.failIf(self.id3.has_key("foo")) def test_empty_file(self): empty = os.path.join('tests', 'data', 'emptyfile.mp3') self.assertRaises(ID3Error, EasyID3, filename=empty) def test_nonexistent_file(self): empty = os.path.join('tests', 'data', 'does', 'not', 'exist') self.assertRaises(IOError, EasyID3, filename=empty) def test_write_single(self): for key in EasyID3.valid_keys: if key == "date": continue elif key.startswith("replaygain_"): continue # Test creation self.id3[key] = "a test value" self.id3.save(self.filename) id3 = EasyID3(self.filename) self.failUnlessEqual(id3[key], ["a test value"]) self.failUnlessEqual(id3.keys(), [key]) # And non-creation setting. self.id3[key] = "a test value" self.id3.save(self.filename) id3 = EasyID3(self.filename) self.failUnlessEqual(id3[key], ["a test value"]) self.failUnlessEqual(id3.keys(), [key]) del(self.id3[key]) def test_write_double(self): for key in EasyID3.valid_keys: if key == "date": continue elif key.startswith("replaygain_"): continue elif key == "musicbrainz_trackid": continue self.id3[key] = ["a test", "value"] self.id3.save(self.filename) id3 = EasyID3(self.filename) self.failUnlessEqual(id3.get(key), ["a test", "value"]) self.failUnlessEqual(id3.keys(), [key]) self.id3[key] = ["a test", "value"] self.id3.save(self.filename) id3 = EasyID3(self.filename) self.failUnlessEqual(id3.get(key), ["a test", "value"]) self.failUnlessEqual(id3.keys(), [key]) del(self.id3[key]) def test_write_date(self): self.id3["date"] = "2004" self.id3.save(self.filename) id3 = EasyID3(self.filename) self.failUnlessEqual(id3["date"], ["2004"]) self.id3["date"] = "2004" self.id3.save(self.filename) id3 = EasyID3(self.filename) self.failUnlessEqual(id3["date"], ["2004"]) def test_date_delete(self): self.id3["date"] = "2004" self.failUnlessEqual(self.id3["date"], ["2004"]) del(self.id3["date"]) self.failIf("date" in self.id3.keys()) def test_write_date_double(self): self.id3["date"] = ["2004", "2005"] self.id3.save(self.filename) id3 = EasyID3(self.filename) self.failUnlessEqual(id3["date"], ["2004", "2005"]) self.id3["date"] = ["2004", "2005"] self.id3.save(self.filename) id3 = EasyID3(self.filename) self.failUnlessEqual(id3["date"], ["2004", "2005"]) def test_write_invalid(self): self.failUnlessRaises(ValueError, self.id3.__getitem__, "notvalid") self.failUnlessRaises(ValueError, self.id3.__delitem__, "notvalid") self.failUnlessRaises( ValueError, self.id3.__setitem__, "notvalid", "tests") def test_perfomer(self): self.id3["performer:coder"] = ["piman", "mu"] self.id3.save(self.filename) id3 = EasyID3(self.filename) self.failUnlessEqual(id3["performer:coder"], ["piman", "mu"]) def test_no_performer(self): self.failIf("performer:foo" in self.id3) def test_performer_delete(self): self.id3["performer:foo"] = "Joe" self.id3["performer:bar"] = "Joe" self.failUnless("performer:foo" in self.id3) self.failUnless("performer:bar" in self.id3) del(self.id3["performer:foo"]) self.failIf("performer:foo" in self.id3) self.failUnless("performer:bar" in self.id3) del(self.id3["performer:bar"]) self.failIf("performer:bar" in self.id3) self.failIf("TMCL" in self.id3._EasyID3__id3) def test_performer_delete_dne(self): self.failUnlessRaises(KeyError, self.id3.__delitem__, "performer:bar") self.id3["performer:foo"] = "Joe" self.failUnlessRaises(KeyError, self.id3.__delitem__, "performer:bar") def test_txxx_empty(self): # http://code.google.com/p/mutagen/issues/detail?id=135 self.id3["asin"] = "" def test_txxx_set_get(self): self.failIf("asin" in self.id3.keys()) self.id3["asin"] = "Hello" self.failUnless("asin" in self.id3.keys()) self.failUnlessEqual(self.id3["asin"], ["Hello"]) self.failUnless("TXXX:ASIN" in self.id3._EasyID3__id3) def test_txxx_del_set_del(self): self.failIf("asin" in self.id3.keys()) self.failUnlessRaises(KeyError, self.id3.__delitem__, "asin") self.id3["asin"] = "Hello" self.failUnless("asin" in self.id3.keys()) self.failUnlessEqual(self.id3["asin"], ["Hello"]) del(self.id3["asin"]) self.failIf("asin" in self.id3.keys()) self.failUnlessRaises(KeyError, self.id3.__delitem__, "asin") def test_txxx_save(self): self.id3["asin"] = "Hello" self.id3.save(self.filename) id3 = EasyID3(self.filename) self.failUnlessEqual(id3["asin"], ["Hello"]) def test_txxx_unicode(self): self.id3["asin"] = u"He\u1234llo" self.failUnlessEqual(self.id3["asin"], [u"He\u1234llo"]) def test_bad_trackid(self): self.failUnlessRaises(ValueError, self.id3.__setitem__, "musicbrainz_trackid", ["a", "b"]) self.failIf(self.id3._EasyID3__id3.getall("RVA2")) def test_gain_bad_key(self): self.failIf("replaygain_foo_gain" in self.id3) self.failIf(self.id3._EasyID3__id3.getall("RVA2")) def test_gain_bad_value(self): self.failUnlessRaises( ValueError, self.id3.__setitem__, "replaygain_foo_gain", []) self.failUnlessRaises( ValueError, self.id3.__setitem__, "replaygain_foo_gain", ["foo"]) self.failUnlessRaises( ValueError, self.id3.__setitem__, "replaygain_foo_gain", ["1", "2"]) self.failIf(self.id3._EasyID3__id3.getall("RVA2")) def test_peak_bad_key(self): self.failIf("replaygain_foo_peak" in self.id3) self.failIf(self.id3._EasyID3__id3.getall("RVA2")) def test_peak_bad_value(self): self.failUnlessRaises( ValueError, self.id3.__setitem__, "replaygain_foo_peak", []) self.failUnlessRaises( ValueError, self.id3.__setitem__, "replaygain_foo_peak", ["foo"]) self.failUnlessRaises( ValueError, self.id3.__setitem__, "replaygain_foo_peak", ["1", "1"]) self.failUnlessRaises( ValueError, self.id3.__setitem__, "replaygain_foo_peak", ["3"]) self.failIf(self.id3._EasyID3__id3.getall("RVA2")) def test_gain_peak_get(self): self.id3["replaygain_foo_gain"] = "+3.5 dB" self.id3["replaygain_bar_peak"] = "0.5" self.failUnlessEqual( self.id3["replaygain_foo_gain"], ["+3.500000 dB"]) self.failUnlessEqual(self.id3["replaygain_foo_peak"], ["0.000000"]) self.failUnlessEqual( self.id3["replaygain_bar_gain"], ["+0.000000 dB"]) self.failUnlessEqual(self.id3["replaygain_bar_peak"], ["0.500000"]) def test_gain_peak_set(self): self.id3["replaygain_foo_gain"] = "+3.5 dB" self.id3["replaygain_bar_peak"] = "0.5" self.id3.save(self.filename) id3 = EasyID3(self.filename) self.failUnlessEqual(id3["replaygain_foo_gain"], ["+3.500000 dB"]) self.failUnlessEqual(id3["replaygain_foo_peak"], ["0.000000"]) self.failUnlessEqual(id3["replaygain_bar_gain"], ["+0.000000 dB"]) self.failUnlessEqual(id3["replaygain_bar_peak"], ["0.500000"]) def test_gain_peak_delete(self): self.id3["replaygain_foo_gain"] = "+3.5 dB" self.id3["replaygain_bar_peak"] = "0.5" del(self.id3["replaygain_bar_gain"]) del(self.id3["replaygain_foo_peak"]) self.failUnless("replaygain_foo_gain" in self.id3.keys()) self.failUnless("replaygain_bar_gain" in self.id3.keys()) del(self.id3["replaygain_foo_gain"]) del(self.id3["replaygain_bar_peak"]) self.failIf("replaygain_foo_gain" in self.id3.keys()) self.failIf("replaygain_bar_gain" in self.id3.keys()) del(self.id3["replaygain_foo_gain"]) del(self.id3["replaygain_bar_peak"]) self.failIf("replaygain_foo_gain" in self.id3.keys()) self.failIf("replaygain_bar_gain" in self.id3.keys()) def test_pickle(self): # http://code.google.com/p/mutagen/issues/detail?id=102 pickle.dumps(self.id3) def test_get_fallback(self): called = [] def get_func(id3, key): id3.getall("") self.failUnlessEqual(key, "nope") called.append(1) self.id3.GetFallback = get_func self.id3["nope"] self.failUnless(called) def test_set_fallback(self): called = [] def set_func(id3, key, value): id3.getall("") self.failUnlessEqual(key, "nope") self.failUnlessEqual(value, ["foo"]) called.append(1) self.id3.SetFallback = set_func self.id3["nope"] = "foo" self.failUnless(called) def test_del_fallback(self): called = [] def del_func(id3, key): id3.getall("") self.failUnlessEqual(key, "nope") called.append(1) self.id3.DeleteFallback = del_func del self.id3["nope"] self.failUnless(called) def test_list_fallback(self): def list_func(id3, key): id3.getall("") self.failIf(key) return ["somekey"] self.id3.ListFallback = list_func self.failUnlessEqual(self.id3.keys(), ["somekey"]) def tearDown(self): os.unlink(self.filename) add(TEasyID3) mutagen-1.22/tests/__init__.py0000644000175000017500000001142212212322524016572 0ustar lazkalazka00000000000000from __future__ import division import re import glob import os import sys import unittest from unittest import TestCase as BaseTestCase suites = [] add = suites.append class TestCase(BaseTestCase): def failUnlessRaisesRegexp(self, exc, re_, fun, *args, **kwargs): def wrapped(*args, **kwargs): try: fun(*args, **kwargs) except Exception, e: self.failUnless(re.search(re_, str(e))) raise self.failUnlessRaises(exc, wrapped, *args, **kwargs) # silence deprec warnings about useless renames failUnless = BaseTestCase.assertTrue failIf = BaseTestCase.assertFalse failUnlessEqual = BaseTestCase.assertEqual failUnlessRaises = BaseTestCase.assertRaises failUnlessAlmostEqual = BaseTestCase.assertAlmostEqual failIfEqual = BaseTestCase.assertNotEqual failIfAlmostEqual = BaseTestCase.assertNotAlmostEqual def assertReallyEqual(self, a, b): self.assertEqual(a, b) self.assertEqual(b, a) self.assertTrue(a == b) self.assertTrue(b == a) self.assertFalse(a != b) self.assertFalse(b != a) self.assertEqual(0, cmp(a, b)) self.assertEqual(0, cmp(b, a)) def assertReallyNotEqual(self, a, b): self.assertNotEqual(a, b) self.assertNotEqual(b, a) self.assertFalse(a == b) self.assertFalse(b == a) self.assertTrue(a != b) self.assertTrue(b != a) self.assertNotEqual(0, cmp(a, b)) self.assertNotEqual(0, cmp(b, a)) for name in glob.glob(os.path.join(os.path.dirname(__file__), "test_*.py")): module = "tests." + os.path.basename(name) __import__(module[:-3], {}, {}, []) class Result(unittest.TestResult): separator1 = '=' * 70 separator2 = '-' * 70 def addSuccess(self, test): unittest.TestResult.addSuccess(self, test) sys.stdout.write('.') def addError(self, test, err): unittest.TestResult.addError(self, test, err) sys.stdout.write('E') def addFailure(self, test, err): unittest.TestResult.addFailure(self, test, err) sys.stdout.write('F') def printErrors(self): succ = self.testsRun - (len(self.errors) + len(self.failures)) v = "%3d" % succ count = 50 - self.testsRun sys.stdout.write((" " * count) + v + "\n") self.printErrorList('ERROR', self.errors) self.printErrorList('FAIL', self.failures) def printErrorList(self, flavour, errors): for test, err in errors: sys.stdout.write(self.separator1 + "\n") sys.stdout.write("%s: %s\n" % (flavour, str(test))) sys.stdout.write(self.separator2 + "\n") sys.stdout.write("%s\n" % err) class Runner(object): def run(self, test): suite = unittest.makeSuite(test) pref = '%s (%d): ' % (test.__name__, len(suite._tests)) print pref + " " * (25 - len(pref)), result = Result() suite(result) result.printErrors() return bool(result.failures + result.errors) def unit(run=[], quick=False): import mmap runner = Runner() failures = False tests = [t for t in suites if not run or t.__name__ in run] # normal run, trace mmap calls orig_mmap = mmap.mmap uses_mmap = [] print "Running tests with real mmap." for test in tests: def new_mmap(*args, **kwargs): if test not in uses_mmap: uses_mmap.append(test) return orig_mmap(*args, **kwargs) mmap.mmap = new_mmap failures |= runner.run(test) mmap.mmap = orig_mmap # make sure the above works if not run: assert len(uses_mmap) > 1 if quick: return failures # run mmap using tests with mocked lockf try: import fcntl except ImportError: print "Unable to run mocked fcntl.lockf tests." else: def MockLockF(*args, **kwargs): raise IOError lockf = fcntl.lockf fcntl.lockf = MockLockF print "Running tests with mocked failing fcntl.lockf." for test in uses_mmap: failures |= runner.run(test) fcntl.lockf = lockf # failing mmap.move class MockMMap(object): def __init__(self, *args, **kwargs): pass def move(self, dest, src, count): raise ValueError def close(self): pass print "Running tests with mocked failing mmap.move." mmap.mmap = MockMMap for test in uses_mmap: failures |= runner.run(test) # failing mmap.mmap def MockMMap2(*args, **kwargs): raise EnvironmentError mmap.mmap = MockMMap2 print "Running tests with mocked failing mmap.mmap." for test in uses_mmap: failures |= runner.run(test) return failures mutagen-1.22/tests/test_oggspeex.py0000644000175000017500000000402612177421270017725 0ustar lazkalazka00000000000000import os import shutil from cStringIO import StringIO from mutagen.ogg import OggPage from mutagen.oggspeex import OggSpeex, OggSpeexInfo, delete from tests import add from tests.test_ogg import TOggFileType from tempfile import mkstemp class TOggSpeex(TOggFileType): Kind = OggSpeex def setUp(self): original = os.path.join("tests", "data", "empty.spx") fd, self.filename = mkstemp(suffix='.ogg') os.close(fd) shutil.copy(original, self.filename) self.audio = self.Kind(self.filename) def test_module_delete(self): delete(self.filename) self.scan_file() self.failIf(OggSpeex(self.filename).tags) def test_channels(self): self.failUnlessEqual(2, self.audio.info.channels) def test_sample_rate(self): self.failUnlessEqual(44100, self.audio.info.sample_rate) def test_bitrate(self): self.failUnlessEqual(0, self.audio.info.bitrate) def test_invalid_not_first(self): page = OggPage(open(self.filename, "rb")) page.first = False self.failUnlessRaises(IOError, OggSpeexInfo, StringIO(page.write())) def test_vendor(self): self.failUnless( self.audio.tags.vendor.startswith("Encoded with Speex 1.1.12")) self.failUnlessRaises(KeyError, self.audio.tags.__getitem__, "vendor") def test_not_my_ogg(self): fn = os.path.join('tests', 'data', 'empty.oggflac') self.failUnlessRaises(IOError, type(self.audio), fn) self.failUnlessRaises(IOError, self.audio.save, fn) self.failUnlessRaises(IOError, self.audio.delete, fn) def test_multiplexed_in_headers(self): shutil.copy( os.path.join("tests", "data", "multiplexed.spx"), self.filename) audio = self.Kind(self.filename) audio.tags["foo"] = ["bar"] audio.save() audio = self.Kind(self.filename) self.failUnlessEqual(audio.tags["foo"], ["bar"]) def test_mime(self): self.failUnless("audio/x-speex" in self.audio.mime) add(TOggSpeex) mutagen-1.22/tests/test_tools_mid3v2.py0000644000175000017500000001440312212144666020431 0ustar lazkalazka00000000000000import os from tempfile import mkstemp import shutil import mutagen from mutagen.id3 import ID3 from tests import add from tests.test_tools import _TTools class TMid3v2(_TTools): TOOL_NAME = "mid3v2" def setUp(self): super(TMid3v2, self).setUp() original = os.path.join('tests', 'data', 'silence-44-s.mp3') fd, self.filename = mkstemp(suffix='.mp3') os.close(fd) shutil.copy(original, self.filename) def tearDown(self): super(TMid3v2, self).tearDown() os.unlink(self.filename) def test_list_genres(self): for arg in ["-L", "--list-genres"]: res, out = self.call(arg) self.failUnlessEqual(res, 0) self.failUnless("Acid Punk" in out) def test_list_frames(self): for arg in ["-f", "--list-frames"]: res, out = self.call(arg) self.failUnlessEqual(res, 0) self.failUnless("--APIC" in out) self.failUnless("--TIT2" in out) def test_list(self): f = ID3(self.filename) album = f["TALB"].text[0] for arg in ["-l", "--list"]: res, out = self.call(arg, self.filename) self.failUnlessEqual(res, 0) self.failUnless("TALB=" + album in out) def test_list_raw(self): f = ID3(self.filename) res, out = self.call("--list-raw", self.filename) self.failUnlessEqual(res, 0) self.failUnless(repr(f["TALB"]) in out) def _test_text_frame(self, short, longer, frame): new_value = "TEST" for arg in [short, longer]: orig = ID3(self.filename) frame_class = mutagen.id3.Frames[frame] orig[frame] = frame_class(text=[u"BLAH"], encoding=3) orig.save() res, out = self.call(arg, new_value, self.filename) self.failUnlessEqual(res, 0) self.failIf(out) self.failUnlessEqual(ID3(self.filename)[frame].text, [new_value]) def test_artist(self): self._test_text_frame("-a", "--artist", "TPE1") def test_album(self): self._test_text_frame("-A", "--album", "TALB") def test_title(self): self._test_text_frame("-t", "--song", "TIT2") def test_genre(self): self._test_text_frame("-g", "--genre", "TCON") def test_convert(self): res, out = self.call("--convert", self.filename) self.failUnlessEqual((res, out), (0, "")) def test_split_escape(self): split_escape = self.get_var("split_escape") inout = [ (("", ":"), [""]), ((":", ":"), ["", ""]), ((":", ":", 0), [":"]), ((":b:c:", ":", 0), [":b:c:"]), ((":b:c:", ":", 1), ["", "b:c:"]), ((":b:c:", ":", 2), ["", "b", "c:"]), ((":b:c:", ":", 3), ["", "b", "c", ""]), (("a\\:b:c", ":"), ["a:b", "c"]), (("a\\\\:b:c", ":"), ["a\\", "b", "c"]), (("a\\\\\\:b:c\\:", ":"), ["a\\:b", "c:"]), (("\\", ":"), [""]), (("\\\\", ":"), ["\\"]), (("\\\\a\\b", ":"), ["\\a\\b"]), ] for inargs, out in inout: self.assertEqual(split_escape(*inargs), out) def test_unescape(self): unescape_string = self.get_var("unescape_string") self.assertEqual(unescape_string("\\n"), "\n") def test_artist_escape(self): res, out = self.call("-e", "-a", "foo\\nbar", self.filename) self.failUnlessEqual(res, 0) self.failIf(out) f = ID3(self.filename) self.failUnlessEqual(f["TPE1"][0], "foo\nbar") def test_txxx_escape(self): res, out = self.call( "-e", "--TXXX", "EscapeTest\\:\\:albumartist:Ex\\:ample", self.filename) self.failUnlessEqual(res, 0) self.failIf(out) f = ID3(self.filename) frame = f.getall("TXXX")[0] self.failUnlessEqual(frame.desc, "EscapeTest::albumartist") self.failUnlessEqual(frame.text, ["Ex:ample"]) def test_txxx(self): res, out = self.call("--TXXX", "A\\:B:C", self.filename) self.failUnlessEqual((res, out), (0, "")) f = ID3(self.filename) frame = f.getall("TXXX")[0] self.failUnlessEqual(frame.desc, "A\\") self.failUnlessEqual(frame.text, ["B:C"]) def test_comm1(self): res, out = self.call("--COMM", "A", self.filename) self.failUnlessEqual((res, out), (0, "")) f = ID3(self.filename) frame = f.getall("COMM:")[0] self.failUnlessEqual(frame.desc, "") self.failUnlessEqual(frame.text, ["A"]) def test_comm2(self): res, out = self.call("--COMM", "Y:B", self.filename) self.failUnlessEqual((res, out), (0, "")) f = ID3(self.filename) frame = f.getall("COMM:Y")[0] self.failUnlessEqual(frame.desc, "Y") self.failUnlessEqual(frame.text, ["B"]) def test_comm2_escape(self): res, out = self.call("-e", "--COMM", "Y\\:B\\nG", self.filename) self.failUnlessEqual((res, out), (0, "")) f = ID3(self.filename) frame = f.getall("COMM:")[0] self.failUnlessEqual(frame.desc, "") self.failUnlessEqual(frame.text, ["Y:B\nG"]) def test_comm3(self): res, out = self.call("--COMM", "Z:B:C:D:ger", self.filename) self.failUnlessEqual((res, out), (0, "")) f = ID3(self.filename) frame = f.getall("COMM:Z")[0] self.failUnlessEqual(frame.desc, "Z") self.failUnlessEqual(frame.text, ["B:C:D"]) self.failUnlessEqual(frame.lang, "ger") def test_encoding_with_escape(self): import locale text = u'\xe4\xf6\xfc' enc = locale.getpreferredencoding() res, out = self.call("-e", "-a", text.encode(enc), self.filename) self.failUnlessEqual((res, out), (0, "")) def test_invalid_encoding(self): res, out = self.call("--TALB", '\\xff', '-e', self.filename) self.failIfEqual(res, 0) self.failUnless("TALB" in out) def test_invalid_escape(self): res, out = self.call("--TALB", '\\xaz', '-e', self.filename) self.failIfEqual(res, 0) self.failUnless("TALB" in out) res, out = self.call("--TALB", '\\', '-e', self.filename) self.failIfEqual(res, 0) self.failUnless("TALB" in out) add(TMid3v2) mutagen-1.22/tests/test_tools_mutagen_inspect.py0000644000175000017500000000113512211140204022467 0ustar lazkalazka00000000000000import os import glob from tests import add from tests.test_tools import _TTools class TMutagenInspect(_TTools): TOOL_NAME = "mutagen-inspect" def test_basic(self): base = os.path.join('tests', 'data') self.paths = glob.glob(os.path.join(base, "empty*")) self.paths += glob.glob(os.path.join(base, "silence-*")) for path in self.paths: res, out = self.call(path) self.failIf(res) self.failUnless(out.strip()) self.failIf("Unknown file type" in out) self.failIf("Errno" in out) add(TMutagenInspect) mutagen-1.22/tests/test_musepack.py0000644000175000017500000001016012211074431017700 0ustar lazkalazka00000000000000import os import shutil from tempfile import mkstemp from mutagen.id3 import ID3, TIT2 from mutagen.musepack import Musepack, MusepackInfo, MusepackHeaderError from cStringIO import StringIO from tests import TestCase, add class TMusepack(TestCase): def setUp(self): self.sv8 = Musepack(os.path.join("tests", "data", "sv8_header.mpc")) self.sv7 = Musepack(os.path.join("tests", "data", "click.mpc")) self.sv5 = Musepack(os.path.join("tests", "data", "sv5_header.mpc")) self.sv4 = Musepack(os.path.join("tests", "data", "sv4_header.mpc")) def test_bad_header(self): self.failUnlessRaises( MusepackHeaderError, Musepack, os.path.join("tests", "data", "almostempty.mpc")) def test_channels(self): self.failUnlessEqual(self.sv8.info.channels, 2) self.failUnlessEqual(self.sv7.info.channels, 2) self.failUnlessEqual(self.sv5.info.channels, 2) self.failUnlessEqual(self.sv4.info.channels, 2) def test_sample_rate(self): self.failUnlessEqual(self.sv8.info.sample_rate, 44100) self.failUnlessEqual(self.sv7.info.sample_rate, 44100) self.failUnlessEqual(self.sv5.info.sample_rate, 44100) self.failUnlessEqual(self.sv4.info.sample_rate, 44100) def test_bitrate(self): self.failUnlessEqual(self.sv8.info.bitrate, 609) self.failUnlessEqual(self.sv7.info.bitrate, 194530) self.failUnlessEqual(self.sv5.info.bitrate, 39) self.failUnlessEqual(self.sv4.info.bitrate, 39) def test_length(self): self.failUnlessAlmostEqual(self.sv8.info.length, 1.49, 1) self.failUnlessAlmostEqual(self.sv7.info.length, 0.07, 2) self.failUnlessAlmostEqual(self.sv5.info.length, 26.3, 1) self.failUnlessAlmostEqual(self.sv4.info.length, 26.3, 1) def test_gain(self): self.failUnlessAlmostEqual(self.sv8.info.title_gain, -4.668, 3) self.failUnlessAlmostEqual(self.sv8.info.title_peak, 0.5288, 3) self.failUnlessEqual(self.sv8.info.title_gain, self.sv8.info.album_gain) self.failUnlessEqual(self.sv8.info.title_peak, self.sv8.info.album_peak) self.failUnlessAlmostEqual(self.sv7.info.title_gain, 9.27, 6) self.failUnlessAlmostEqual(self.sv7.info.title_peak, 0.1149, 4) self.failUnlessEqual(self.sv7.info.title_gain, self.sv7.info.album_gain) self.failUnlessEqual(self.sv7.info.title_peak, self.sv7.info.album_peak) self.failUnlessRaises(AttributeError, getattr, self.sv5, 'title_gain') def test_not_my_file(self): self.failUnlessRaises( MusepackHeaderError, Musepack, os.path.join("tests", "data", "empty.ogg")) self.failUnlessRaises( MusepackHeaderError, Musepack, os.path.join("tests", "data", "emptyfile.mp3")) def test_almost_my_file(self): self.failUnlessRaises( MusepackHeaderError, MusepackInfo, StringIO("MP+" + "\x00" * 32)) self.failUnlessRaises( MusepackHeaderError, MusepackInfo, StringIO("MP+" + "\x00" * 100)) self.failUnlessRaises( MusepackHeaderError, MusepackInfo, StringIO("MPCK" + "\x00" * 100)) def test_pprint(self): self.sv8.pprint() self.sv7.pprint() self.sv5.pprint() self.sv4.pprint() def test_mime(self): self.failUnless("audio/x-musepack" in self.sv7.mime) add(TMusepack) class TMusepackWithID3(TestCase): SAMPLE = os.path.join("tests", "data", "click.mpc") def setUp(self): fd, self.NEW = mkstemp(suffix='mpc') os.close(fd) shutil.copy(self.SAMPLE, self.NEW) self.failUnlessEqual(open(self.SAMPLE).read(), open(self.NEW).read()) def tearDown(self): os.unlink(self.NEW) def test_ignore_id3(self): id3 = ID3() id3.add(TIT2(encoding=0, text='id3 title')) id3.save(self.NEW) f = Musepack(self.NEW) f['title'] = 'apev2 title' f.save() id3 = ID3(self.NEW) self.failUnlessEqual(id3['TIT2'], 'id3 title') f = Musepack(self.NEW) self.failUnlessEqual(f['title'], 'apev2 title') add(TMusepackWithID3) mutagen-1.22/tests/test__id3specs.py0000644000175000017500000001762412211651443017764 0ustar lazkalazka00000000000000import sys from tests import TestCase, add from mutagen.id3 import BitPaddedInt class SpecSanityChecks(TestCase): def test_bytespec(self): from mutagen.id3 import ByteSpec s = ByteSpec('name') self.assertEquals((97, 'bcdefg'), s.read(None, 'abcdefg')) self.assertEquals('a', s.write(None, 97)) self.assertRaises(TypeError, s.write, None, 'abc') self.assertRaises(TypeError, s.write, None, None) def test_encodingspec(self): from mutagen.id3 import EncodingSpec s = EncodingSpec('name') self.assertEquals((0, 'abcdefg'), s.read(None, 'abcdefg')) self.assertEquals((3, 'abcdefg'), s.read(None, '\x03abcdefg')) self.assertEquals('\x00', s.write(None, 0)) self.assertRaises(TypeError, s.write, None, 'abc') self.assertRaises(TypeError, s.write, None, None) def test_stringspec(self): from mutagen.id3 import StringSpec s = StringSpec('name', 3) self.assertEquals(('abc', 'defg'), s.read(None, 'abcdefg')) self.assertEquals('abc', s.write(None, 'abcdefg')) self.assertEquals('\x00\x00\x00', s.write(None, None)) self.assertEquals('\x00\x00\x00', s.write(None, '\x00')) self.assertEquals('a\x00\x00', s.write(None, 'a')) def test_binarydataspec(self): from mutagen.id3 import BinaryDataSpec s = BinaryDataSpec('name') self.assertEquals(('abcdefg', ''), s.read(None, 'abcdefg')) self.assertEquals('None', s.write(None, None)) self.assertEquals('43', s.write(None, 43)) def test_encodedtextspec(self): from mutagen.id3 import EncodedTextSpec, Frame s = EncodedTextSpec('name') f = Frame(); f.encoding = 0 self.assertEquals(('abcd', 'fg'), s.read(f, 'abcd\x00fg')) self.assertEquals('abcdefg\x00', s.write(f, 'abcdefg')) self.assertRaises(AttributeError, s.write, f, None) def test_timestampspec(self): from mutagen.id3 import TimeStampSpec, Frame, ID3TimeStamp s = TimeStampSpec('name') f = Frame(); f.encoding = 0 self.assertEquals((ID3TimeStamp('ab'), 'fg'), s.read(f, 'ab\x00fg')) self.assertEquals((ID3TimeStamp('1234'), ''), s.read(f, '1234\x00')) self.assertEquals('1234\x00', s.write(f, ID3TimeStamp('1234'))) self.assertRaises(AttributeError, s.write, f, None) def test_volumeadjustmentspec(self): from mutagen.id3 import VolumeAdjustmentSpec s = VolumeAdjustmentSpec('gain') self.assertEquals((0.0, ''), s.read(None, '\x00\x00')) self.assertEquals((2.0, ''), s.read(None, '\x04\x00')) self.assertEquals((-2.0, ''), s.read(None, '\xfc\x00')) self.assertEquals('\x00\x00', s.write(None, 0.0)) self.assertEquals('\x04\x00', s.write(None, 2.0)) self.assertEquals('\xfc\x00', s.write(None, -2.0)) add(SpecSanityChecks) class SpecValidateChecks(TestCase): def test_volumeadjustmentspec(self): from mutagen.id3 import VolumeAdjustmentSpec s = VolumeAdjustmentSpec('gain') self.assertRaises(ValueError, s.validate, None, 65) def test_volumepeakspec(self): from mutagen.id3 import VolumePeakSpec s = VolumePeakSpec('peak') self.assertRaises(ValueError, s.validate, None, 2) def test_bytespec(self): from mutagen.id3 import ByteSpec s = ByteSpec('byte') self.assertRaises(ValueError, s.validate, None, 1000) add(SpecValidateChecks) class NoHashSpec(TestCase): def test_spec(self): from mutagen.id3 import Spec self.failUnlessRaises(TypeError, {}.__setitem__, Spec("foo"), None) add(NoHashSpec) class BitPaddedIntTest(TestCase): def test_zero(self): self.assertEquals(BitPaddedInt('\x00\x00\x00\x00'), 0) def test_1(self): self.assertEquals(BitPaddedInt('\x00\x00\x00\x01'), 1) def test_1l(self): self.assertEquals(BitPaddedInt('\x01\x00\x00\x00', bigendian=False), 1) def test_129(self): self.assertEquals(BitPaddedInt('\x00\x00\x01\x01'), 0x81) def test_129b(self): self.assertEquals(BitPaddedInt('\x00\x00\x01\x81'), 0x81) def test_65(self): self.assertEquals(BitPaddedInt('\x00\x00\x01\x81', 6), 0x41) def test_32b(self): self.assertEquals(BitPaddedInt('\xFF\xFF\xFF\xFF', bits=8), 0xFFFFFFFF) def test_32bi(self): self.assertEquals(BitPaddedInt(0xFFFFFFFF, bits=8), 0xFFFFFFFF) def test_s32b(self): self.assertEquals(BitPaddedInt('\xFF\xFF\xFF\xFF', bits=8).as_str(), '\xFF\xFF\xFF\xFF') def test_s0(self): self.assertEquals(BitPaddedInt.to_str(0), '\x00\x00\x00\x00') def test_s1(self): self.assertEquals(BitPaddedInt.to_str(1), '\x00\x00\x00\x01') def test_s1l(self): self.assertEquals( BitPaddedInt.to_str(1, bigendian=False), '\x01\x00\x00\x00') def test_s129(self): self.assertEquals(BitPaddedInt.to_str(129), '\x00\x00\x01\x01') def test_s65(self): self.assertEquals(BitPaddedInt.to_str(0x41, 6), '\x00\x00\x01\x01') def test_w129(self): self.assertEquals(BitPaddedInt.to_str(129, width=2), '\x01\x01') def test_w129l(self): self.assertEquals( BitPaddedInt.to_str(129, width=2, bigendian=False), '\x01\x01') def test_wsmall(self): self.assertRaises(ValueError, BitPaddedInt.to_str, 129, width=1) def test_str_int_init(self): from struct import pack self.assertEquals(BitPaddedInt(238).as_str(), BitPaddedInt(pack('>L', 238)).as_str()) def test_varwidth(self): self.assertEquals(len(BitPaddedInt.to_str(100)), 4) self.assertEquals(len(BitPaddedInt.to_str(100, width=-1)), 4) self.assertEquals(len(BitPaddedInt.to_str(2**32, width=-1)), 5) def test_minwidth(self): self.assertEquals( len(BitPaddedInt.to_str(100, width=-1, minwidth=6)), 6) def test_inval_input(self): self.assertRaises(TypeError, BitPaddedInt, None) def test_promote_long(self): l = BitPaddedInt(sys.maxint ** 2) self.assertTrue(isinstance(l, long)) self.assertEqual(BitPaddedInt(l.as_str(width=-1)), l) def test_has_valid_padding(self): self.failUnless(BitPaddedInt.has_valid_padding("\xff\xff", bits=8)) self.failIf(BitPaddedInt.has_valid_padding("\xff")) self.failIf(BitPaddedInt.has_valid_padding("\x00\xff")) self.failUnless(BitPaddedInt.has_valid_padding("\x7f\x7f")) self.failIf(BitPaddedInt.has_valid_padding("\x7f", bits=6)) self.failIf(BitPaddedInt.has_valid_padding("\x9f", bits=6)) self.failUnless(BitPaddedInt.has_valid_padding("\x3f", bits=6)) self.failUnless(BitPaddedInt.has_valid_padding(0xff, bits=8)) self.failIf(BitPaddedInt.has_valid_padding(0xff)) self.failIf(BitPaddedInt.has_valid_padding(0xff << 8)) self.failUnless(BitPaddedInt.has_valid_padding(0x7f << 8)) self.failIf(BitPaddedInt.has_valid_padding(0x9f << 32, bits=6)) self.failUnless(BitPaddedInt.has_valid_padding(0x3f << 16, bits=6)) add(BitPaddedIntTest) class TestUnsynch(TestCase): def test_unsync_encode(self): from mutagen.id3 import unsynch as un for d in ('\xff\xff\xff\xff', '\xff\xf0\x0f\x00', '\xff\x00\x0f\xf0'): self.assertEquals(d, un.decode(un.encode(d))) self.assertNotEqual(d, un.encode(d)) self.assertEquals('\xff\x44', un.encode('\xff\x44')) self.assertEquals('\xff\x00\x00', un.encode('\xff\x00')) def test_unsync_decode(self): from mutagen.id3 import unsynch as un self.assertRaises(ValueError, un.decode, '\xff\xff\xff\xff') self.assertRaises(ValueError, un.decode, '\xff\xf0\x0f\x00') self.assertRaises(ValueError, un.decode, '\xff\xe0') self.assertRaises(ValueError, un.decode, '\xff') self.assertEquals('\xff\x44', un.decode('\xff\x44')) add(TestUnsynch) mutagen-1.22/tests/test_tools.py0000644000175000017500000000163712212316116017241 0ustar lazkalazka00000000000000import os import sys import StringIO from tests import TestCase def get_var(tool_name, entry="main"): tool_path = os.path.join("tools", tool_name) env = {} execfile(tool_path, env) return env[entry] class _TTools(TestCase): TOOL_NAME = None def setUp(self): self._main = get_var(self.TOOL_NAME) def get_var(self, name): return get_var(self.TOOL_NAME, name) def call(self, *args): for arg in args: assert isinstance(arg, str) old_stdout = sys.stdout try: out = StringIO.StringIO() sys.stdout = out try: ret = self._main([self.TOOL_NAME] + list(args)) except SystemExit, e: ret = e.code ret = ret or 0 return (ret, out.getvalue()) finally: sys.stdout = old_stdout def tearDown(self): del self._main mutagen-1.22/tests/test__id3frames.py0000644000175000017500000003015412212326243020113 0ustar lazkalazka00000000000000from tests import TestCase, add from mutagen.id3 import Frames, Frames_2_2, ID3 _22 = ID3(); _22.version = (2,2,0) _23 = ID3(); _23.version = (2,3,0) _24 = ID3(); _24.version = (2,4,0) class FrameSanityChecks(TestCase): def test_TF(self): from mutagen.id3 import TextFrame self.assert_(isinstance(TextFrame(text='text'), TextFrame)) def test_UF(self): from mutagen.id3 import UrlFrame self.assert_(isinstance(UrlFrame('url'), UrlFrame)) def test_WXXX(self): from mutagen.id3 import WXXX self.assert_(isinstance(WXXX(url='durl'), WXXX)) def test_NTF(self): from mutagen.id3 import NumericTextFrame self.assert_(isinstance(NumericTextFrame(text='1'), NumericTextFrame)) def test_NTPF(self): from mutagen.id3 import NumericPartTextFrame self.assert_( isinstance(NumericPartTextFrame(text='1/2'), NumericPartTextFrame)) def test_MTF(self): from mutagen.id3 import TextFrame self.assert_(isinstance(TextFrame(text=['a','b']), TextFrame)) def test_TXXX(self): from mutagen.id3 import TXXX self.assert_(isinstance(TXXX(desc='d',text='text'), TXXX)) def test_22_uses_direct_ints(self): data = 'TT1\x00\x00\x83\x00' + ('123456789abcdef' * 16) tag = list(_22._ID3__read_frames(data, Frames_2_2))[0] self.assertEquals(data[7:7+0x82].decode('latin1'), tag.text[0]) def test_frame_too_small(self): self.assertEquals([], list(_24._ID3__read_frames('012345678', Frames))) self.assertEquals([], list(_23._ID3__read_frames('012345678', Frames))) self.assertEquals([], list(_22._ID3__read_frames('01234', Frames_2_2))) self.assertEquals( [], list(_22._ID3__read_frames('TT1'+'\x00'*3, Frames_2_2))) def test_unknown_22_frame(self): data = 'XYZ\x00\x00\x01\x00' self.assertEquals([data], list(_22._ID3__read_frames(data, {}))) def test_zlib_latin1(self): from mutagen.id3 import TPE1 tag = TPE1.fromData(_24, 0x9, '\x00\x00\x00\x0f' 'x\x9cc(\xc9\xc8,V\x00\xa2D\xfd\x92\xd4\xe2\x12\x00&\x7f\x05%') self.assertEquals(tag.encoding, 0) self.assertEquals(tag, ['this is a/test']) def test_datalen_but_not_compressed(self): from mutagen.id3 import TPE1 tag = TPE1.fromData(_24, 0x01, '\x00\x00\x00\x06\x00A test') self.assertEquals(tag.encoding, 0) self.assertEquals(tag, ['A test']) def test_utf8(self): from mutagen.id3 import TPE1 tag = TPE1.fromData(_23, 0x00, '\x03this is a test') self.assertEquals(tag.encoding, 3) self.assertEquals(tag, 'this is a test') def test_zlib_utf16(self): from mutagen.id3 import TPE1 data = ('\x00\x00\x00\x1fx\x9cc\xfc\xff\xaf\x84!\x83!\x93\xa1\x98A' '\x01J&2\xe83\x940\xa4\x02\xd9%\x0c\x00\x87\xc6\x07#') tag = TPE1.fromData(_23, 0x80, data) self.assertEquals(tag.encoding, 1) self.assertEquals(tag, ['this is a/test']) tag = TPE1.fromData(_24, 0x08, data) self.assertEquals(tag.encoding, 1) self.assertEquals(tag, ['this is a/test']) def test_load_write(self): from mutagen.id3 import TPE1, Frames artists= [s.decode('utf8') for s in ['\xc2\xb5', '\xe6\x97\xa5\xe6\x9c\xac']] artist = TPE1(encoding=3, text=artists) id3 = ID3() tag = list(id3._ID3__read_frames( id3._ID3__save_frame(artist), Frames))[0] self.assertEquals('TPE1', type(tag).__name__) self.assertEquals(artist.text, tag.text) def test_22_to_24(self): from mutagen.id3 import TT1 id3 = ID3() tt1 = TT1(encoding=0, text=u'whatcha staring at?') id3.loaded_frame(tt1) tit1 = id3['TIT1'] self.assertEquals(tt1.encoding, tit1.encoding) self.assertEquals(tt1.text, tit1.text) self.assert_('TT1' not in id3) def test_single_TXYZ(self): from mutagen.id3 import TIT2 self.assertEquals(TIT2(text="a").HashKey, TIT2(text="b").HashKey) def test_multi_TXXX(self): from mutagen.id3 import TXXX self.assertEquals(TXXX(text="a").HashKey, TXXX(text="b").HashKey) self.assertNotEquals(TXXX(desc="a").HashKey, TXXX(desc="b").HashKey) def test_multi_WXXX(self): from mutagen.id3 import WXXX self.assertEquals(WXXX(text="a").HashKey, WXXX(text="b").HashKey) self.assertNotEquals(WXXX(desc="a").HashKey, WXXX(desc="b").HashKey) def test_multi_COMM(self): from mutagen.id3 import COMM self.assertEquals(COMM(text="a").HashKey, COMM(text="b").HashKey) self.assertNotEquals(COMM(desc="a").HashKey, COMM(desc="b").HashKey) self.assertNotEquals( COMM(lang="abc").HashKey, COMM(lang="def").HashKey) def test_multi_RVA2(self): from mutagen.id3 import RVA2 self.assertEquals(RVA2(gain=1).HashKey, RVA2(gain=2).HashKey) self.assertNotEquals(RVA2(desc="a").HashKey, RVA2(desc="b").HashKey) def test_multi_APIC(self): from mutagen.id3 import APIC self.assertEquals(APIC(data="1").HashKey, APIC(data="2").HashKey) self.assertNotEquals(APIC(desc="a").HashKey, APIC(desc="b").HashKey) def test_multi_POPM(self): from mutagen.id3 import POPM self.assertEquals(POPM(count=1).HashKey, POPM(count=2).HashKey) self.assertNotEquals(POPM(email="a").HashKey, POPM(email="b").HashKey) def test_multi_GEOB(self): from mutagen.id3 import GEOB self.assertEquals(GEOB(data="1").HashKey, GEOB(data="2").HashKey) self.assertNotEquals(GEOB(desc="a").HashKey, GEOB(desc="b").HashKey) def test_multi_UFID(self): from mutagen.id3 import UFID self.assertEquals(UFID(data="1").HashKey, UFID(data="2").HashKey) self.assertNotEquals(UFID(owner="a").HashKey, UFID(owner="b").HashKey) def test_multi_USER(self): from mutagen.id3 import USER self.assertEquals(USER(text="a").HashKey, USER(text="b").HashKey) self.assertNotEquals( USER(lang="abc").HashKey, USER(lang="def").HashKey) add(FrameSanityChecks) class Genres(TestCase): from mutagen.id3 import TCON TCON = TCON from mutagen._constants import GENRES GENRES = GENRES def _g(self, s): return self.TCON(text=s).genres def test_empty(self): self.assertEquals(self._g(""), []) def test_num(self): for i in range(len(self.GENRES)): self.assertEquals(self._g("%02d" % i), [self.GENRES[i]]) def test_parened_num(self): for i in range(len(self.GENRES)): self.assertEquals(self._g("(%02d)" % i), [self.GENRES[i]]) def test_unknown(self): self.assertEquals(self._g("(255)"), ["Unknown"]) self.assertEquals(self._g("199"), ["Unknown"]) self.assertNotEqual(self._g("256"), ["Unknown"]) def test_parened_multi(self): self.assertEquals(self._g("(00)(02)"), ["Blues", "Country"]) def test_coverremix(self): self.assertEquals(self._g("CR"), ["Cover"]) self.assertEquals(self._g("(CR)"), ["Cover"]) self.assertEquals(self._g("RX"), ["Remix"]) self.assertEquals(self._g("(RX)"), ["Remix"]) def test_parened_text(self): self.assertEquals( self._g("(00)(02)Real Folk Blues"), ["Blues", "Country", "Real Folk Blues"]) def test_escape(self): self.assertEquals(self._g("(0)((A genre)"), ["Blues", "(A genre)"]) self.assertEquals(self._g("(10)((20)"), ["New Age", "(20)"]) def test_nullsep(self): self.assertEquals(self._g("0\x00A genre"), ["Blues", "A genre"]) def test_nullsep_empty(self): self.assertEquals(self._g("\x000\x00A genre"), ["Blues", "A genre"]) def test_crazy(self): self.assertEquals( self._g("(20)(CR)\x0030\x00\x00Another\x00(51)Hooray"), ['Alternative', 'Cover', 'Fusion', 'Another', 'Techno-Industrial', 'Hooray']) def test_repeat(self): self.assertEquals(self._g("(20)Alternative"), ["Alternative"]) self.assertEquals( self._g("(20)\x00Alternative"), ["Alternative", "Alternative"]) def test_set_genre(self): gen = self.TCON(encoding=0, text="") self.assertEquals(gen.genres, []) gen.genres = ["a genre", "another"] self.assertEquals(gen.genres, ["a genre", "another"]) def test_set_string(self): gen = self.TCON(encoding=0, text="") gen.genres = "foo" self.assertEquals(gen.genres, ["foo"]) def test_nodoubledecode(self): gen = self.TCON(encoding=1, text=u"(255)genre") gen.genres = gen.genres self.assertEquals(gen.genres, [u"Unknown", u"genre"]) add(Genres) class TimeStamp(TestCase): from mutagen.id3 import ID3TimeStamp as Stamp Stamp = Stamp def test_Y(self): s = self.Stamp('1234') self.assertEquals(s.year, 1234) self.assertEquals(s.text, '1234') def test_yM(self): s = self.Stamp('1234-56') self.assertEquals(s.year, 1234) self.assertEquals(s.month, 56) self.assertEquals(s.text, '1234-56') def test_ymD(self): s = self.Stamp('1234-56-78') self.assertEquals(s.year, 1234) self.assertEquals(s.month, 56) self.assertEquals(s.day, 78) self.assertEquals(s.text, '1234-56-78') def test_ymdH(self): s = self.Stamp('1234-56-78T12') self.assertEquals(s.year, 1234) self.assertEquals(s.month, 56) self.assertEquals(s.day, 78) self.assertEquals(s.hour, 12) self.assertEquals(s.text, '1234-56-78 12') def test_ymdhM(self): s = self.Stamp('1234-56-78T12:34') self.assertEquals(s.year, 1234) self.assertEquals(s.month, 56) self.assertEquals(s.day, 78) self.assertEquals(s.hour, 12) self.assertEquals(s.minute, 34) self.assertEquals(s.text, '1234-56-78 12:34') def test_ymdhmS(self): s = self.Stamp('1234-56-78T12:34:56') self.assertEquals(s.year, 1234) self.assertEquals(s.month, 56) self.assertEquals(s.day, 78) self.assertEquals(s.hour, 12) self.assertEquals(s.minute, 34) self.assertEquals(s.second, 56) self.assertEquals(s.text, '1234-56-78 12:34:56') def test_Ymdhms(self): s = self.Stamp('1234-56-78T12:34:56') s.month = None self.assertEquals(s.text, '1234') def test_alternate_reprs(self): s = self.Stamp('1234-56.78 12:34:56') self.assertEquals(s.text, '1234-56-78 12:34:56') def test_order(self): s = self.Stamp('1234') t = self.Stamp('1233-12') u = self.Stamp('1234-01') self.assert_(t < s < u) self.assert_(u > s > t) add(TimeStamp) class NoHashFrame(TestCase): def test_frame(self): from mutagen.id3 import TIT1 self.failUnlessRaises( TypeError, {}.__setitem__, TIT1(encoding=0, text="foo"), None) add(NoHashFrame) class FrameIDValidate(TestCase): def test_valid(self): from mutagen.id3 import is_valid_frame_id self.failUnless(is_valid_frame_id("APIC")) self.failUnless(is_valid_frame_id("TPE2")) def test_invalid(self): from mutagen.id3 import is_valid_frame_id self.failIf(is_valid_frame_id("MP3e")) self.failIf(is_valid_frame_id("+ABC")) add(FrameIDValidate) class TimeStampTextFrame(TestCase): from mutagen.id3 import TimeStampTextFrame as Frame Frame = Frame def test_compare_to_unicode(self): frame = self.Frame(encoding=0, text=[u'1987', u'1988']) self.failUnlessEqual(frame, unicode(frame)) add(TimeStampTextFrame) class TTextFrame(TestCase): def test_list_iface(self): from mutagen.id3 import TextFrame frame = TextFrame() frame.append("a") frame.extend(["b", "c"]) self.assertEqual(frame.text, ["a", "b", "c"]) add(TTextFrame) class TRVA2(TestCase): def test_basic(self): from mutagen.id3 import RVA2 r = RVA2(gain=1, channel=1, peak=1) self.assertReallyEqual(r, r) self.assertReallyNotEqual(r, 42) add(TRVA2) mutagen-1.22/tests/test_apev2.py0000644000175000017500000002756112212316534017126 0ustar lazkalazka00000000000000# FIXME: This test suite is a mess, a lot of it dates from PyMusepack so # it doesn't match the other Mutagen test conventions/quality. import os import shutil from tempfile import mkstemp from tests import TestCase, add import mutagen.apev2 from mutagen.apev2 import APEv2File, APEv2, is_valid_apev2_key DIR = os.path.dirname(__file__) SAMPLE = os.path.join(DIR, "data", "click.mpc") OLD = os.path.join(DIR, "data", "oldtag.apev2") BROKEN = os.path.join(DIR, "data", "brokentag.apev2") LYRICS2 = os.path.join(DIR, "data", "apev2-lyricsv2.mp3") INVAL_ITEM_COUNT = os.path.join(DIR, "data", "145-invalid-item-count.apev2") class Tis_valid_apev2_key(TestCase): def test_yes(self): for key in ["foo", "Foo", " f ~~~"]: self.failUnless(is_valid_apev2_key(key)) def test_no(self): for key in ["\x11hi", "ffoo\xFF", u"\u1234", "a", "", "foo" * 100]: self.failIf(is_valid_apev2_key(key)) add(Tis_valid_apev2_key) class TAPEInvalidItemCount(TestCase): # http://code.google.com/p/mutagen/issues/detail?id=145 def test_load(self): x = mutagen.apev2.APEv2(INVAL_ITEM_COUNT) self.failUnlessEqual(len(x.keys()), 17) add(TAPEInvalidItemCount) class TAPEWriter(TestCase): offset = 0 def setUp(self): shutil.copy(SAMPLE, SAMPLE + ".new") shutil.copy(BROKEN, BROKEN + ".new") tag = mutagen.apev2.APEv2() self.values = {"artist": "Joe Wreschnig\0unittest", "album": "Mutagen tests", "title": "Not really a song"} for k, v in self.values.items(): tag[k] = v tag.save(SAMPLE + ".new") tag.save(SAMPLE + ".justtag") tag.save(SAMPLE + ".tag_at_start") fileobj = open(SAMPLE + ".tag_at_start", "ab") fileobj.write("tag garbage" * 1000) fileobj.close() self.tag = mutagen.apev2.APEv2(SAMPLE + ".new") def test_changed(self): size = os.path.getsize(SAMPLE + ".new") self.tag.save() self.failUnlessEqual( os.path.getsize(SAMPLE + ".new"), size - self.offset) def test_fix_broken(self): # Clean up garbage from a bug in pre-Mutagen APEv2. # This also tests removing ID3v1 tags on writes. self.failIfEqual(os.path.getsize(OLD), os.path.getsize(BROKEN)) tag = mutagen.apev2.APEv2(BROKEN) tag.save(BROKEN + ".new") self.failUnlessEqual( os.path.getsize(OLD), os.path.getsize(BROKEN+".new")) def test_readback(self): for k, v in self.tag.items(): self.failUnlessEqual(str(v), self.values[k]) def test_size(self): self.failUnlessEqual( os.path.getsize(SAMPLE + ".new"), os.path.getsize(SAMPLE) + os.path.getsize(SAMPLE + ".justtag")) def test_delete(self): mutagen.apev2.delete(SAMPLE + ".justtag") tag = mutagen.apev2.APEv2(SAMPLE + ".new") tag.delete() self.failUnlessEqual(os.path.getsize(SAMPLE + ".justtag"), self.offset) self.failUnlessEqual(os.path.getsize(SAMPLE) + self.offset, os.path.getsize(SAMPLE + ".new")) self.failIf(tag) def test_empty(self): self.failUnlessRaises( IOError, mutagen.apev2.APEv2, os.path.join("tests", "data", "emptyfile.mp3")) def test_tag_at_start(self): filename = SAMPLE + ".tag_at_start" tag = mutagen.apev2.APEv2(filename) self.failUnlessEqual(tag["album"], "Mutagen tests") def test_tag_at_start_write(self): filename = SAMPLE + ".tag_at_start" tag = mutagen.apev2.APEv2(filename) tag.save() tag = mutagen.apev2.APEv2(filename) self.failUnlessEqual(tag["album"], "Mutagen tests") self.failUnlessEqual( os.path.getsize(SAMPLE + ".justtag"), os.path.getsize(filename) - (len("tag garbage") * 1000)) def test_tag_at_start_delete(self): filename = SAMPLE + ".tag_at_start" tag = mutagen.apev2.APEv2(filename) tag.delete() self.failUnlessRaises(IOError, mutagen.apev2.APEv2, filename) self.failUnlessEqual( os.path.getsize(filename), len("tag garbage") * 1000) def test_case_preservation(self): mutagen.apev2.delete(SAMPLE + ".justtag") tag = mutagen.apev2.APEv2(SAMPLE + ".new") tag["FoObaR"] = "Quux" tag.save() tag = mutagen.apev2.APEv2(SAMPLE + ".new") self.failUnless("FoObaR" in tag.keys()) self.failIf("foobar" in tag.keys()) def test_unicode_key(self): # http://code.google.com/p/mutagen/issues/detail?id=123 tag = mutagen.apev2.APEv2(SAMPLE + ".new") tag["abc"] = u'\xf6\xe4\xfc' tag[u"cba"] = "abc" tag.save() def tearDown(self): os.unlink(SAMPLE + ".new") os.unlink(BROKEN + ".new") os.unlink(SAMPLE + ".justtag") os.unlink(SAMPLE + ".tag_at_start") add(TAPEWriter) class TAPEv2ThenID3v1Writer(TAPEWriter): offset = 128 def setUp(self): super(TAPEv2ThenID3v1Writer, self).setUp() f = open(SAMPLE + ".new", "ab+") f.write("TAG" + "\x00" * 125) f.close() f = open(BROKEN + ".new", "ab+") f.write("TAG" + "\x00" * 125) f.close() f = open(SAMPLE + ".justtag", "ab+") f.write("TAG" + "\x00" * 125) f.close() def test_tag_at_start_write(self): pass add(TAPEv2ThenID3v1Writer) class TAPEv2(TestCase): def setUp(self): fd, self.filename = mkstemp(".apev2") os.close(fd) shutil.copy(OLD, self.filename) self.audio = APEv2(self.filename) def test_invalid_key(self): self.failUnlessRaises( KeyError, self.audio.__setitem__, u"\u1234", "foo") def test_guess_text(self): from mutagen.apev2 import APETextValue self.audio["test"] = u"foobar" self.failUnlessEqual(self.audio["test"], "foobar") self.failUnless(isinstance(self.audio["test"], APETextValue)) def test_guess_text_list(self): from mutagen.apev2 import APETextValue self.audio["test"] = [u"foobar", "quuxbarz"] self.failUnlessEqual(self.audio["test"], "foobar\x00quuxbarz") self.failUnless(isinstance(self.audio["test"], APETextValue)) def test_guess_utf8(self): from mutagen.apev2 import APETextValue self.audio["test"] = "foobar" self.failUnlessEqual(self.audio["test"], "foobar") self.failUnless(isinstance(self.audio["test"], APETextValue)) def test_guess_not_utf8(self): from mutagen.apev2 import APEBinaryValue self.audio["test"] = "\xa4woo" self.failUnless(isinstance(self.audio["test"], APEBinaryValue)) self.failUnlessEqual(4, len(self.audio["test"])) def test_bad_value_type(self): from mutagen.apev2 import APEValue self.failUnlessRaises(ValueError, APEValue, "foo", 99) def test_module_delete_empty(self): from mutagen.apev2 import delete delete(os.path.join("tests", "data", "emptyfile.mp3")) def test_invalid(self): self.failUnlessRaises(IOError, mutagen.apev2.APEv2, "dne") def test_no_tag(self): self.failUnlessRaises(IOError, mutagen.apev2.APEv2, os.path.join("tests", "data", "empty.mp3")) def test_cases(self): self.failUnlessEqual(self.audio["artist"], self.audio["ARTIST"]) self.failUnless("artist" in self.audio) self.failUnless("artisT" in self.audio) def test_keys(self): self.failUnless("Track" in self.audio.keys()) self.failUnless("AnArtist" in self.audio.values()) self.failUnlessEqual( self.audio.items(), zip(self.audio.keys(), self.audio.values())) def test_invalid_keys(self): self.failUnlessRaises(KeyError, self.audio.__getitem__, "\x00") self.failUnlessRaises(KeyError, self.audio.__setitem__, "\x00", "") self.failUnlessRaises(KeyError, self.audio.__delitem__, "\x00") def test_dictlike(self): self.failUnless(self.audio.get("track")) self.failUnless(self.audio.get("Track")) def test_del(self): s = self.audio["artist"] del(self.audio["artist"]) self.failIf("artist" in self.audio) self.failUnlessRaises(KeyError, self.audio.__getitem__, "artist") self.audio["Artist"] = s self.failUnlessEqual(self.audio["artist"], "AnArtist") def test_values(self): self.failUnlessEqual(self.audio["artist"], self.audio["artist"]) self.failUnless(self.audio["artist"] < self.audio["title"]) self.failUnlessEqual(self.audio["artist"], "AnArtist") self.failUnlessEqual(self.audio["title"], "Some Music") self.failUnlessEqual(self.audio["album"], "A test case") self.failUnlessEqual("07", self.audio["track"]) self.failIfEqual(self.audio["album"], "A test Case") def test_pprint(self): self.failUnless(self.audio.pprint()) def tearDown(self): os.unlink(self.filename) add(TAPEv2) class TAPEv2ThenID3v1(TAPEv2): def setUp(self): super(TAPEv2ThenID3v1, self).setUp() f = open(self.filename, "ab+") f.write("TAG" + "\x00" * 125) f.close() self.audio = APEv2(self.filename) add(TAPEv2ThenID3v1) class TAPEv2WithLyrics2(TestCase): def setUp(self): self.tag = mutagen.apev2.APEv2(LYRICS2) def test_values(self): self.failUnlessEqual(self.tag["MP3GAIN_MINMAX"], "000,179") self.failUnlessEqual(self.tag["REPLAYGAIN_TRACK_GAIN"], "-4.080000 dB") self.failUnlessEqual(self.tag["REPLAYGAIN_TRACK_PEAK"], "1.008101") add(TAPEv2WithLyrics2) class TAPEBinaryValue(TestCase): from mutagen.apev2 import APEBinaryValue as BV BV = BV def setUp(self): self.sample = "\x12\x45\xde" self.value = mutagen.apev2.APEValue(self.sample,mutagen.apev2.BINARY) def test_type(self): self.failUnless(isinstance(self.value, self.BV)) def test_const(self): self.failUnlessEqual(self.sample, str(self.value)) def test_repr(self): repr(self.value) def test_pprint(self): self.value.pprint() add(TAPEBinaryValue) class TAPETextValue(TestCase): from mutagen.apev2 import APETextValue as TV TV = TV def setUp(self): self.sample = ["foo", "bar", "baz"] self.value = mutagen.apev2.APEValue( "\0".join(self.sample), mutagen.apev2.TEXT) def test_type(self): self.failUnless(isinstance(self.value, self.TV)) def test_list(self): self.failUnlessEqual(self.sample, list(self.value)) def test_setitem_list(self): self.value[2] = self.sample[2] = 'quux' self.test_list() self.test_getitem() self.value[2] = self.sample[2] = 'baz' def test_getitem(self): for i in range(len(self.value)): self.failUnlessEqual(self.sample[i], self.value[i]) def test_repr(self): repr(self.value) add(TAPETextValue) class TAPEExtValue(TestCase): from mutagen.apev2 import APEExtValue as EV EV = EV def setUp(self): self.sample = "http://foo" self.value = mutagen.apev2.APEValue( self.sample, mutagen.apev2.EXTERNAL) def test_type(self): self.failUnless(isinstance(self.value, self.EV)) def test_const(self): self.failUnlessEqual(self.sample, str(self.value)) def test_repr(self): repr(self.value) def test_pprint(self): self.value.pprint() add(TAPEExtValue) class TAPEv2File(TestCase): def setUp(self): self.audio = APEv2File("tests/data/click.mpc") def test_add_tags(self): self.failUnless(self.audio.tags is None) self.audio.add_tags() self.failUnless(self.audio.tags is not None) self.failUnlessRaises(ValueError, self.audio.add_tags) def test_unknown_info(self): info = self.audio.info info.pprint() add(TAPEv2File) mutagen-1.22/tests/test_ogg.py0000644000175000017500000004525012211074434016657 0ustar lazkalazka00000000000000import os import random import shutil from StringIO import StringIO from tests import TestCase, add from mutagen.ogg import OggPage, error as OggError from mutagen._util import cdata from tempfile import mkstemp try: from os.path import devnull except ImportError: devnull = "/dev/null" class TOggPage(TestCase): def setUp(self): self.fileobj = open(os.path.join("tests", "data", "empty.ogg"), "rb") self.page = OggPage(self.fileobj) pages = [OggPage(), OggPage(), OggPage()] pages[0].packets = ["foo"] pages[1].packets = ["bar"] pages[2].packets = ["baz"] for i in range(len(pages)): pages[i].sequence = i for page in pages: page.serial = 1 self.pages = pages def test_flags(self): self.failUnless(self.page.first) self.failIf(self.page.continued) self.failIf(self.page.last) self.failUnless(self.page.complete) for first in [True, False]: self.page.first = first for last in [True, False]: self.page.last = last for continued in [True, False]: self.page.continued = continued self.failUnlessEqual(self.page.first, first) self.failUnlessEqual(self.page.last, last) self.failUnlessEqual(self.page.continued, continued) def test_flags_next_page(self): page = OggPage(self.fileobj) self.failIf(page.first) self.failIf(page.continued) self.failIf(page.last) def test_length(self): # Always true for Ogg Vorbis files self.failUnlessEqual(self.page.size, 58) self.failUnlessEqual(len(self.page.write()), 58) def test_first_metadata_page_is_separate(self): self.failIf(OggPage(self.fileobj).continued) def test_single_page_roundtrip(self): self.failUnlessEqual( self.page, OggPage(StringIO(self.page.write()))) def test_at_least_one_audio_page(self): page = OggPage(self.fileobj) while not page.last: page = OggPage(self.fileobj) self.failUnless(page.last) def test_crappy_fragmentation(self): packets = ["1" * 511, "2" * 511, "3" * 511] pages = OggPage.from_packets(packets, default_size=510, wiggle_room=0) self.failUnless(len(pages) > 3) self.failUnlessEqual(OggPage.to_packets(pages), packets) def test_wiggle_room(self): packets = ["1" * 511, "2" * 511, "3" * 511] pages = OggPage.from_packets(packets, default_size=510, wiggle_room=100) self.failUnlessEqual(len(pages), 3) self.failUnlessEqual(OggPage.to_packets(pages), packets) def test_one_packet_per_wiggle(self): packets = ["1" * 511, "2" * 511, "3" * 511] pages = OggPage.from_packets( packets, default_size=1000, wiggle_room=1000000) self.failUnlessEqual(len(pages), 2) self.failUnlessEqual(OggPage.to_packets(pages), packets) def test_renumber(self): self.failUnlessEqual( [page.sequence for page in self.pages], [0, 1, 2]) fileobj = StringIO() for page in self.pages: fileobj.write(page.write()) fileobj.seek(0) OggPage.renumber(fileobj, 1, 10) fileobj.seek(0) pages = [OggPage(fileobj) for i in range(3)] self.failUnlessEqual([page.sequence for page in pages], [10, 11, 12]) fileobj.seek(0) OggPage.renumber(fileobj, 1, 20) fileobj.seek(0) pages = [OggPage(fileobj) for i in range(3)] self.failUnlessEqual([page.sequence for page in pages], [20, 21, 22]) def test_renumber_extradata(self): fileobj = StringIO() for page in self.pages: fileobj.write(page.write()) fileobj.write("left over data") fileobj.seek(0) # Trying to rewrite should raise an error... self.failUnlessRaises(Exception, OggPage.renumber, fileobj, 1, 10) fileobj.seek(0) # But the already written data should remain valid, pages = [OggPage(fileobj) for i in range(3)] self.failUnlessEqual([page.sequence for page in pages], [10, 11, 12]) # And the garbage that caused the error should be okay too. self.failUnlessEqual(fileobj.read(), "left over data") def test_renumber_reread(self): try: fd, filename = mkstemp(suffix=".ogg") os.close(fd) shutil.copy(os.path.join("tests", "data", "multipagecomment.ogg"), filename) fileobj = open(filename, "rb+") OggPage.renumber(fileobj, 1002429366L, 20) fileobj.close() fileobj = open(filename, "rb+") OggPage.renumber(fileobj, 1002429366L, 0) fileobj.close() finally: try: os.unlink(filename) except OSError: pass def test_renumber_muxed(self): pages = [OggPage() for i in range(10)] for seq, page in enumerate(pages[0:1] + pages[2:]): page.serial = 0 page.sequence = seq pages[1].serial = 2 pages[1].sequence = 100 data = StringIO("".join([page.write() for page in pages])) OggPage.renumber(data, 0, 20) data.seek(0) pages = [OggPage(data) for i in range(10)] self.failUnlessEqual(pages[1].serial, 2) self.failUnlessEqual(pages[1].sequence, 100) pages.pop(1) self.failUnlessEqual([page.sequence for page in pages], range(20, 29)) def test_to_packets(self): self.failUnlessEqual( ["foo", "bar", "baz"], OggPage.to_packets(self.pages)) self.pages[0].complete = False self.pages[1].continued = True self.failUnlessEqual( ["foobar", "baz"], OggPage.to_packets(self.pages)) def test_to_packets_mixed_stream(self): self.pages[0].serial = 3 self.failUnlessRaises(ValueError, OggPage.to_packets, self.pages) def test_to_packets_missing_sequence(self): self.pages[0].sequence = 3 self.failUnlessRaises(ValueError, OggPage.to_packets, self.pages) def test_to_packets_continued(self): self.pages[0].continued = True self.failUnlessEqual( OggPage.to_packets(self.pages), ["foo", "bar", "baz"]) def test_to_packets_continued_strict(self): self.pages[0].continued = True self.failUnlessRaises( ValueError, OggPage.to_packets, self.pages, strict=True) def test_to_packets_strict(self): for page in self.pages: page.complete = False self.failUnlessRaises( ValueError, OggPage.to_packets, self.pages, strict=True) def test_from_packets_short_enough(self): packets = ["1" * 200, "2" * 200, "3" * 200] pages = OggPage.from_packets(packets) self.failUnlessEqual(OggPage.to_packets(pages), packets) def test_from_packets_position(self): packets = ["1" * 100000] pages = OggPage.from_packets(packets) self.failUnless(len(pages) > 1) for page in pages[:-1]: self.failUnlessEqual(-1, page.position) self.failUnlessEqual(0, pages[-1].position) def test_from_packets_long(self): packets = ["1" * 100000, "2" * 100000, "3" * 100000] pages = OggPage.from_packets(packets) self.failIf(pages[0].complete) self.failUnless(pages[1].continued) self.failUnlessEqual(OggPage.to_packets(pages), packets) def test_random_data_roundtrip(self): try: random_file = open("/dev/urandom", "rb") except (IOError, OSError): print "WARNING: Random data round trip test disabled." return for i in range(10): num_packets = random.randrange(2, 100) lengths = [random.randrange(10, 10000) for i in range(num_packets)] packets = map(random_file.read, lengths) self.failUnlessEqual( packets, OggPage.to_packets(OggPage.from_packets(packets))) def test_packet_exactly_255(self): page = OggPage() page.packets = ["1" * 255] page.complete = False page2 = OggPage() page2.packets = [""] page2.sequence = 1 page2.continued = True self.failUnlessEqual( ["1" * 255], OggPage.to_packets([page, page2])) def test_page_max_size_alone_too_big(self): page = OggPage() page.packets = ["1" * 255 * 255] page.complete = True self.failUnlessRaises(ValueError, page.write) def test_page_max_size(self): page = OggPage() page.packets = ["1" * 255 * 255] page.complete = False page2 = OggPage() page2.packets = [""] page2.sequence = 1 page2.continued = True self.failUnlessEqual( ["1" * 255 * 255], OggPage.to_packets([page, page2])) def test_complete_zero_length(self): packets = [""] * 20 page = OggPage.from_packets(packets)[0] new_page = OggPage(StringIO(page.write())) self.failUnlessEqual(new_page, page) self.failUnlessEqual(OggPage.to_packets([new_page]), packets) def test_too_many_packets(self): packets = ["1"] * 3000 pages = OggPage.from_packets(packets) map(OggPage.write, pages) self.failUnless(len(pages) > 3000/255) def test_read_max_size(self): page = OggPage() page.packets = ["1" * 255 * 255] page.complete = False page2 = OggPage() page2.packets = ["", "foo"] page2.sequence = 1 page2.continued = True data = page.write() + page2.write() fileobj = StringIO(data) self.failUnlessEqual(OggPage(fileobj), page) self.failUnlessEqual(OggPage(fileobj), page2) self.failUnlessRaises(EOFError, OggPage, fileobj) def test_invalid_version(self): page = OggPage() OggPage(StringIO(page.write())) page.version = 1 self.failUnlessRaises(OggError, OggPage, StringIO(page.write())) def test_not_enough_lacing(self): data = OggPage().write()[:-1] + "\x10" self.failUnlessRaises(OggError, OggPage, StringIO(data)) def test_not_enough_data(self): data = OggPage().write()[:-1] + "\x01\x10" self.failUnlessRaises(OggError, OggPage, StringIO(data)) def test_not_equal(self): self.failIfEqual(OggPage(), 12) def test_find_last(self): pages = [OggPage() for i in range(10)] for i, page in enumerate(pages): page.sequence = i data = StringIO("".join([page.write() for page in pages])) self.failUnlessEqual( OggPage.find_last(data, pages[0].serial), pages[-1]) def test_find_last_really_last(self): pages = [OggPage() for i in range(10)] pages[-1].last = True for i, page in enumerate(pages): page.sequence = i data = StringIO("".join([page.write() for page in pages])) self.failUnlessEqual( OggPage.find_last(data, pages[0].serial), pages[-1]) def test_find_last_muxed(self): pages = [OggPage() for i in range(10)] for i, page in enumerate(pages): page.sequence = i pages[-2].last = True pages[-1].serial = pages[0].serial + 1 data = StringIO("".join([page.write() for page in pages])) self.failUnlessEqual( OggPage.find_last(data, pages[0].serial), pages[-2]) def test_find_last_no_serial(self): pages = [OggPage() for i in range(10)] for i, page in enumerate(pages): page.sequence = i data = StringIO("".join([page.write() for page in pages])) self.failUnless(OggPage.find_last(data, pages[0].serial + 1) is None) def test_find_last_invalid(self): data = StringIO("if you think this is an Ogg, you're crazy") self.failUnlessRaises(OggError, OggPage.find_last, data, 0) # Disabled because GStreamer will write Oggs with bad data, # which we need to make a best guess for. # #def test_find_last_invalid_sync(self): # data = StringIO("if you think this is an OggS, you're crazy") # self.failUnlessRaises(OggError, OggPage.find_last, data, 0) def test_find_last_invalid_sync(self): data = StringIO("if you think this is an OggS, you're crazy") page = OggPage.find_last(data, 0) self.failIf(page) def test_crc_py25(self): # Make sure page.write can handle both signed/unsigned int # return values of crc32. # http://code.google.com/p/mutagen/issues/detail?id=63 # http://docs.python.org/library/zlib.html#zlib.crc32 import zlib old_crc = zlib.crc32 def zlib_uint(*args): return (old_crc(*args) & 0xffffffff) def zlib_int(*args): return cdata.int_be(cdata.to_uint_be(old_crc(*args) & 0xffffffff)) try: page = OggPage() page.packets = ["abc"] zlib.crc32 = zlib_uint uint_data = page.write() zlib.crc32 = zlib_int int_data = page.write() finally: zlib.crc32 = old_crc self.failUnlessEqual(uint_data, int_data) def tearDown(self): self.fileobj.close() add(TOggPage) class TOggFileType(TestCase): def scan_file(self): fileobj = open(self.filename, "rb") try: try: while True: OggPage(fileobj) except EOFError: pass finally: fileobj.close() def test_pprint_empty(self): self.audio.pprint() def test_pprint_stuff(self): self.test_set_two_tags() self.audio.pprint() def test_length(self): self.failUnlessAlmostEqual(3.7, self.audio.info.length, 1) def test_no_tags(self): self.failIf(self.audio.tags) self.failIf(self.audio.tags is None) def test_vendor_safe(self): self.audio["vendor"] = "a vendor" self.audio.save() audio = self.Kind(self.filename) self.failUnlessEqual(audio["vendor"], ["a vendor"]) def test_set_two_tags(self): self.audio["foo"] = ["a"] self.audio["bar"] = ["b"] self.audio.save() audio = self.Kind(self.filename) self.failUnlessEqual(len(audio.tags.keys()), 2) self.failUnlessEqual(audio["foo"], ["a"]) self.failUnlessEqual(audio["bar"], ["b"]) self.scan_file() def test_save_twice(self): self.audio.save() self.audio.save() self.failUnlessEqual(self.Kind(self.filename).tags, self.audio.tags) self.scan_file() def test_set_delete(self): self.test_set_two_tags() self.audio.tags.clear() self.audio.save() audio = self.Kind(self.filename) self.failIf(audio.tags) self.scan_file() def test_delete(self): self.test_set_two_tags() self.audio.delete() self.failIf(self.audio.tags) audio = self.Kind(self.filename) self.failIf(audio.tags) self.audio["foobar"] = "foobar" * 1000 self.audio.save() audio = self.Kind(self.filename) self.failUnlessEqual(self.audio["foobar"], audio["foobar"]) self.scan_file() def test_really_big(self): self.audio["foo"] = "foo" * (2**16) self.audio["bar"] = "bar" * (2**16) self.audio["baz"] = "quux" * (2**16) self.audio.save() audio = self.Kind(self.filename) self.failUnlessEqual(audio["foo"], ["foo" * 2**16]) self.failUnlessEqual(audio["bar"], ["bar" * 2**16]) self.failUnlessEqual(audio["baz"], ["quux" * 2**16]) self.scan_file() def test_delete_really_big(self): self.audio["foo"] = "foo" * (2**16) self.audio["bar"] = "bar" * (2**16) self.audio["baz"] = "quux" * (2**16) self.audio.save() self.audio.delete() audio = self.Kind(self.filename) self.failIf(audio.tags) self.scan_file() def test_invalid_open(self): self.failUnlessRaises(IOError, self.Kind, os.path.join('tests', 'data', 'xing.mp3')) def test_invalid_delete(self): self.failUnlessRaises(IOError, self.audio.delete, os.path.join('tests', 'data', 'xing.mp3')) def test_invalid_save(self): self.failUnlessRaises(IOError, self.audio.save, os.path.join('tests', 'data', 'xing.mp3')) def ogg_reference(self, filename): self.scan_file() if have_ogginfo: value = os.system("ogginfo %s > %s 2> %s" % (filename, devnull, devnull)) self.failIf(value and value != NOTFOUND, "ogginfo failed on %s" % filename) if have_oggz_validate: if filename.endswith(".opus") and not have_oggz_validate_opus: return value = os.system( "oggz-validate %s > %s" % (filename, devnull)) self.failIf(value and value != NOTFOUND, "oggz-validate failed on %s" % filename) def test_ogg_reference_simple_save(self): self.audio.save() self.ogg_reference(self.filename) def test_ogg_reference_really_big(self): self.test_really_big() self.audio.save() self.ogg_reference(self.filename) def test_ogg_reference_delete(self): self.audio.delete() self.ogg_reference(self.filename) def test_ogg_reference_medium_sized(self): self.audio["foobar"] = "foobar" * 1000 self.audio.save() self.ogg_reference(self.filename) def test_ogg_reference_delete_readd(self): self.audio.delete() self.audio.tags.clear() self.audio["foobar"] = "foobar" * 1000 self.audio.save() self.ogg_reference(self.filename) def test_mime_secondary(self): self.failUnless('application/ogg' in self.audio.mime) def tearDown(self): os.unlink(self.filename) NOTFOUND = os.system("tools/notarealprogram 2> %s" % devnull) have_ogginfo = True if os.system("ogginfo 2> %s > %s" % (devnull, devnull)) == NOTFOUND: have_ogginfo = False print "WARNING: Skipping ogginfo reference tests." have_oggz_validate = True have_oggz_validate_opus = True if os.system("oggz-validate 2> %s > %s" % (devnull, devnull)) == NOTFOUND: have_oggz_validate = False print "WARNING: Skipping oggz-validate reference tests." else: f = os.popen("oggz-validate --version") try: version_string = f.read() version_part = version_string.split()[-1] version = tuple(map(int, version_part.split("."))) if version <= (0, 9, 9): have_oggz_validate_opus = False print "WARNING: Skipping oggz-validate reference tests for opus" finally: f.close() mutagen-1.22/tests/test_optimfrog.py0000644000175000017500000000224712211074436020112 0ustar lazkalazka00000000000000import os from mutagen.optimfrog import OptimFROG, OptimFROGHeaderError from tests import TestCase, add class TOptimFROG(TestCase): def setUp(self): self.ofr = OptimFROG(os.path.join("tests", "data", "empty.ofr")) self.ofs = OptimFROG(os.path.join("tests", "data", "empty.ofs")) def test_channels(self): self.failUnlessEqual(self.ofr.info.channels, 2) self.failUnlessEqual(self.ofs.info.channels, 2) def test_sample_rate(self): self.failUnlessEqual(self.ofr.info.sample_rate, 44100) self.failUnlessEqual(self.ofs.info.sample_rate, 44100) def test_length(self): self.failUnlessAlmostEqual(self.ofr.info.length, 3.68, 2) self.failUnlessAlmostEqual(self.ofs.info.length, 3.68, 2) def test_not_my_file(self): self.failUnlessRaises( OptimFROGHeaderError, OptimFROG, os.path.join("tests", "data", "empty.ogg")) self.failUnlessRaises( OptimFROGHeaderError, OptimFROG, os.path.join("tests", "data", "click.mpc")) def test_pprint(self): self.failUnless(self.ofr.pprint()) self.failUnless(self.ofs.pprint()) add(TOptimFROG) mutagen-1.22/tests/test_tools_mid3iconv.py0000644000175000017500000000530212211661424021211 0ustar lazkalazka00000000000000import os from tempfile import mkstemp import shutil from mutagen.id3 import ID3 from tests import add from tests.test_tools import _TTools AMBIGUOUS = "\xc3\xae\xc3\xa5\xc3\xb4\xc3\xb2 \xc3\xa0\xc3\xa9\xc3\xa7\xc3" \ "\xa5\xc3\xa3 \xc3\xb9\xc3\xac \xc3\xab\xc3\xa5\xc3\xa5\xc3\xb8" \ "\xc3\xba" CODECS = ["utf8", "latin-1", "Windows-1255", "gbk"] class TMid3Iconv(_TTools): TOOL_NAME = "mid3iconv" def setUp(self): super(TMid3Iconv, self).setUp() original = os.path.join('tests', 'data', 'silence-44-s.mp3') fd, self.filename = mkstemp(suffix='.mp3') os.close(fd) shutil.copy(original, self.filename) def tearDown(self): super(TMid3Iconv, self).tearDown() os.unlink(self.filename) def test_noop(self): res, out = self.call() self.failIf(res) self.failUnless("Usage:" in out) def test_debug(self): res, out = self.call("-d", "-p", self.filename) self.failIf(res) self.failUnless("TCON=Silence" in out) def test_quiet(self): res, out = self.call("-q", self.filename) self.failIf(res) self.failIf(out) def test_test_data(self): results = set() for codec in CODECS: results.add(AMBIGUOUS.decode(codec)) self.failUnlessEqual(len(results), len(CODECS)) def test_conv_basic(self): from mutagen.id3 import TALB for codec in CODECS: f = ID3(self.filename) f.add(TALB(text=[AMBIGUOUS.decode("latin-1")], encoding=0)) f.save() res, out = self.call("-d", "-e", codec, self.filename) f = ID3(self.filename) self.failUnlessEqual(f["TALB"].encoding, 1) self.failUnlessEqual(f["TALB"].text[0] , AMBIGUOUS.decode(codec)) def test_comm(self): from mutagen.id3 import COMM for codec in CODECS: f = ID3(self.filename) frame = COMM(desc="", lang="eng", encoding=0, text=[AMBIGUOUS.decode("latin-1")]) f.add(frame) f.save() res, out = self.call("-d", "-e", codec, self.filename) f = ID3(self.filename) new_frame = f[frame.HashKey] self.failUnlessEqual(new_frame.encoding, 1) self.failUnlessEqual(new_frame.text[0] , AMBIGUOUS.decode(codec)) def test_remove_v1(self): from mutagen.id3 import ParseID3v1 res, out = self.call("--remove-v1", self.filename) with open(self.filename, "rb") as h: h.seek(-128, 2) data = h.read() self.failUnlessEqual(len(data), 128) self.failIf(ParseID3v1(data)) add(TMid3Iconv) mutagen-1.22/tests/test_mp3.py0000644000175000017500000001447612211074413016605 0ustar lazkalazka00000000000000import os import shutil from tests import TestCase from cStringIO import StringIO from tests import add from mutagen.mp3 import MP3, error as MP3Error, delete, MPEGInfo, EasyMP3 from mutagen.id3 import ID3 from tempfile import mkstemp class TMP3(TestCase): silence = os.path.join('tests', 'data', 'silence-44-s.mp3') silence_nov2 = os.path.join('tests', 'data', 'silence-44-s-v1.mp3') silence_mpeg2 = os.path.join('tests', 'data', 'silence-44-s-mpeg2.mp3') silence_mpeg25 = os.path.join('tests', 'data', 'silence-44-s-mpeg25.mp3') def setUp(self): original = os.path.join("tests", "data", "silence-44-s.mp3") fd, self.filename = mkstemp(suffix='.mp3') os.close(fd) shutil.copy(original, self.filename) self.mp3 = MP3(self.filename) self.mp3_2 = MP3(self.silence_nov2) self.mp3_3 = MP3(self.silence_mpeg2) self.mp3_4 = MP3(self.silence_mpeg25) def test_mode(self): from mutagen.mp3 import JOINTSTEREO self.failUnlessEqual(self.mp3.info.mode, JOINTSTEREO) self.failUnlessEqual(self.mp3_2.info.mode, JOINTSTEREO) self.failUnlessEqual(self.mp3_3.info.mode, JOINTSTEREO) self.failUnlessEqual(self.mp3_4.info.mode, JOINTSTEREO) def test_id3(self): self.failUnlessEqual(self.mp3.tags, ID3(self.silence)) self.failUnlessEqual(self.mp3_2.tags, ID3(self.silence_nov2)) def test_length(self): self.assertAlmostEquals(self.mp3.info.length, 3.77, 2) self.assertAlmostEquals(self.mp3_2.info.length, 3.77, 2) self.assertAlmostEquals(self.mp3_3.info.length, 3.77, 2) self.assertAlmostEquals(self.mp3_4.info.length, 3.84, 2) def test_version(self): self.failUnlessEqual(self.mp3.info.version, 1) self.failUnlessEqual(self.mp3_2.info.version, 1) self.failUnlessEqual(self.mp3_3.info.version, 2) self.failUnlessEqual(self.mp3_4.info.version, 2.5) def test_layer(self): self.failUnlessEqual(self.mp3.info.layer, 3) self.failUnlessEqual(self.mp3_2.info.layer, 3) self.failUnlessEqual(self.mp3_3.info.layer, 3) self.failUnlessEqual(self.mp3_4.info.layer, 3) def test_bitrate(self): self.failUnlessEqual(self.mp3.info.bitrate, 32000) self.failUnlessEqual(self.mp3_2.info.bitrate, 32000) self.failUnlessEqual(self.mp3_3.info.bitrate, 18191) self.failUnlessEqual(self.mp3_4.info.bitrate, 9300) def test_notmp3(self): self.failUnlessRaises(MP3Error, MP3, "README") def test_sketchy(self): self.failIf(self.mp3.info.sketchy) self.failIf(self.mp3_2.info.sketchy) self.failIf(self.mp3_3.info.sketchy) self.failIf(self.mp3_4.info.sketchy) def test_sketchy_notmp3(self): notmp3 = MP3(os.path.join("tests", "data", "silence-44-s.flac")) self.failUnless(notmp3.info.sketchy) def test_pprint(self): self.failUnless(self.mp3.pprint()) def test_pprint_no_tags(self): self.mp3.tags = None self.failUnless(self.mp3.pprint()) def test_xing(self): mp3 = MP3(os.path.join("tests", "data", "xing.mp3")) self.failUnlessEqual(int(round(mp3.info.length)), 26122) self.failUnlessEqual(mp3.info.bitrate, 306) def test_vbri(self): mp3 = MP3(os.path.join("tests", "data", "vbri.mp3")) self.failUnlessEqual(int(round(mp3.info.length)), 222) def test_empty_xing(self): MP3(os.path.join("tests", "data", "bad-xing.mp3")) def test_delete(self): self.mp3.delete() self.failIf(self.mp3.tags) self.failUnless(MP3(self.filename).tags is None) def test_module_delete(self): delete(self.filename) self.failUnless(MP3(self.filename).tags is None) def test_save(self): self.mp3["TIT1"].text = ["foobar"] self.mp3.save() self.failUnless(MP3(self.filename)["TIT1"] == "foobar") def test_load_non_id3(self): filename = os.path.join("tests", "data", "apev2-lyricsv2.mp3") from mutagen.apev2 import APEv2 mp3 = MP3(filename, ID3=APEv2) self.failUnless("replaygain_track_peak" in mp3.tags) def test_add_tags(self): mp3 = MP3(os.path.join("tests", "data", "xing.mp3")) self.failIf(mp3.tags) mp3.add_tags() self.failUnless(isinstance(mp3.tags, ID3)) def test_add_tags_already_there(self): mp3 = MP3(os.path.join("tests", "data", "silence-44-s.mp3")) self.failUnless(mp3.tags) self.failUnlessRaises(Exception, mp3.add_tags) def test_save_no_tags(self): self.mp3.tags = None self.failUnlessRaises(ValueError, self.mp3.save) def test_mime(self): self.failUnless("audio/mp3" in self.mp3.mime) def tearDown(self): os.unlink(self.filename) add(TMP3) class TMPEGInfo(TestCase): def test_not_real_file(self): filename = os.path.join("tests", "data", "silence-44-s-v1.mp3") fileobj = StringIO(open(filename, "rb").read(20)) MPEGInfo(fileobj) def test_empty(self): fileobj = StringIO("") self.failUnlessRaises(IOError, MPEGInfo, fileobj) add(TMPEGInfo) class TEasyMP3(TestCase): def setUp(self): original = os.path.join("tests", "data", "silence-44-s.mp3") fd, self.filename = mkstemp(suffix='.mp3') os.close(fd) shutil.copy(original, self.filename) self.mp3 = EasyMP3(self.filename) def test_artist(self): self.failUnless("artist" in self.mp3) def test_no_composer(self): self.failIf("composer" in self.mp3) def test_length(self): # http://code.google.com/p/mutagen/issues/detail?id=125 # easyid3, normal id3 and mpeg loading without tags should skip # the tags and get the right offset of the first frame easy = self.mp3.info noneasy = MP3(self.filename).info nonid3 = MPEGInfo(open(self.filename, "rb")) self.failUnlessEqual(easy.length, noneasy.length) self.failUnlessEqual(noneasy.length, nonid3.length) def tearDown(self): os.unlink(self.filename) add(TEasyMP3) class Issue72_TooShortFile(TestCase): def test_load(self): mp3 = MP3(os.path.join('tests', 'data', 'too-short.mp3')) self.failUnlessEqual(mp3["TIT2"], "Track 10") self.failUnlessAlmostEqual(mp3.info.length, 0.03, 2) add(Issue72_TooShortFile) mutagen-1.22/tests/test_m4a.py0000644000175000017500000002124412211074403016555 0ustar lazkalazka00000000000000import os import shutil from cStringIO import StringIO from tempfile import mkstemp from tests import TestCase, add import warnings warnings.simplefilter("ignore", DeprecationWarning) from mutagen.m4a import M4A, Atom, Atoms, M4ATags, M4AInfo, \ delete, M4ACover, M4AMetadataError try: from os.path import devnull except ImportError: devnull = "/dev/null" class TAtom(TestCase): def test_no_children(self): fileobj = StringIO("\x00\x00\x00\x08atom") atom = Atom(fileobj) self.failUnlessRaises(KeyError, atom.__getitem__, "test") def test_length_1(self): fileobj = StringIO("\x00\x00\x00\x01atom" + "\x00" * 8) self.failUnlessRaises(IOError, Atom, fileobj) def test_render_too_big(self): class TooBig(str): def __len__(self): return 1L << 32 data = TooBig("test") try: len(data) except OverflowError: # Py_ssize_t is still only 32 bits on this system. self.failUnlessRaises(OverflowError, Atom.render, "data", data) else: data = Atom.render("data", data) self.failUnlessEqual(len(data), 4 + 4 + 8 + 4) def test_length_0(self): fileobj = StringIO("\x00\x00\x00\x00atom") Atom(fileobj) self.failUnlessEqual(fileobj.tell(), 8) add(TAtom) class TAtoms(TestCase): filename = os.path.join("tests", "data", "has-tags.m4a") def setUp(self): self.atoms = Atoms(open(self.filename, "rb")) def test___contains__(self): self.failUnless(self.atoms["moov"]) self.failUnless(self.atoms["moov.udta"]) self.failUnlessRaises(KeyError, self.atoms.__getitem__, "whee") def test_name(self): self.failUnlessEqual(self.atoms.atoms[0].name, "ftyp") def test_children(self): self.failUnless(self.atoms.atoms[2].children) def test_no_children(self): self.failUnless(self.atoms.atoms[0].children is None) def test_repr(self): repr(self.atoms) add(TAtoms) class TM4AInfo(TestCase): def test_no_soun(self): self.failUnlessRaises( IOError, self.test_mdhd_version_1, "no so und data here") def test_mdhd_version_1(self, soun="soun"): mdhd = Atom.render("mdhd", ("\x01\x00\x00\x00" + "\x00" * 16 + "\x00\x00\x00\x02" + # 2 Hz "\x00\x00\x00\x00\x00\x00\x00\x10")) hdlr = Atom.render("hdlr", soun) mdia = Atom.render("mdia", mdhd + hdlr) trak = Atom.render("trak", mdia) moov = Atom.render("moov", trak) fileobj = StringIO(moov) atoms = Atoms(fileobj) info = M4AInfo(atoms, fileobj) self.failUnlessEqual(info.length, 8) add(TM4AInfo) class TM4ATags(TestCase): def wrap_ilst(self, data): ilst = Atom.render("ilst", data) meta = Atom.render("meta", "\x00" * 4 + ilst) data = Atom.render("moov", Atom.render("udta", meta)) fileobj = StringIO(data) return M4ATags(Atoms(fileobj), fileobj) def test_bad_freeform(self): mean = Atom.render("mean", "net.sacredchao.Mutagen") name = Atom.render("name", "empty test key") bad_freeform = Atom.render("----", "\x00" * 4 + mean + name) self.failIf(self.wrap_ilst(bad_freeform)) def test_genre(self): data = Atom.render("data", "\x00" * 8 + "\x00\x01") genre = Atom.render("gnre", data) tags = self.wrap_ilst(genre) self.failIf("gnre" in tags) self.failUnlessEqual(tags.get("\xa9gen"), "Blues") def test_empty_cpil(self): cpil = Atom.render("cpil", Atom.render("data", "\x00" * 8)) tags = self.wrap_ilst(cpil) self.failUnless("cpil" in tags) self.failIf(tags["cpil"]) def test_genre_too_big(self): data = Atom.render("data", "\x00" * 8 + "\x01\x00") genre = Atom.render("gnre", data) tags = self.wrap_ilst(genre) self.failIf("gnre" in tags) self.failIf("\xa9gen" in tags) def test_strips_unknown_types(self): data = Atom.render("data", "\x00" * 8 + "whee") foob = Atom.render("foob", data) tags = self.wrap_ilst(foob) self.failIf(tags) def test_bad_covr(self): data = Atom.render("foob", "\x00\x00\x00\x0E" + "\x00" * 4 + "whee") covr = Atom.render("covr", data) self.failUnlessRaises(M4AMetadataError, self.wrap_ilst, covr) add(TM4ATags) class TM4A(TestCase): def setUp(self): fd, self.filename = mkstemp(suffix='m4a') os.close(fd) shutil.copy(self.original, self.filename) self.audio = M4A(self.filename) def faad(self): if not have_faad: return value = os.system( "faad %s -o %s > %s 2> %s" % ( self.filename, devnull, devnull, devnull)) self.failIf(value and value != NOTFOUND) def test_bitrate(self): self.failUnlessEqual(self.audio.info.bitrate, 2914) def test_length(self): self.failUnlessAlmostEqual(3.7, self.audio.info.length, 1) def set_key(self, key, value): self.audio[key] = value self.audio.save() audio = M4A(self.audio.filename) self.failUnless(key in audio) self.failUnlessEqual(audio[key], value) self.faad() def test_save_text(self): self.set_key('\xa9nam', u"Some test name") def test_freeform(self): self.set_key('----:net.sacredchao.Mutagen:test key', "whee") def test_tracknumber(self): self.set_key('trkn', (1, 10)) def test_disk(self): self.set_key('disk', (18, 0)) def test_tracknumber_too_small(self): self.failUnlessRaises(ValueError, self.set_key, 'trkn', (-1, 0)) self.failUnlessRaises(ValueError, self.set_key, 'trkn', (2**18, 1)) def test_disk_too_small(self): self.failUnlessRaises(ValueError, self.set_key, 'disk', (-1, 0)) self.failUnlessRaises(ValueError, self.set_key, 'disk', (2**18, 1)) def test_tracknumber_wrong_size(self): self.failUnlessRaises(ValueError, self.set_key, 'trkn', (1,)) self.failUnlessRaises(ValueError, self.set_key, 'trkn', (1, 2, 3,)) def test_disk_wrong_size(self): self.failUnlessRaises(ValueError, self.set_key, 'disk', (1,)) self.failUnlessRaises(ValueError, self.set_key, 'disk', (1, 2, 3,)) def test_tempo(self): self.set_key('tmpo', 150) def test_tempo_invalid(self): self.failUnlessRaises(ValueError, self.set_key, 'tmpo', 100000) def test_compilation(self): self.set_key('cpil', True) def test_compilation_false(self): self.set_key('cpil', False) def test_cover(self): self.set_key('covr', 'woooo') def test_cover_png(self): self.set_key('covr', M4ACover('woooo', M4ACover.FORMAT_PNG)) def test_cover_jpeg(self): self.set_key('covr', M4ACover('hoooo', M4ACover.FORMAT_JPEG)) def test_pprint(self): self.audio.pprint() def test_pprint_binary(self): self.audio["covr"] = "\x00\xa9\garbage" self.audio.pprint() def test_delete(self): self.audio.delete() audio = M4A(self.audio.filename) self.failIf(audio.tags) self.faad() def test_module_delete(self): delete(self.filename) audio = M4A(self.audio.filename) self.failIf(audio.tags) self.faad() def test_reads_unknown_text(self): self.set_key("foob", u"A test") def test_mime(self): self.failUnless("audio/mp4" in self.audio.mime) def tearDown(self): os.unlink(self.filename) class TM4AHasTags(TM4A): original = os.path.join("tests", "data", "has-tags.m4a") def test_save_simple(self): self.audio.save() self.faad() def test_shrink(self): map(self.audio.__delitem__, self.audio.keys()) self.audio.save() audio = M4A(self.audio.filename) self.failIf(audio.tags) def test_has_tags(self): self.failUnless(self.audio.tags) def test_has_covr(self): self.failUnless('covr' in self.audio.tags) covr = self.audio.tags['covr'] self.failUnlessEqual(covr.imageformat, M4ACover.FORMAT_PNG) def test_not_my_file(self): self.failUnlessRaises( IOError, M4A, os.path.join("tests", "data", "empty.ogg")) add(TM4AHasTags) class TM4ANoTags(TM4A): original = os.path.join("tests", "data", "no-tags.m4a") def test_no_tags(self): self.failUnless(self.audio.tags is None) add(TM4ANoTags) NOTFOUND = os.system("tools/notarealprogram 2> %s" % devnull) have_faad = True if os.system("faad 2> %s > %s" % (devnull, devnull)) == NOTFOUND: have_faad = False print "WARNING: Skipping FAAD reference tests." mutagen-1.22/tests/test_oggtheora.py0000644000175000017500000000433612177421270020067 0ustar lazkalazka00000000000000import os import shutil from tempfile import mkstemp from cStringIO import StringIO from mutagen.oggtheora import OggTheora, OggTheoraInfo, delete from mutagen.ogg import OggPage from tests import add from tests.test_ogg import TOggFileType class TOggTheora(TOggFileType): Kind = OggTheora def setUp(self): original = os.path.join("tests", "data", "sample.oggtheora") fd, self.filename = mkstemp(suffix='.ogg') os.close(fd) shutil.copy(original, self.filename) self.audio = OggTheora(self.filename) self.audio2 = OggTheora( os.path.join("tests", "data", "sample_length.oggtheora")) self.audio3 = OggTheora( os.path.join("tests", "data", "sample_bitrate.oggtheora")) def test_theora_bad_version(self): page = OggPage(open(self.filename, "rb")) packet = page.packets[0] packet = packet[:7] + "\x03\x00" + packet[9:] page.packets = [packet] fileobj = StringIO(page.write()) self.failUnlessRaises(IOError, OggTheoraInfo, fileobj) def test_theora_not_first_page(self): page = OggPage(open(self.filename, "rb")) page.first = False fileobj = StringIO(page.write()) self.failUnlessRaises(IOError, OggTheoraInfo, fileobj) def test_vendor(self): self.failUnless( self.audio.tags.vendor.startswith("Xiph.Org libTheora")) self.failUnlessRaises(KeyError, self.audio.tags.__getitem__, "vendor") def test_not_my_ogg(self): fn = os.path.join('tests', 'data', 'empty.ogg') self.failUnlessRaises(IOError, type(self.audio), fn) self.failUnlessRaises(IOError, self.audio.save, fn) self.failUnlessRaises(IOError, self.audio.delete, fn) def test_length(self): self.failUnlessAlmostEqual(5.5, self.audio.info.length, 1) self.failUnlessAlmostEqual(0.75, self.audio2.info.length, 2) def test_bitrate(self): self.failUnlessEqual(16777215, self.audio3.info.bitrate) def test_module_delete(self): delete(self.filename) self.scan_file() self.failIf(OggTheora(self.filename).tags) def test_mime(self): self.failUnless("video/x-theora" in self.audio.mime) add(TOggTheora) mutagen-1.22/tests/test_easymp4.py0000644000175000017500000001162112177421270017465 0ustar lazkalazka00000000000000import os import shutil from tests import add, TestCase from mutagen.easymp4 import EasyMP4, error as MP4Error from tempfile import mkstemp class TEasyMP4(TestCase): def setUp(self): fd, self.filename = mkstemp('.mp4') os.close(fd) empty = os.path.join('tests', 'data', 'has-tags.m4a') shutil.copy(empty, self.filename) self.mp4 = EasyMP4(self.filename) self.mp4.delete() def test_pprint(self): self.mp4["artist"] = "baz" self.mp4.pprint() def test_has_key(self): self.failIf(self.mp4.has_key("foo")) def test_empty_file(self): empty = os.path.join('tests', 'data', 'emptyfile.mp3') self.assertRaises(MP4Error, EasyMP4, filename=empty) def test_nonexistent_file(self): empty = os.path.join('tests', 'data', 'does', 'not', 'exist') self.assertRaises(IOError, EasyMP4, filename=empty) def test_write_single(self): for key in EasyMP4.Get: if key in ["tracknumber", "discnumber", "date", "bpm"]: continue # Test creation self.mp4[key] = "a test value" self.mp4.save(self.filename) mp4 = EasyMP4(self.filename) self.failUnlessEqual(mp4[key], ["a test value"]) self.failUnlessEqual(mp4.keys(), [key]) # And non-creation setting. self.mp4[key] = "a test value" self.mp4.save(self.filename) mp4 = EasyMP4(self.filename) self.failUnlessEqual(mp4[key], ["a test value"]) self.failUnlessEqual(mp4.keys(), [key]) del(self.mp4[key]) def test_write_double(self): for key in EasyMP4.Get: if key in ["tracknumber", "discnumber", "date", "bpm"]: continue self.mp4[key] = ["a test", "value"] self.mp4.save(self.filename) mp4 = EasyMP4(self.filename) self.failUnlessEqual(mp4.get(key), ["a test", "value"]) self.failUnlessEqual(mp4.keys(), [key]) self.mp4[key] = ["a test", "value"] self.mp4.save(self.filename) mp4 = EasyMP4(self.filename) self.failUnlessEqual(mp4.get(key), ["a test", "value"]) self.failUnlessEqual(mp4.keys(), [key]) del(self.mp4[key]) def test_write_date(self): self.mp4["date"] = "2004" self.mp4.save(self.filename) mp4 = EasyMP4(self.filename) self.failUnlessEqual(mp4["date"], ["2004"]) self.mp4["date"] = "2004" self.mp4.save(self.filename) mp4 = EasyMP4(self.filename) self.failUnlessEqual(mp4["date"], ["2004"]) def test_date_delete(self): self.mp4["date"] = "2004" self.failUnlessEqual(self.mp4["date"], ["2004"]) del(self.mp4["date"]) self.failIf("date" in self.mp4) def test_write_date_double(self): self.mp4["date"] = ["2004", "2005"] self.mp4.save(self.filename) mp4 = EasyMP4(self.filename) self.failUnlessEqual(mp4["date"], ["2004", "2005"]) self.mp4["date"] = ["2004", "2005"] self.mp4.save(self.filename) mp4 = EasyMP4(self.filename) self.failUnlessEqual(mp4["date"], ["2004", "2005"]) def test_write_invalid(self): self.failUnlessRaises(ValueError, self.mp4.__getitem__, "notvalid") self.failUnlessRaises(ValueError, self.mp4.__delitem__, "notvalid") self.failUnlessRaises( ValueError, self.mp4.__setitem__, "notvalid", "tests") def test_numeric(self): for tag in ["bpm"]: self.mp4[tag] = "3" self.failUnlessEqual(self.mp4[tag], ["3"]) self.mp4.save() mp4 = EasyMP4(self.filename) self.failUnlessEqual(mp4[tag], ["3"]) del(mp4[tag]) self.failIf(tag in mp4) self.failUnlessRaises(KeyError, mp4.__delitem__, tag) self.failUnlessRaises( ValueError, self.mp4.__setitem__, tag, "hello") def test_numeric_pairs(self): for tag in ["tracknumber", "discnumber"]: self.mp4[tag] = "3" self.failUnlessEqual(self.mp4[tag], ["3"]) self.mp4.save() mp4 = EasyMP4(self.filename) self.failUnlessEqual(mp4[tag], ["3"]) del(mp4[tag]) self.failIf(tag in mp4) self.failUnlessRaises(KeyError, mp4.__delitem__, tag) self.mp4[tag] = "3/10" self.failUnlessEqual(self.mp4[tag], ["3/10"]) self.mp4.save() mp4 = EasyMP4(self.filename) self.failUnlessEqual(mp4[tag], ["3/10"]) del(mp4[tag]) self.failIf(tag in mp4) self.failUnlessRaises(KeyError, mp4.__delitem__, tag) self.failUnlessRaises( ValueError, self.mp4.__setitem__, tag, "hello") def tearDown(self): os.unlink(self.filename) add(TEasyMP4) mutagen-1.22/tests/test_wavpack.py0000644000175000017500000000142212211074167017533 0ustar lazkalazka00000000000000import os from mutagen.wavpack import WavPack from tests import TestCase, add class TWavPack(TestCase): def setUp(self): self.audio = WavPack(os.path.join("tests", "data", "silence-44-s.wv")) def test_channels(self): self.failUnlessEqual(self.audio.info.channels, 2) def test_sample_rate(self): self.failUnlessEqual(self.audio.info.sample_rate, 44100) def test_length(self): self.failUnlessAlmostEqual(self.audio.info.length, 3.68, 2) def test_not_my_file(self): self.failUnlessRaises( IOError, WavPack, os.path.join("tests", "data", "empty.ogg")) def test_pprint(self): self.audio.pprint() def test_mime(self): self.failUnless("audio/x-wavpack" in self.audio.mime) add(TWavPack) mutagen-1.22/tests/test_oggopus.py0000644000175000017500000000320312157371172017566 0ustar lazkalazka00000000000000import os import shutil from tempfile import mkstemp from cStringIO import StringIO from mutagen.oggopus import OggOpus, OggOpusInfo, delete from mutagen.ogg import OggPage from tests import add from tests.test_ogg import TOggFileType class TOggOpus(TOggFileType): Kind = OggOpus def setUp(self): original = os.path.join("tests", "data", "example.opus") fd, self.filename = mkstemp(suffix='.opus') os.close(fd) shutil.copy(original, self.filename) self.audio = self.Kind(self.filename) def test_length(self): self.failUnlessAlmostEqual(self.audio.info.length, 11.35, 2) def test_misc(self): self.failUnlessEqual(self.audio.info.channels, 1) self.failUnless(self.audio.tags.vendor.startswith("libopus")) def test_module_delete(self): delete(self.filename) self.scan_file() self.failIf(self.Kind(self.filename).tags) def test_mime(self): self.failUnless("audio/ogg" in self.audio.mime) self.failUnless("audio/ogg; codecs=opus" in self.audio.mime) def test_invalid_not_first(self): page = OggPage(open(self.filename, "rb")) page.first = False self.failUnlessRaises(IOError, OggOpusInfo, StringIO(page.write())) def test_unsupported_version(self): page = OggPage(open(self.filename, "rb")) data = list(page.packets[0]) data[8] = "\x03" page.packets[0] = "".join(data) OggOpusInfo(StringIO(page.write())) data[8] = "\x10" page.packets[0] = "".join(data) self.failUnlessRaises(IOError, OggOpusInfo, StringIO(page.write())) add(TOggOpus) mutagen-1.22/man/0000755000175000017500000000000012213137321014072 5ustar lazkalazka00000000000000mutagen-1.22/man/mid3v2.10000644000175000017500000001035512211607066015272 0ustar lazkalazka00000000000000.\" Man page generated from reStructuredText. . .TH MID3V2 1 "October 30th, 2010" "" "" .SH NAME mid3v2 \- audio tag editor similar to 'id3v2' . .nr rst2man-indent-level 0 . .de1 rstReportMargin \\$1 \\n[an-margin] level \\n[rst2man-indent-level] level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] - \\n[rst2man-indent0] \\n[rst2man-indent1] \\n[rst2man-indent2] .. .de1 INDENT .\" .rstReportMargin pre: . RS \\$1 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] . nr rst2man-indent-level +1 .\" .rstReportMargin post: .. .de UNINDENT . RE .\" indent \\n[an-margin] .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] .nr rst2man-indent-level -1 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. .SH SYNOPSIS .sp \fBmid3v2\fP [\fIoptions\fP] \fIfilename\fP ... .SH DESCRIPTION .sp mid3v2 is a Mutagen\-based replacement for id3lib\(aqs \fBid3v2\fP. It supports ID3v2.4 and more frames; it also does not have the numerous bugs that plague \fBid3v2\fP. .sp This program exists mostly for compatibility with programs that want to tag files using \fBid3v2\fP. For a more usable interface, we recommend Ex Falso. .SH OPTIONS .INDENT 0.0 .TP .B \-q, \-\-quiet Be quiet: do not mention file operations that perform the user\(aqs request. Warnings will still be printed. .TP .B \-v, \-\-verbose Be verbose: state all operations performed. This is the opposite of \-\-quiet. This is the default. .TP .B \-e, \-\-escape Enable interpretation of backslash escapes for tag values. Makes it possible to escape the colon\-separator in TXXX, COMM values like \(aq\e:\(aq and insert escape sequences like \(aq\en\(aq, \(aq\et\(aq etc. .TP .B \-f, \-\-list\-frames Display all supported ID3v2.3/2.4 frames and their meanings. .TP .B \-L, \-\-list\-genres List all ID3v1 numeric genres. These can be used to set TCON frames, but it is not recommended. .TP .B \-l, \-\-list List all tags in the files. The output format is \fInot\fP the same as \fBid3v2\fP\(aqs; instead, it is easily parsable and readable. Some tags may not have human\-readable representations. .TP .B \-\-list\-raw List all tags in the files, in raw format. Although this format is nominally human\-readable, it may be very long if the tag contains embedded binary data. .TP .B \-d, \-\-delete\-v2 Delete ID3v2 tags. .TP .B \-s, \-\-delete\-v1 Delete ID3v1 tags. .TP .B \-D, \-\-delete\-all Delete all ID3 tags. .UNINDENT .INDENT 0.0 .TP .B \-\-delete\-frames=FID1,FID2,... Delete specific ID3v2 frames (or groups of frames) from the files. .UNINDENT .INDENT 0.0 .TP .B \-C, \-\-convert Convert ID3v1 tags to ID3v2 tags. This will also happen automatically during any editing. .UNINDENT .INDENT 0.0 .TP .B \-a, \-\-artist=artist Set the artist information (TPE1). .TP .B \-A, \-\-album=album Set the album information (TALB). .TP .B \-t, \-\-song=title Set the title information (TIT2). .TP .B \-c, \-\-comment=DESCRIPTION:COMMENT:LANGUAGE Set a comment (COMM). The language and description may be omitted, in which case the language defaults to English, and the description to an empty string. .TP .B \-g, \-\-genre=genre Set the genre information (TCON). .TP .B \-y, \-\-year=, \-\-date=YYYY\-[MM\-DD] Set the year/date information (TDRC). .TP .B \-Tnum/num, \-\-track=num/num Set the track number (TRCK). .UNINDENT .sp Any text or URL frame (those beginning with T or W) can be modified or added by prefixing the name of the frame with "\-\-". For example, \fB\-\-TIT3 "Monkey!"\fP will set the TIT3 (subtitle) frame to \fBMonkey!\fP. .sp The TXXX frame requires a colon\-separated description key; many TXXX frames may be set in the file as long as they have different keys. To set this key, just separate the text with a colon, e.g. \fB\-\-TXXX "ALBUMARTISTSORT:Examples, The"\fP. .sp The special POPM frame can be set in a similar way: \fB\-\-POPM "bob@example.com:128:2"\fP to set Bob\(aqs rating to 128/255 with 2 plays. .SH BUGS .sp No sanity checking is done on the editing operations you perform, so mid3v2 will happily accept \-\-TSIZ when editing an ID3v2.4 frame. However, it will also automatically throw it out during the next edit operation. .SH AUTHOR .sp Joe Wreschnig is the author of mid3v2, but he doesn\(aqt like to admit it. .\" Generated by docutils manpage writer. . mutagen-1.22/man/mid3iconv.10000644000175000017500000000300712211607066016055 0ustar lazkalazka00000000000000.\" Man page generated from reStructuredText. . .TH MID3ICONV 1 "April 10th, 2006" "" "" .SH NAME mid3iconv \- convert ID3 tag encodings . .nr rst2man-indent-level 0 . .de1 rstReportMargin \\$1 \\n[an-margin] level \\n[rst2man-indent-level] level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] - \\n[rst2man-indent0] \\n[rst2man-indent1] \\n[rst2man-indent2] .. .de1 INDENT .\" .rstReportMargin pre: . RS \\$1 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] . nr rst2man-indent-level +1 .\" .rstReportMargin post: .. .de UNINDENT . RE .\" indent \\n[an-margin] .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] .nr rst2man-indent-level -1 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. .SH SYNOPSIS .sp \fBmid3iconv\fP [\fIoptions\fP] \fIfilename\fP ... .SH DESCRIPTION .sp \fBmid3iconv\fP converts ID3 tags from legacy encodings to Unicode and stores them using the ID3v2 format. .SH OPTIONS .INDENT 0.0 .TP .B \-\-debug, \-d Print updated tags .TP .B \-\-dry\-run, \-p Do not actually modify files .TP .B \-\-encoding, \-e Convert from this encoding. By default, your locale\(aqs default encoding is used. .TP .B \-\-force\-v1 Use an ID3v1 tag even if an ID3v2 tag is present .TP .B \-\-quiet, \-q Only output errors .TP .B \-\-remove\-v1 Remove any ID3v1 tag after processing the files .UNINDENT .SH AUTHOR .sp Emfox Zhou. .sp Based on id3iconv (\fI\%http://www.cs.berkeley.edu/~zf/id3iconv/\fP) by Feng Zhou. .\" Generated by docutils manpage writer. . mutagen-1.22/man/mutagen-pony.10000644000175000017500000000216312211607067016610 0ustar lazkalazka00000000000000.\" Man page generated from reStructuredText. . .TH MUTAGEN-PONY 1 "February 20th, 2006" "" "" .SH NAME mutagen-pony \- scan a collection of MP3 files . .nr rst2man-indent-level 0 . .de1 rstReportMargin \\$1 \\n[an-margin] level \\n[rst2man-indent-level] level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] - \\n[rst2man-indent0] \\n[rst2man-indent1] \\n[rst2man-indent2] .. .de1 INDENT .\" .rstReportMargin pre: . RS \\$1 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] . nr rst2man-indent-level +1 .\" .rstReportMargin post: .. .de UNINDENT . RE .\" indent \\n[an-margin] .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] .nr rst2man-indent-level -1 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. .SH SYNOPSIS .sp \fBmutagen\-pony\fP \fIdirectory\fP ... .SH DESCRIPTION .sp \fBmutagen\-pony\fP scans any directories given and reports on the kinds of tags in the MP3s it finds in them. Ride the pony. .sp It is primarily intended as a debugging tool for Mutagen. .SH AUTHORS .sp Michael Urman and Joe Wreschnig .\" Generated by docutils manpage writer. . mutagen-1.22/man/moggsplit.10000644000175000017500000000324412211607066016172 0ustar lazkalazka00000000000000.\" Man page generated from reStructuredText. . .TH MOGGSPLIT 1 "Nov 14th, 2009" "" "" .SH NAME moggsplit \- split Ogg logical streams . .nr rst2man-indent-level 0 . .de1 rstReportMargin \\$1 \\n[an-margin] level \\n[rst2man-indent-level] level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] - \\n[rst2man-indent0] \\n[rst2man-indent1] \\n[rst2man-indent2] .. .de1 INDENT .\" .rstReportMargin pre: . RS \\$1 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] . nr rst2man-indent-level +1 .\" .rstReportMargin post: .. .de UNINDENT . RE .\" indent \\n[an-margin] .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] .nr rst2man-indent-level -1 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. .SH SYNOPSIS .sp \fBmoggsplit\fP \fIfilename\fP ... .SH DESCRIPTION .sp \fBmoggsplit\fP splits a multiplexed Ogg stream into separate files. For example, it can separate an OGM into separate Ogg DivX and Ogg Vorbis streams, or a chained Ogg Vorbis file into two separate files. .SH OPTIONS .INDENT 0.0 .TP .B \-\-extension Use the supplied extension when generating new files; the default is \fBogg\fP. .TP .B \-\-pattern Use the supplied pattern when generating new files. This is a Python keyword format string with three variables, \fIbase\fP for the original file\(aqs base name, \fIstream\fP for the stream\(aqs serial number, and ext for the extension give by \fB\-\-extension\fP. .sp The default is \fB%(base)s\-%(stream)d.%(ext)s\fP. .TP .B \-\-m3u Generate an m3u playlist along with the newly generated files. Useful for large chained Oggs. .UNINDENT .SH AUTHOR .sp Joe Wreschnig .\" Generated by docutils manpage writer. . mutagen-1.22/man/mutagen-inspect.10000644000175000017500000000217312211607067017271 0ustar lazkalazka00000000000000.\" Man page generated from reStructuredText. . .TH MUTAGEN-INSPECT 1 "May 27th, 2006" "" "" .SH NAME mutagen-inspect \- view Mutagen-supported audio tags . .nr rst2man-indent-level 0 . .de1 rstReportMargin \\$1 \\n[an-margin] level \\n[rst2man-indent-level] level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] - \\n[rst2man-indent0] \\n[rst2man-indent1] \\n[rst2man-indent2] .. .de1 INDENT .\" .rstReportMargin pre: . RS \\$1 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] . nr rst2man-indent-level +1 .\" .rstReportMargin post: .. .de UNINDENT . RE .\" indent \\n[an-margin] .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] .nr rst2man-indent-level -1 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. .SH SYNOPSIS .sp \fBmutagen\-inspect\fP \fIfilename\fP ... .SH DESCRIPTION .sp \fBmutagen\-inspect\fP loads and prints information about an audio file and its tags. .sp It is primarily intended as a debugging tool for Mutagen, but can be useful for extracting tags from the command line. .SH AUTHOR .sp Joe Wreschnig .\" Generated by docutils manpage writer. . mutagen-1.22/TODO0000644000175000017500000000111112211610740014000 0ustar lazkalazka00000000000000mutagen.id3 needs: * Detect tags in non-beginning locations, particularly if multiple separate version tags exist at the beginning of the file * Perhaps implement tag merging - "intelligently" merge information from all known tag formats * Some test cleanup mutagen.apev2 needs: * General cleanup to fit the basic structure of the rest of Mutagen. Profile Hotspots: * __determine_bpi takes 84% of TMP3,TEasyID3; BitPaddedInt takes 46%. * OggPage.replace takes 43% of Ogg tests; OggPage.write takes 25%. * insert_bytes/delete_bytes is still a hotspot but is much faster. mutagen-1.22/setup.py0000755000175000017500000001623712212316122015042 0ustar lazkalazka00000000000000#!/usr/bin/env python # Copyright 2005-2009,2011 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. import glob import os import shutil import sys import subprocess from distutils.core import setup, Command from distutils.command.clean import clean as distutils_clean from distutils.command.sdist import sdist as distutils_sdist class clean(distutils_clean): def run(self): # In addition to what the normal clean run does, remove pyc # and pyo and backup files from the source tree. distutils_clean.run(self) def should_remove(filename): if (filename.lower()[-4:] in [".pyc", ".pyo"] or filename.endswith("~") or (filename.startswith("#") and filename.endswith("#"))): return True else: return False for pathname, dirs, files in os.walk(os.path.dirname(__file__)): for filename in filter(should_remove, files): try: os.unlink(os.path.join(pathname, filename)) except EnvironmentError, err: print str(err) try: os.unlink("MANIFEST") except OSError: pass for base in ["coverage", "build", "dist"]: path = os.path.join(os.path.dirname(__file__), base) if os.path.isdir(path): shutil.rmtree(path) class sdist(distutils_sdist): def run(self): self.run_command("test") distutils_sdist.run(self) # make sure MANIFEST.in includes all tracked files if subprocess.call(["hg", "status"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0: # contains the packaged files after run() is finished included_files = self.filelist.files process = subprocess.Popen(["hg", "locate"], stdout=subprocess.PIPE) out, err = process.communicate() assert process.returncode == 0 tracked_files = out.splitlines() for ignore in [".hgignore", ".hgtags"]: tracked_files.remove(ignore) assert not set(tracked_files) - set(included_files), \ "Not all tracked files included in tarball, update MANIFEST.in" class build_sphinx(Command): description = "build sphinx documentation" user_options = [ ("build-dir=", "d", "build directory"), ] def initialize_options(self): self.build_dir = None def finalize_options(self): self.build_dir = self.build_dir or "build" def run(self): docs = "docs" target = os.path.join(self.build_dir, "sphinx") self.spawn(["sphinx-build", "-b", "html", "-n", docs, target]) class test_cmd(Command): description = "run automated tests" user_options = [ ("to-run=", None, "list of tests to run (default all)"), ("quick", None, "don't run slow mmap-failing tests"), ] def initialize_options(self): self.to_run = [] self.quick = False def finalize_options(self): if self.to_run: self.to_run = self.to_run.split(",") def run(self): import tests if tests.unit(self.to_run, self.quick): raise SystemExit("Test failures are listed above.") class coverage_cmd(Command): description = "generate test coverage data" user_options = [ ("quick", None, "don't run slow mmap-failing tests"), ] def initialize_options(self): self.quick = None def finalize_options(self): self.quick = bool(self.quick) def run(self): import trace tracer = trace.Trace( count=True, trace=False, ignoredirs=[sys.prefix, sys.exec_prefix]) def run_tests(): import mutagen import mutagen._util reload(mutagen._util) reload(mutagen) cmd = self.reinitialize_command("test") cmd.quick = self.quick cmd.ensure_finalized() cmd.run() tracer.runfunc(run_tests) results = tracer.results() coverage = os.path.join(os.path.dirname(__file__), "coverage") results.write_results(show_missing=True, coverdir=coverage) map(os.unlink, glob.glob(os.path.join(coverage, "[!m]*.cover"))) try: os.unlink(os.path.join(coverage, "..setup.cover")) except OSError: pass total_lines = 0 bad_lines = 0 for filename in glob.glob(os.path.join(coverage, "*.cover")): lines = file(filename, "rU").readlines() total_lines += len(lines) bad_lines += len( [line for line in lines if (line.startswith(">>>>>>") and "finally:" not in line and '"""' not in line)]) pct = 100.0 * (total_lines - bad_lines) / float(total_lines) print "Coverage data written to", coverage, "(%d/%d, %0.2f%%)" % ( total_lines - bad_lines, total_lines, pct) if pct < 98.66: raise SystemExit( "Coverage percentage went down; write more tests.") if pct > 98.7: raise SystemExit("Coverage percentage went up; change setup.py.") if os.name == "posix": data_files = [('share/man/man1', glob.glob("man/*.1"))] else: data_files = [] if __name__ == "__main__": from mutagen import version_string cmd_classes = { "clean": clean, "test": test_cmd, "coverage": coverage_cmd, "sdist": sdist, "build_sphinx": build_sphinx, } setup(cmdclass=cmd_classes, name="mutagen", version=version_string, url="http://code.google.com/p/mutagen/", description="read and write audio tags for many formats", author="Michael Urman", author_email="quod-libet-development@groups.google.com", license="GNU GPL v2", classifiers=[ 'Operating System :: OS Independent', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', 'Topic :: Multimedia :: Sound/Audio', ], packages=["mutagen"], data_files=data_files, scripts=glob.glob("tools/m*[!~]"), long_description="""\ Mutagen is a Python module to handle audio metadata. It supports ASF, FLAC, M4A, Monkey's Audio, MP3, Musepack, Ogg FLAC, Ogg Speex, Ogg Theora, Ogg Vorbis, True Audio, WavPack and OptimFROG audio files. All versions of ID3v2 are supported, and all standard ID3v2.4 frames are parsed. It can read Xing headers to accurately calculate the bitrate and length of MP3s. ID3 and APEv2 tags can be edited regardless of audio format. It can also manipulate Ogg streams on an individual packet/page level. """ ) mutagen-1.22/README0000644000175000017500000000265112211574315014211 0ustar lazkalazka00000000000000Mutagen ======= Mutagen is a Python module to handle audio metadata. It supports ASF, FLAC, M4A, Monkey's Audio, MP3, Musepack, Ogg Opus, Ogg FLAC, Ogg Speex, Ogg Theora, Ogg Vorbis, True Audio, WavPack and OptimFROG audio files. All versions of ID3v2 are supported, and all standard ID3v2.4 frames are parsed. It can read Xing headers to accurately calculate the bitrate and length of MP3s. ID3 and APEv2 tags can be edited regardless of audio format. It can also manipulate Ogg streams on an individual packet/page level. Mutagen works on Python 2.6+ / PyPy and has no dependencies outside the CPython standard library. Installing ---------- $ ./setup.py build $ su -c "./setup.py install" Documentation ------------- The primary documentation for Mutagen is the doc strings found in the source code and the sphinx documentation in the docs/ directory. To build the docs (needs sphinx): $ ./setup.py build_sphinx The tools/ directory contains several useful examples. The docs are also hosted on readthedocs.org: http://mutagen.readthedocs.org Testing the Module ------------------ To test Mutagen's MP3 reading support, run $ tools/mutagen-pony Mutagen will try to load all of them, and report any errors. To look at the tags in files, run $ tools/mutagen-inspect filename ... To run our test suite, $ ./setup.py test Compatibility/Bugs ------------------ See docs/bugs.rst mutagen-1.22/PKG-INFO0000644000175000017500000000234012213137321014413 0ustar lazkalazka00000000000000Metadata-Version: 1.1 Name: mutagen Version: 1.22 Summary: read and write audio tags for many formats Home-page: http://code.google.com/p/mutagen/ Author: Michael Urman Author-email: quod-libet-development@groups.google.com License: GNU GPL v2 Description: Mutagen is a Python module to handle audio metadata. It supports ASF, FLAC, M4A, Monkey's Audio, MP3, Musepack, Ogg FLAC, Ogg Speex, Ogg Theora, Ogg Vorbis, True Audio, WavPack and OptimFROG audio files. All versions of ID3v2 are supported, and all standard ID3v2.4 frames are parsed. It can read Xing headers to accurately calculate the bitrate and length of MP3s. ID3 and APEv2 tags can be edited regardless of audio format. It can also manipulate Ogg streams on an individual packet/page level. Platform: UNKNOWN Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2) Classifier: Topic :: Multimedia :: Sound/Audio mutagen-1.22/MANIFEST.in0000644000175000017500000000041012211407012015043 0ustar lazkalazka00000000000000include COPYING include NEWS include README include TODO include MANIFEST.in include tests/data/* include tests/*.py include man/*.1 include docs/Makefile include docs/*.py include docs/*.rst include docs/api/*.rst include docs/man/*.rst include docs/man/Makefile mutagen-1.22/tools/0000755000175000017500000000000012213137321014457 5ustar lazkalazka00000000000000mutagen-1.22/tools/moggsplit0000755000175000017500000000425112211141755016420 0ustar lazkalazka00000000000000#!/usr/bin/env python # Split a multiplex/chained Ogg file into its component parts. # Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. import os import sys from optparse import OptionParser import mutagen.ogg def main(argv): from mutagen.ogg import OggPage parser = OptionParser( usage="%prog [options] filename.ogg ...", description="Split Ogg logical streams using Mutagen.", version="Mutagen %s" % ".".join(map(str, mutagen.version)) ) parser.add_option( "--extension", dest="extension", default="ogg", metavar='ext', help="use this extension (default 'ogg')") parser.add_option( "--pattern", dest="pattern", default="%(base)s-%(stream)d.%(ext)s", metavar='pattern', help="name files using this pattern") parser.add_option( "--m3u", dest="m3u", action="store_true", default=False, help="generate an m3u (playlist) file") (options, args) = parser.parse_args(argv[1:]) if not args: raise SystemExit(parser.print_help() or 1) format = {'ext': options.extension} for filename in args: fileobjs = {} format["base"] = os.path.splitext(os.path.basename(filename))[0] fileobj = open(filename, "rb") if options.m3u: m3u = open(format["base"] + ".m3u", "w") fileobjs["m3u"] = m3u else: m3u = None while True: try: page = OggPage(fileobj) except EOFError: break else: format["stream"] = page.serial if page.serial not in fileobjs: new_filename = options.pattern % format new_fileobj = open(new_filename, "wb") fileobjs[page.serial] = new_fileobj if m3u: m3u.write(new_filename + "\r\n") fileobjs[page.serial].write(page.write()) map(file.close, fileobjs.values()) if __name__ == "__main__": main(sys.argv) mutagen-1.22/tools/mutagen-inspect0000755000175000017500000000222612211142003017501 0ustar lazkalazka00000000000000#!/usr/bin/env python # Full tag list for any given file. # Copyright 2005 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. import sys import locale from optparse import OptionParser def main(argv): from mutagen import File parser = OptionParser() parser.add_option("--no-flac", help="Compatibility; does nothing.") parser.add_option("--no-mp3", help="Compatibility; does nothing.") parser.add_option("--no-apev2", help="Compatibility; does nothing.") (options, args) = parser.parse_args(argv[1:]) if not args: raise SystemExit(parser.print_help() or 1) enc = locale.getpreferredencoding() for filename in args: print "--", filename try: print "- " + File(filename).pprint().encode(enc, 'replace') except AttributeError: print "- Unknown file type" except KeyboardInterrupt: raise except Exception, err: print str(err) print if __name__ == "__main__": main(sys.argv) mutagen-1.22/tools/mid3iconv0000755000175000017500000001103512211661006016300 0ustar lazkalazka00000000000000#!/usr/bin/env python # ID3iconv is a Java based ID3 encoding convertor, here's the Python version. # Copyright 2006 Emfox Zhou # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. import sys import locale from optparse import OptionParser import mutagen import mutagen.id3 VERSION = (0, 3) def isascii(string): return not string or ord(max(string)) < 128 class ID3OptionParser(OptionParser): def __init__(self): mutagen_version = ".".join(map(str, mutagen.version)) my_version = ".".join(map(str, VERSION)) version = "mid3iconv %s\nUses Mutagen %s" % ( my_version, mutagen_version) return OptionParser.__init__( self, version=version, usage="%prog [OPTION] [FILE]...", description=("Mutagen-based replacement the id3iconv utility, " "which converts ID3 tags from legacy encodings " "to Unicode and stores them using the ID3v2 format.")) def format_help(self, *args, **kwargs): text = OptionParser.format_help(self, *args, **kwargs) return text + "\nFiles are updated in-place, so use --dry-run first.\n" def update(options, filenames): encoding = options.encoding or locale.getpreferredencoding() verbose = options.verbose noupdate = options.noupdate force_v1 = options.force_v1 remove_v1 = options.remove_v1 def conv(uni): return uni.encode('iso-8859-1').decode(encoding) for filename in filenames: if verbose != "quiet": print "Updating", filename if has_id3v1(filename) and not noupdate and force_v1: mutagen.id3.delete(filename, False, True) try: id3 = mutagen.id3.ID3(filename) except mutagen.id3.ID3NoHeaderError: if verbose != "quiet": print "No ID3 header found; skipping..." continue except Exception, err: print >>sys.stderr, str(err) continue for tag in filter(lambda t: t.startswith(("T", "COMM")), id3): frame = id3[tag] if isinstance(frame, mutagen.id3.TimeStampTextFrame): # non-unicode fields continue try: text = frame.text except AttributeError: continue try: text = map(conv, frame.text) except (UnicodeError, LookupError): continue else: frame.text = text if not text or min(map(isascii, text)): frame.encoding = 3 else: frame.encoding = 1 enc = locale.getpreferredencoding() if verbose == "debug": print id3.pprint().encode(enc, "replace") if not noupdate: if remove_v1: id3.save(filename, v1=False) else: id3.save(filename) def has_id3v1(filename): try: f = open(filename, 'rb+') f.seek(-128, 2) return f.read(3) == "TAG" except IOError: return False def main(argv): parser = ID3OptionParser() parser.add_option( "-e", "--encoding", metavar="ENCODING", action="store", type="string", dest="encoding", help=("Specify original tag encoding (default is %s)" % ( locale.getpreferredencoding()))) parser.add_option( "-p", "--dry-run", action="store_true", dest="noupdate", help="Do not actually modify files") parser.add_option( "--force-v1", action="store_true", dest="force_v1", help="Use an ID3v1 tag even if an ID3v2 tag is present") parser.add_option( "--remove-v1", action="store_true", dest="remove_v1", help="Remove v1 tag after processing the files") parser.add_option( "-q", "--quiet", action="store_const", dest="verbose", const="quiet", help="Only output errors") parser.add_option( "-d", "--debug", action="store_const", dest="verbose", const="debug", help="Output updated tags") for i, arg in enumerate(sys.argv): if arg == "-v1": sys.argv[i] = "--force-v1" elif arg == "-removev1": sys.argv[i] = "--remove-v1" (options, args) = parser.parse_args(argv[1:]) if args: update(options, args) else: parser.print_help() if __name__ == "__main__": main(sys.argv) mutagen-1.22/tools/mutagen-pony0000755000175000017500000000635512212167050017042 0ustar lazkalazka00000000000000#!/usr/bin/env python # Copyright 2005 Joe Wreschnig, Michael Urman # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. import os import sys import traceback class Report(object): def __init__(self, pathname): self.name = pathname self.files = 0 self.unsync = 0 self.missings = 0 self.errors = [] self.exceptions = {} self.versions = {} def missing(self, filename): self.missings += 1 self.files += 1 def error(self, filename): Ex, value, trace = sys.exc_info() self.exceptions.setdefault(Ex, 0) self.exceptions[Ex] += 1 self.errors.append((filename, Ex, value, trace)) self.files += 1 def success(self, id3): self.versions.setdefault(id3.version, 0) self.versions[id3.version] += 1 self.files += 1 if id3.f_unsynch: self.unsync += 1 def __str__(self): strings = ["-- Report for %s --" % self.name] if self.files == 0: return strings[0] + "\n" + "No MP3 files found.\n" good = self.files - len(self.errors) strings.append("Loaded %d/%d files (%d%%)" % ( good, self.files, (float(good)/self.files) * 100)) strings.append("%d files with unsynchronized frames." % self.unsync) strings.append("%d files without tags." % self.missings) strings.append("\nID3 Versions:") items = self.versions.items() items.sort() for v, i in items: strings.append(" %s\t%d" % (".".join(map(str, v)), i)) if self.exceptions: strings.append("\nExceptions:") items = self.exceptions.items() items.sort() for Ex, i in items: strings.append(" %-20s\t%d" % (Ex.__name__, i)) if self.errors: strings.append("\nERRORS:\n") for filename, Ex, value, trace in self.errors: strings.append("\nReading %s:" % filename) strings.append( "".join(traceback.format_exception(Ex, value, trace)[1:])) else: strings.append("\nNo errors!") return "\n".join(strings) def check_dir(path): from mutagen.id3 import ID3 from mutagen.mp3 import MP3 class ID3Custom(ID3): PEDANTIC = False rep = Report(path) print "Scanning", path for path, dirs, files in os.walk(path): files.sort() for fn in files: if not fn.lower().endswith('.mp3'): continue ffn = os.path.join(path, fn) try: mp3 = MP3(ffn, ID3=ID3Custom) except KeyboardInterrupt: raise except Exception: rep.error(ffn) else: if mp3.tags is None: rep.missing(ffn) else: rep.success(mp3.tags) print str(rep) def main(argv): if len(argv) == 1: print "Usage: %s directory ..." % argv[0] else: for path in argv[1:]: check_dir(path) if __name__ == "__main__": main(sys.argv) mutagen-1.22/tools/mid3v20000755000175000017500000003106212212144767015526 0ustar lazkalazka00000000000000#!/usr/bin/env python # Pretend to be /usr/bin/id3v2 from id3lib, sort of. # Copyright 2005 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. import sys import locale from optparse import OptionParser, SUPPRESS_HELP import mutagen import mutagen.id3 VERSION = (1, 3) global verbose verbose = True class ID3OptionParser(OptionParser): def __init__(self): mutagen_version = ".".join(map(str, mutagen.version)) my_version = ".".join(map(str, VERSION)) version = "mid3v2 %s\nUses Mutagen %s" % (my_version, mutagen_version) self.edits = [] OptionParser.__init__( self, version=version, usage="%prog [OPTION] [FILE]...", description="Mutagen-based replacement for id3lib's id3v2.") def format_help(self, *args, **kwargs): text = OptionParser.format_help(self, *args, **kwargs) return text + """\ You can set the value for any ID3v2 frame by using '--' and then a frame ID. For example: mid3v2 --TIT3 "Monkey!" file.mp3 would set the "Subtitle/Description" frame to "Monkey!". Any editing operation will cause the ID3 tag to be upgraded to ID3v2.4. """ def split_escape(string, sep, maxsplit=None, escape_char=u"\\"): """Like unicode.split but allows for the separator to be escaped""" assert len(sep) == 1 assert len(escape_char) == 1 if maxsplit is None: maxsplit = len(string) result = [] current = u"" escaped = False for char in string: if escaped: if char != escape_char and char != sep: current += escape_char current += char escaped = False else: if char == escape_char: escaped = True elif char == sep and len(result) < maxsplit: result.append(current) current = u"" else: current += char result.append(current) return result def unescape_string(string): assert isinstance(string, str) return string.decode("string_escape") def list_frames(option, opt, value, parser): items = mutagen.id3.Frames.items() items.sort() for name, frame in items: print " --%s %s" % (name, frame.__doc__.split("\n")[0]) raise SystemExit def list_frames_2_2(option, opt, value, parser): items = mutagen.id3.Frames_2_2.items() items.sort() for name, frame in items: print " --%s %s" % (name, frame.__doc__.split("\n")[0]) raise SystemExit def list_genres(option, opt, value, parser): for i, genre in enumerate(mutagen.id3.TCON.GENRES): print "%3d: %s" % (i, genre) raise SystemExit def delete_tags(filenames, v1, v2): for filename in filenames: if verbose: print "deleting ID3 tag info in %s" % filename mutagen.id3.delete(filename, v1, v2) def delete_frames(deletes, filenames): frames = deletes.split(",") for filename in filenames: if verbose: print "deleting %s from %s" % (deletes, filename) try: id3 = mutagen.id3.ID3(filename) except mutagen.id3.ID3NoHeaderError: if verbose: print "No ID3 header found; skipping." except StandardError, err: print str(err) else: map(id3.delall, frames) id3.save() def write_files(edits, filenames, escape): enc = locale.getpreferredencoding() # unescape escape sequences and decode values encoded_edits = [] for frame, value in edits: if not value: continue frame = frame[2:] if escape: try: value = unescape_string(value) except ValueError as err: print "%s: %s" % (frame, str(err)) raise SystemExit(1) try: value = value.decode(enc) except UnicodeDecodeError as err: print "%s: %s" % (frame, str(err)) raise SystemExit(1) encoded_edits.append((frame, value)) edits = encoded_edits # preprocess: # for all [frame,value] pairs in the edits list # gather values for identical frames into a list tmp = {} for frame, value in edits: if frame in tmp: tmp[frame].append(value) else: tmp[frame] = [value] # edits is now a dictionary of frame -> [list of values] edits = tmp # escape also enables escaping of the split separator if escape: string_split = split_escape else: string_split = lambda s, *args, **kwargs: s.split(*args, **kwargs) for filename in filenames: if verbose: print "Writing", filename try: id3 = mutagen.id3.ID3(filename) except mutagen.id3.ID3NoHeaderError: if verbose: print "No ID3 header found; creating a new tag" id3 = mutagen.id3.ID3() except StandardError, err: print str(err) continue for (frame, vlist) in edits.items(): if frame == "POPM": for value in vlist: values = string_split(value, ":") if len(values) == 1: email, rating, count = values[0], 0, 0 elif len(values) == 2: email, rating, count = values[0], values[1], 0 else: email, rating, count = values frame = mutagen.id3.POPM( email=email, rating=int(rating), count=int(count)) id3.add(frame) elif frame == "COMM": for value in vlist: values = string_split(value, ":") if len(values) == 1: value, desc, lang = values[0], "", "eng" elif len(values) == 2: desc, value, lang = values[0], values[1], "eng" else: value = ":".join(values[1:-1]) desc, lang = values[0], values[-1] frame = mutagen.id3.COMM( encoding=3, text=value, lang=lang, desc=desc) id3.add(frame) elif frame == "TXXX": for value in vlist: values = string_split(value, ":", 1) if len(values) == 1: desc, value = "", values[0] else: desc, value = values[0], values[1] frame = mutagen.id3.TXXX(encoding=3, text=value, desc=desc) id3.add(frame) elif issubclass(mutagen.id3.Frames[frame], mutagen.id3.UrlFrame): frame = mutagen.id3.Frames[frame](encoding=3, url=vlist) id3.add(frame) else: frame = mutagen.id3.Frames[frame](encoding=3, text=vlist) id3.add(frame) id3.save(filename) def list_tags(filenames): enc = locale.getpreferredencoding() for filename in filenames: print "IDv2 tag info for %s:" % filename try: id3 = mutagen.id3.ID3(filename, translate=False) except StandardError, err: print str(err) else: print id3.pprint().encode(enc, "replace") def list_tags_raw(filenames): for filename in filenames: print "Raw IDv2 tag info for %s:" % filename try: id3 = mutagen.id3.ID3(filename, translate=False) except StandardError, err: print str(err) else: for frame in id3.values(): print repr(frame) def main(argv): parser = ID3OptionParser() parser.add_option( "-v", "--verbose", action="store_true", dest="verbose", default=False, help="be verbose") parser.add_option( "-q", "--quiet", action="store_false", dest="verbose", help="be quiet (the default)") parser.add_option( "-e", "--escape", action="store_true", default=False, help="enable interpretation of backslash escapes") parser.add_option( "-f", "--list-frames", action="callback", callback=list_frames, help="Display all possible frames for ID3v2.3 / ID3v2.4") parser.add_option( "--list-frames-v2.2", action="callback", callback=list_frames_2_2, help="Display all possible frames for ID3v2.2") parser.add_option( "-L", "--list-genres", action="callback", callback=list_genres, help="Lists all ID3v1 genres") parser.add_option( "-l", "--list", action="store_const", dest="action", const="list", help="Lists the tag(s) on the open(s)") parser.add_option( "--list-raw", action="store_const", dest="action", const="list-raw", help="Lists the tag(s) on the open(s) in Python format") parser.add_option( "-d", "--delete-v2", action="store_const", dest="action", const="delete-v2", help="Deletes ID3v2 tags") parser.add_option( "-s", "--delete-v1", action="store_const", dest="action", const="delete-v1", help="Deletes ID3v1 tags") parser.add_option( "-D", "--delete-all", action="store_const", dest="action", const="delete-v1-v2", help="Deletes ID3v1 and ID3v2 tags") parser.add_option( '--delete-frames', metavar='FID1,FID2,...', action='store', dest='deletes', default='', help="Delete the given frames") parser.add_option( "-C", "--convert", action="store_const", dest="action", const="convert", help="Convert tags to ID3v2.4 (any editing will do this)") parser.add_option( "-a", "--artist", metavar='"ARTIST"', action="callback", help="Set the artist information", type="string", callback=lambda *args: args[3].edits.append(("--TPE1", args[2]))) parser.add_option( "-A", "--album", metavar='"ALBUM"', action="callback", help="Set the album title information", type="string", callback=lambda *args: args[3].edits.append(("--TALB", args[2]))) parser.add_option( "-t", "--song", metavar='"SONG"', action="callback", help="Set the song title information", type="string", callback=lambda *args: args[3].edits.append(("--TIT2", args[2]))) parser.add_option( "-c", "--comment", metavar='"DESCRIPTION":"COMMENT":"LANGUAGE"', action="callback", help="Set the comment information", type="string", callback=lambda *args: args[3].edits.append(("--COMM", args[2]))) parser.add_option( "-g", "--genre", metavar='"GENRE"', action="callback", help="Set the genre or genre number", type="string", callback=lambda *args: args[3].edits.append(("--TCON", args[2]))) parser.add_option( "-y", "--year", "--date", metavar='YYYY[-MM-DD]', action="callback", help="Set the year/date", type="string", callback=lambda *args: args[3].edits.append(("--TDRC", args[2]))) parser.add_option( "-T", "--track", metavar='"num/num"', action="callback", help="Set the track number/(optional) total tracks", type="string", callback=lambda *args: args[3].edits.append(("--TRCK", args[2]))) for frame in mutagen.id3.Frames: if (issubclass(mutagen.id3.Frames[frame], mutagen.id3.TextFrame) or issubclass(mutagen.id3.Frames[frame], mutagen.id3.UrlFrame) or issubclass(mutagen.id3.Frames[frame], mutagen.id3.POPM)): parser.add_option( "--" + frame, action="callback", help=SUPPRESS_HELP, type='string', metavar="value", # optparse blows up with this callback=lambda *args: args[3].edits.append(args[1:3])) (options, args) = parser.parse_args(argv[1:]) global verbose verbose = options.verbose if args: if parser.edits or options.deletes: if options.deletes: delete_frames(options.deletes, args) if parser.edits: write_files(parser.edits, args, options.escape) elif options.action in [None, 'list']: list_tags(args) elif options.action == "list-raw": list_tags_raw(args) elif options.action == "convert": write_files([], args, options.escape) elif options.action.startswith("delete"): delete_tags(args, "v1" in options.action, "v2" in options.action) else: parser.print_help() else: parser.print_help() if __name__ == "__main__": main(sys.argv) mutagen-1.22/NEWS0000644000175000017500000003214712213134350014024 0ustar lazkalazka000000000000001.22 - 2013.09.08 * Minimum required Python version is now 2.6 * Online API reference at https://mutagen.readthedocs.org/ * EasyID3: * Fix crash with empty TXXX values. (#135) * ID3: * id3v2.3 writing support (#85) * Add iTunes podcast frames (TGID, TDES, WFED) (#141) * Updated id3v1 genre list * MP4: * add_tags() will not replace existing tags. (#101) * Don't ignore tags if parsing unknown atoms fails. * Raise on invalid 64bit atom size (#132, Sidnei da Silva) * APEv2: * Handle invalid tag item count. (#145, Dawid Zamirski) * Ogg: * Faster parsing of files with large packets. * VComment: * Preserve text case for field names added through the dict interface (#152) * mid3v2: * New -e,--escape switch to enable interpretation of escape sequences and makes escaping of the colon separator possible. (#159) * mid3iconv: * Convert COMM frames (#128) 1.21 - 2013.01.30 * Fix Python 2.3 compatibility (broken in 1.19). * Fix many warnings triggered by -3. (#27) * mid3v2: * Add --TXXX support. (#62, Tim Phipps) * Add --POPM support. (#71) * Allow setting multiple COMM or TXXX frames with one command line. * FLAC: * Try to handle corrupt Vorbis comment block sizes. (#52) * Try to handle corrupt Picture block sizes (#106, Christoph Reiter) * Don't leak file handle with PyPy (#111, Marien Zwart) * ID3: * MakeID3v1: Do not generate bad tags when given short dates. (#69) * ParseID3v1: Parse short (< 128 byte) tags generated by old Mutagen implementations of MakeID3v1, and tags with garbage on the front. * pprint: Sort frames by name. * Upgrade unknown 2.3 frames (#97, Christoph Reiter) * Fix handling of invalid SYLT frames (#105, Christoph Reiter) * MP3: * Fix error when loading extremely small MP3s. (#72) * Fix rounding error in CBR length calculation (#93, Christoph Reiter) * Use 'open' rather than 'file' everywhere. (#74, Dan Callahan) * mid3iconv: * Accurately copy QL-style frame encoding behavior. (#75) * Skip unopenable files. (#79) * ID3FileType: * Remember which tag type load() was called with even if the file doesn't yet have any ID3 tags. (#89) * VComment: * Prevent MemoryError when parsing invalid header (#112, Jyrki Pulliainen) * ASF: * Don't corrupt files on the second save() call (#81, Christoph Reiter) * Always store GUID objects in the MetadataLibraryBlock (#81) * OggTheora: Fix length/bitrate calculation. (#99, Christoph Reiter) * MP4: * Less strict MP4 covr atom parsing. (#86, Lukáš Lalinský) * Support atoms that extend to the end of the file. (#109, Sidnei da Silva) * Preserve freeform format flags (#103, Christoph Reiter) * OggOpus support. (#115, Christoph Reiter) * Musepack: * Fix SV7 bitrate calculation (#7, Christoph Reiter) * Support SV8 (#7, Christoph Reiter) 1.20 - 2010.08.04 * ASF: Don't store blocks over 64K in the MetadataObject block; use the MetadataLibraryBlock instead. (#60, Lukáš Lalinský) * ID3: Faster parsing of files with lots of padding. (#65, Christoph Reiter) * FLAC: Correct check for audio data start. (#67) 1.19 - 2010.02.18 * ID3: * POPM: 'count' is optional; the attribute may not exist. (#33) * TimeStampTextFrame: Fix a TypeError in unicode comparisons. (#43) * MakeID3v1: Translate TYER into ID3v1 year if TDRC is not present. (#42) * mid3v2: * Allow --delete followed by --frame, and --genre 1 --genre 2. (#37) * Add --quiet and --verbose flags. (#40) * moggsplit: --m3u option to write an M3U playlist of the new files. (#39) * mid3iconv: Fix crash when processing TCML or TIPL frames. (#41) * VCommentDict: Correctly normalize key names for .keys() iterator. (#45) * MP3: Correct length calculation for MPEG-2 files. (#46) * oggflac: Fix typo in docstring. (#53) * EasyID3: Force UTF-8 encoding. (#54) * EasyMP4: Fix 'genre' translation. (#56) 1.18 - 2009.10.22 * ASF: * Distinguish between empty and absent tag values in ContentDescriptionObjects. (#29) * mid3iconv: * Fix a crash when processing empty (invalid) text frames. * MAJOR API INCOMPATIBILITY!!!! * EasyID3FileType is now in mutagen.easyid3, not mutagen.id3. This change was necessary to restore API compatibility with 1.16, as 1.17 accidentally contained a circular import preventing mutagen.easyid3 from importing by itself. (#32) 1.17 - 2009.10.07 * ID3: * Support for the iTunes non-standard TSO2 and TSOC frames. * Attempt to recover from bad SYLT frames. (#2) * Attempt to recover from faulty extended header flags. (#4, #21) * Fix a bug in ID3v2.4 footer flag detection, (#5) * MP4: * Don't fail or double-encode UTF-8 strings when given a str. * Don't corrupt 64 bit atom sizes when resizing atoms. (#17) * EasyID3: * Extension API for defining new "easy" tags at runtime. * Support for many, many more tags. * OggVorbis, OggSpeex: Handle bitrates below 0 as per the spec. (#30) * EasyMP4: Like EasyID3, but for iTunes MPEG-4 files. * mutagen.File: New 'easy=True' argument to create new EasyMP3, EasyMP4, EasyTrueAudio, and EasyID3FileType instances. 1.16 - 2009.06.15 * Website / code repository move. * Bug Fixes: * EasyID3: Invalid keys now raise KeyError (and ValueError). * mutagen.File: .flac files with an ID3 tag will be opened as FLAC. * MAJOR API INCOMPATIBILITY!!!! * Python 2.6 has required us to rename the .format attribute of M4A/MP4 cover atoms, because it conflicts with the new str.format method. It has been renamed .imageformat. 1.15 - 2008.12.01 * Bug Fixes: * mutagen.File: Import order no longer affects what type is returned. * mutagen.id3: Compression of frames is now disabled. * mutagen.flac.StreamInfo: Fix channel mask (support channels > 2). [35] * mutagen.mp3: Ignore Xing headers if they are obviously wrong. 1.14 - 2008.05.31 * Bug Fixes: * MP4/M4A: Fixed saving of atoms with 64-bit size on 64-bit platforms. * MP4: Conversion of 'gnre' atoms to '\xa9gen' text atoms now correctly produces a list of string values, not just a single value. * ID3: Broken RVA2 frames are now discarded. (Vladislav Naumov) * ID3: Use long integers when appropriate. * VCommentDict: Raise UnicodeEncodeErrors when trying to use a Unicode key that is not valid ASCII; keys are also normalized to ASCII str objects. (Forest Bond) * Tests: * FLAC: Use 2**64 instead of 2**32 to test overflow behavior. 1.13 - 2007.12.03 * Bug Fixes: * FLAC: Raise IOError, instead of UnboundLocalError, when trying to open a non-existant file. (Lukáš Lalinský, Debian #448734) * Throw out invalid frames when upgrading from 2.3 to 2.4. * Fixed reading of Unicode strings from ASF files on big-endian platforms. * TCP/TCMP support. (Debian #452231) * Faster implementation of file-writing when mmap fails, and exclusive advisory locking when available. * Test cases to ensure Mutagen is not vulnerable to CVE-2007-4619. It is not now, nor was it ever. * Use VBRI header to calculate length of VBR MP3 files if the Xing header is not found. 1.12 - 2007.08.04 * Write important ID3v2 frames near the start. (Lukáš Lalinský) * Clean up distutils functions. 1.11 - 2007.04.26 * New Features: * mid3v2 can now set URL frames. (Vladislav Naumov) * Musepack: Skip ID3v2 tags. (Lukáš Lalinský) * Bug Fixes: * mid3iconv: Skip all timestamp frames. (Lukáš Lalinský) * WavPack: More accurate length calculation. ('ak') * PairedTextFrame: Fix typo in documentation. (Lukáš Lalinský) * ID3: Fixed incorrect TDAT conversion. The format is DDMM, not MMDD. (Lukáš Lalinský) * API: * Metadata no longer inherits from dict. * Relatedly, the MRO has changed on several types. * More documentation for MP4 atoms. (Lukáš Lalinský) * Prefer MP3 for files with unknown extensions and ID3 tags. 1.10.1 - 2007.01.23 * Bug Fixes: * Documentation mentions ASF support. * APEv2 flags and valid keys are fixed. * Tests pass on Python 2.3 again. 1.10 - 2007.01.21 * New Features: * FLAC: Skip ID3 tags. Added option to delete them on save. * EncodedTextSpec: Make private members more private. * Corrupted Oggs generated by GStreamer (e.g. Sound Juicer) can be read. * FileTypes have a .mime attribute which is a list of likely MIME types for the file. * ASF (WMA/WMV) support. * Bug Fixes: * ID3: Fixed reading of v2.3 tags with unsynchronized data. * ID3: The data length indicator for compressed tags is written as a synch-safe integer. 1.9 - 2006.12.09 * New Features: * OptimFROG support. * New mutagen.mp4 module with support for multiple data fields per atom and more compatible tag saving implementation. * Support for embedded pictures in FLAC files (new in FLAC 1.1.3). * mutagen.m4a is deprecated in favor of mutagen.mp4. 1.8 - 2006.10.02 * New Features: * MonkeysAudio support. (#851, Lukáš Lalinský) * APEv2 support on Python 2.5; see API-NOTES. (#852) 1.7.1 - 2006.09.24 * Bug Fixes: * Expose full ID3 tag size as .size. (#848) * New Features: * Musepack Replay Gain data is available in SV7 files. 1.7 - 2006.09.15 * Bug Fixes: * Trying to save an empty tag deletes it. (#813) * The semi-public API removal mentioned in 1.6's API-NOTES happened. * Stricter frame ID validation. (#830, Lukáš Lalinský) * Use os.path.devnull on Win32/Mac OS X. (#831, Lukáš Lalinský) * New Features: * FLAC cuesheet and seektable support. (#791, Nuutti Kotivuori) * Kwargs can be passed to ID3 constructors. (#824, Lukáš Lalinský) * mutagen.musepack: Read/tag Musepack files. (#825, Lukáš Lalinský) * Tools: * mutagen-inspect responds immediately to keyboard interrupts. 1.6 - 2006.08.09 * Bug Fixes: * IOError rather than NameError is raised when File succeeds in typefinding but fails in stream parsing. * errors= kwarg is correctly interpreted for FLAC tags now. * Handle struct.pack API change in Python 2.5b2. (SF #1530559) * Metadata 'load' methods always reset in-memory tags. * Metadata 'delete' methods always clear in-memory tags. * New Features: * Vorbis comment vendor strings include the Mutagen version. * mutagen.id3: Read ASPI, ETCO, SYTC, MLLT, EQU2, and LINK frames. * mutagen.m4a: Read/tag MPEG-4 AAC audio files with iTunes tags. (#681) * mutagen.oggspeex: Read/tag Ogg Speex files. * mutagen.trueaudio: Read/tag True Audio files. * mutagen.wavpack: Read/tag WavPack files. * Tools: * mid3v2: --delete-frames. (#635) 1.5.1 - 2006.06.26 * Bug Fixes: * Handle ENODEV from mmap (e.g. on fuse+sshfs). * Reduce test rerun time. 1.5 - 2006.06.20 * Bug Fixes: * APEv2 * Invalid Lyrics3v2 tags are ignored/overwritten. * Binary values are autodetected as documented. * OggVorbis, OggFLAC: * Write when the setup packet spans multiple pages. * Zero granule position for header packets. * New Features: * mutagen.oggtheora: Read/tag Ogg Theora files. * Test Ogg formats with ogginfo, if present. 1.4 - 2006.06.03 * Bug Fixes: * EasyID3: Fix tag["key"] = "string" handler. (#693) * APEv2: * Skip Lyrics3v2 tags. (Miguel Angel Alvarez) * Avoid infinite loop on malformed tags at the start of the file. * Proper ANSI semantics for file positioning. (#707) * New Features: * VComment: Handle malformed Vorbis comments when errors='ignore' or errors='replace' is passed to VComment#load. (Bastian Kleineidam, #696) * Test running is now controlled through setup.py (./setup.py test). * Test coverage data can be generated (./setup.py coverage). * Considerably more test coverage. 1.3 - 2006.05.29 * New Features: * mutagen.File: Automatic file type detection. * mutagen.ogg: Generic Ogg stream parsing. (#612) * mutagen.oggflac: Read/tag Ogg FLAC files. * mutagen.oggvorbis no longer depends on pyvorbis. * ID3: SYLT support. (#672) 1.2 - 2006.04.23 * Bug Fixes: * MP3: Load files with zeroed Xing headers. (#626) * ID3: Upgrade ID3v2.2 PIC tags to ID3v2.4 APIC tags properly. * Tests exit with non-zero status if any have failed. * Full dict protocol support for VCommentDict, FileType, and APEv2 objects. * New features: * mutagen.oggvorbis gives pyvorbis a Mutagen-like API. * mutagen.easyid3 makes simple ID3 tag changes easier. * A brief TUTORIAL was added. * Tools: * mid3iconv, a clone of id3iconv, was added by Emfox Zhou. (#605) 1.1 - 2006.04.04 * ID3: * Frame and Spec objects are not hashable. * COMM, USER: Accept non-ASCII (completely invalid) language codes. * Enable redundant data length bit for compressed frames. 1.0 - 2006.03.13 * mutagen.FileType, an abstract container for tags and stream information. * MP3: A new FileType subclass for MPEG audio files. * FLAC: * Add FLAC#delete. * Raise correct exception when saving to a non-FLAC file. * FLAC#vc is deprecated in favor of FLAC#tags. * VComment (used by FLAC): * VComment#clear to clear all tags. * VComment#as_dict to return a dict of the tags. * ID3: * Fix typos in PRIV#_pprint, OWNE#_pprint, UFID#_pprint. * mutagen-pony: Try finding lengths as well as tags. * mutagen-inspect: Output stream information with tags. 0.9 - 2006.02.21 * Initial release. mutagen-1.22/COPYING0000644000175000017500000004311012146763412014364 0ustar lazkalazka00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program 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 2 of the License, or (at your option) any later version. This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License.