mutagen-1.31/0000755000175000017500000000000012574256060013333 5ustar lazkalazka00000000000000mutagen-1.31/docs/0000755000175000017500000000000012574256060014263 5ustar lazkalazka00000000000000mutagen-1.31/docs/man/0000755000175000017500000000000012574256060015036 5ustar lazkalazka00000000000000mutagen-1.31/docs/man/moggsplit.rst0000644000175000017500000000174712574061222017600 0ustar lazkalazka00000000000000=========== moggsplit =========== ------------------------- split Ogg logical streams ------------------------- :Manual section: 1 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.31/docs/man/mutagen-pony.rst0000644000175000017500000000073612574061222020213 0ustar lazkalazka00000000000000============== mutagen-pony ============== --------------------------------- scan a collection of MP3 files --------------------------------- :Manual section: 1 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.31/docs/man/index.rst0000644000175000017500000000020412574061222016665 0ustar lazkalazka00000000000000Tools ===== .. toctree:: :titlesonly: mid3cp mid3iconv mid3v2 moggsplit mutagen-inspect mutagen-pony mutagen-1.31/docs/man/mutagen-inspect.rst0000644000175000017500000000075512574061222020674 0ustar lazkalazka00000000000000================= mutagen-inspect ================= --------------------------------- view Mutagen-supported audio tags --------------------------------- :Manual section: 1 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.31/docs/man/mid3v2.rst0000644000175000017500000000666612574061222016704 0ustar lazkalazka00000000000000========= mid3v2 ========= ----------------------------------- audio tag editor similar to 'id3v2' ----------------------------------- :Manual section: 1 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=FRAMES Delete specific ID3v2 frames (or groups of frames) from the files. `FRAMES` is a "," separated list of frame names e.g. ``"TPE1,TALB"`` -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= 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= Set the year/date information (TDRC). -T, --track= 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.31/docs/man/Makefile0000644000175000017500000000021212574061222016463 0ustar lazkalazka00000000000000# update man pages all: mid3cp.1 mid3iconv.1 mid3v2.1 moggsplit.1 mutagen-inspect.1 mutagen-pony.1 %.1:%.rst rst2man $< > ../../man/$@ mutagen-1.31/docs/man/mid3iconv.rst0000644000175000017500000000144612574061222017462 0ustar lazkalazka00000000000000=========== mid3iconv =========== ------------------------- convert ID3 tag encodings ------------------------- :Manual section: 1 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.31/docs/man/mid3cp.rst0000644000175000017500000000145612574061222016747 0ustar lazkalazka00000000000000======== mid3cp ======== ------------- copy ID3 tags ------------- :Manual section: 1 SYNOPSIS ======== **mid3cp** [*options*] *source* *dest* DESCRIPTION =========== **mid3cp** copies the ID3 tags from a source file to a destination file. It is designed to provide similar functionality to id3lib's id3cp tool, and can optionally write ID3v1 tags. It can also exclude specific tags from being copied. OPTIONS ======= --verbose, -v Be verbose: state all operations performed, and list tags in source file. --write-v1 Write ID3v1 tags to the destination file, derived from the ID3v2 tags. --exclude-tag, -x Exclude a specific tag from being copied. Can be specified multiple times. AUTHOR ====== Marcus Sundman. Based on id3cp (part of id3lib) by Dirk Mahoney and Scott Thomas Haug. mutagen-1.31/docs/index.rst0000644000175000017500000000761112574061222016123 0ustar lazkalazka00000000000000.. image:: images/logo.png :align: center :width: 400px ---- .. toctree:: :titlesonly: :maxdepth: 2 tutorial changelog api_notes bugs api/index man/index ===================== Mutagen Documentation ===================== .. 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, OptimFROG, and AIFF 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, 2.7, 3.3, 3.4 (CPython and PyPy) and has no dependencies outside the Python standard library. There is a :doc:`brief tutorial with several API examples. ` Where do I get it? ------------------ Mutagen is hosted on `Bitbucket `_. The `download page `_ will have the latest version or check out the Mercurial repository:: $ 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.31/docs/tutorial.rst0000644000175000017500000002576212574061222016666 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. ID3 Versions ^^^^^^^^^^^^ .. py:currentmodule:: mutagen.id3 Mutagen's ID3 API is primary targeted at id3v2.4, so by default any id3 tags will be upgraded to 2.4 and saving a file will make it 2.4 as well. Saving as 2.3 is possible but needs some extra steps. By default mutagen will: * Load the file * Upgrade any ID3v2.2 frames to their ID3v2.3/4 counterparts (``TT2`` to ``TIT2`` for example) * Upgrade 2.3 only frames to their 2.4 counterparts or throw them away in case there exists no sane upgrade path. In code it comes down to this:: from mutagen.id3 import ID3 audio = ID3("example.mp3") audio.save() The :attr:`ID3.version` attribute contains the id3 version the loaded file had. For more control the following functions are important: * :func:`ID3` which loads the tags and if ``translate=True`` (default) calls either :meth:`ID3.update_to_v24` or :meth:`ID3.update_to_v23` depending on the ``v2_version`` argument (defaults to ``4``) * :meth:`ID3.update_to_v24` which upgrades v2.2/3 frames to v2.4 * :meth:`ID3.update_to_v23` which downgrades v2.4 and upgrades v2.2 frames to v2.3 * :meth:`ID3.save` which will save as v2.3 if ``v2_version=3`` (defaults to ``4``) and also allows specifying a separator for joining multiple text values into one (defaults to ``v23_sep='/'``). To load any ID3 tag and save it as v2.3 do the following:: from mutagen.id3 import ID3 audio = ID3("example.mp3", v2_version=3) audio.save(v2_version=3) You may notice that if you load a v2.4 file this way, the text frames will still have multiple values or are defined to be saved using UTF-8, both of which isn't valid in v2.3. But the resulting file will still be valid because the following will happen in :meth:`ID3.save`: * Frames that use UTF-8 as text encoding will be saved as UTF-16 instead. * Multiple values in text frames will be joined with ``v23_sep`` as passed to :meth:`ID3.save`. Nonstandard ID3v2.3 Tricks ~~~~~~~~~~~~~~~~~~~~~~~~~~ Saving v2.4 frames in v2.3 tags While not standard conform, you can exclude certain v2.4 frames from being thrown out by :meth:`ID3.update_to_v23` by removing them temporarily:: audio = ID3("example.mp3", translate=False) keep_these = audio.getall("TSOP") audio.update_to_v23() audio.setall("TSOP", keep_these) audio.save(v2_version=3) Saving Multiple Text Values in v2.3 Tags The v2.3 standard states that after a text termination "all the following information should be ignored and not be displayed". So, saving multiple values separated by the text terminator should allow v2.3 only readers to read the first value while providing a way to read all values back. But editing these files will probably throw out all the other values and some implementations might get confused about the extra non-NULL data, so this isn't recommended. To use the terminator as value separator pass ``v23_sep=None`` to :meth:`ID3.save`. :: audio = ID3("example.mp3", v2_version=3) audio.save(v2_version=3, v23_sep=None) Mutagen itself disregards the v2.3 spec in this case and will read them back as multiple values. 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. VorbisComment ^^^^^^^^^^^^^ VorbisComment is the tagging format used in Ogg and FLAC container formats. In mutagen this corresponds to the tags in all subclasses of :class:`mutagen.ogg.OggFileType` and the :class:`mutagen.flac.FLAC` class. Embedded Images ~~~~~~~~~~~~~~~ The most common way to include images in VorbisComment is to store a base64 encoded FLAC Picture block with the key ``metadata_block_picture`` [0]. See the following code example on how to read and write images this way:: # READING / SAVING import base64 from mutagen.oggvorbis import OggVorbis from mutagen.flac import Picture, error as FLACError file_ = OggVorbis("somefile.ogg") for b64_data in file_.get("metadata_block_picture", []): try: data = base64.b64decode(b64_data) except (TypeError, ValueError): continue try: picture = Picture(data) except FLACError: continue extensions = { "image/jpeg": "jpg", "image/png": "png", "image/gif": "gif", } ext = extensions.get(picture.mime, "jpg") with open("image.%s" % ext, "wb") as h: h.write(picture.data) :: # WRITING import base64 from mutagen.oggvorbis import OggVorbis from mutagen.flac import Picture file_ = OggVorbis("somefile.ogg") with open("image.jpeg", "rb") as h: data = h.read() picture = Picture() picture.data = data picture.type = 17 picture.desc = u"A bright coloured fish" picture.mime = u"image/jpeg" picture.width = 100 picture.height = 100 picture.depth = 24 picture_data = picture.write() encoded_data = base64.b64encode(picture_data) vcomment_value = encoded_data.decode("ascii") file_["metadata_block_picture"] = [vcomment_value] file_.save() Some programs also write base64 encoded image data directly into the ``coverart`` field and sometimes a corresponding mime type into the ``coverartmime`` field:: # READING import base64 import itertools from mutagen.oggvorbis import OggVorbis file_ = OggVorbis("somefile.ogg") values = file_.get("coverart", []) mimes = file_.get("coverartmime", []) for value, mime in itertools.izip_longest(values, mimes, fillvalue=u""): try: image_data = base64.b64decode(value.encode("ascii")) except (TypeError, ValueError): continue print(mime) print(image_data) FLAC supports images directly, see :class:`mutagen.flac.Picture`, :attr:`mutagen.flac.FLAC.pictures`, :meth:`mutagen.flac.FLAC.add_picture` and :meth:`mutagen.flac.FLAC.clear_pictures`. [0] https://wiki.xiph.org/VorbisComment#Cover_art Padding ~~~~~~~ Many formats mutagen supports include a notion of metadata padding, empty space in the file following the metadata. In case the size of the metadata increases, this empty space can be claimed and written into. The alternative would be to resize the whole file, which means everything after the metadata needs to be rewritten. This can be a time consuming operation if the file is large. For formats where mutagen supports using such a padding it will use the existing padding for extending metadata, add additional padding if the added data exceeds the size of the existing padding and reduce the padding size if it makes up more than a significant part of the file size. It also provides additional API to control the padding usage. Some `mutagen.FileType` and `mutagen.Metadata` subclasses provide a ``save()`` method which can be passed a padding callback. This callback gets called with a `mutagen.PaddingInfo` instance and should return the amount of padding to write to the file. :: from mutagen.mp3 import MP3 def no_padding(info): # this will remove all padding return 0 def default_implementation(info): # this is the default implementation, which can be extended return info.get_default_padding() def no_new_padding(info): # this will use existing padding but never add new one return max(info.padding, 0) f = MP3("somefile.mp3") f.save(padding=no_padding) f.save(padding=default_implementation) f.save(padding=no_new_padding) mutagen-1.31/docs/Makefile0000644000175000017500000000055612574061222015723 0ustar lazkalazka00000000000000all: _rtd_theme sphinx-build -E -Dhtml_theme=_rtd_theme -Dhtml_theme_path=. -b html -n . _build clean: rm -rf _build _rtd_theme .PHONY: clean _rtd_theme: wget https://github.com/snide/sphinx_rtd_theme/archive/master.tar.gz tar --strip-components=1 -zxvf master.tar.gz sphinx_rtd_theme-master/sphinx_rtd_theme mv sphinx_rtd_theme _rtd_theme rm master.tar.gz mutagen-1.31/docs/changelog.rst0000644000175000017500000000011112574061222016727 0ustar lazkalazka00000000000000Changelog ========= .. py:currentmodule:: mutagen .. include:: ../NEWS mutagen-1.31/docs/images/0000755000175000017500000000000012574256060015530 5ustar lazkalazka00000000000000mutagen-1.31/docs/images/logo.svg0000644000175000017500000007251712574061222017217 0ustar lazkalazka00000000000000 mutagen-1.31/docs/images/logo.png0000644000175000017500000002170312574061222017173 0ustar lazkalazka00000000000000PNG  IHDR<{sBIT|d pHYs btEXtSoftwarewww.inkscape.org< IDATxyU+!$̄)9&d!*('LM7v"ТMp C0Є9@ &X$u9gp{~W߽UU{Uqq2 pq6.HqR qqJq) q.HqR qqJq) q.HqR qqJq) q.HqR qqJq)2n@">kƮ'T.aw`]W+h8P@1C|8-c,Uݬ ^V}^68 UmDdrߙl!]w2 iY؅?b q}-ܯt &60FUv 8C~4Sk:;lEs!ywj QuRJU[8~$F$YAr@D$8~$1N"f.Hq}%HDd- F@q#Ğs@8C$ĩjtBcEwr ɣ2MDVbYqq?AgD _uGq$y;*[ym.WX8ΐof:9{XUU畬{`.fe0VU痩qgO#p/6IS! `:pOاw[\ǿׇ 0x+QDY.^? J$ ppUsF%ʏT>ok% 댓m~P X0J{uey^=FFavy È~{݁$n^>^A:qO$ c\x 1qMO%;D:S[yjP gW+i$ӰYaʉ]($\3KS.uu,X95QY;->hł$ծXwlY&} _ źa]A+Ɖs Aֳ/{U}"-]&3{s%w廊LLI}땆#XڬEI12 oit+r;@YC{"˖J"X6-YUl*p|LaxSװ/fǃ}ƒiv\U$۰Yrs; FBkaZHo"qp<91x0O})hU|l3؏<&os ͱ6=P:,*c }a a-$_MbllP_, /x UIy`F3V7pMH\Xөi7ˆ`wnq$ܯ;d W궪iv' #ia4V*I#"7xCT S0͉Okn{?cDd_"U}mn`?9JDۦl`Dܢ[{/xm-"E:sDdC&8br-y6pTs1AT_GׂӁԏ >!"'hlYEUfZDdmɜ٦ (THLU­`m_q.Ho%1bU`=/aFkL5JO.8gep_bNRB` fĿS1%]lnj5aK0Ϧ/c[e^nS's|,(`'":b?=KTPgcga3-_dl.iz|6|̽÷0Mɓ`7)ao$UvQWy$CUJDX+Pb!v p&?Q}==|{`5u">pgzF>Hّ_^vTɭ č+ȲNAA9`{US MR(gUcz0;׈}UNkV?G,\獝zZd9pT&X2'-a"N &F$Uټl@CA9\z^%;b'Owo`wlvTt5n'3OU謁 9y`@ 65?`(Ʀ|CUfGַ%fØ9eL1E{IzڊU1c:vܫBJD;UU""Wf~VaCD6QUcݹt#ەPժI ,> ۯ3 NbڌdSe6Ran-3z 9.pCQmDd>R l+FUUw _/drR+&j]p\lvj;vQ[ETKϨzZ"iKk\1v 3p~l ozi#ِ8! 6cIK&6YY _Q,\Nإbq^ <ZROQ1Y$v6 <6* _j}$Z$9ADf%[8KAMvhөn@ >:?6/!RDDaI,+s@lS՗s50Am"D1FܜT5m.vTRvDrh[Ȕ'"8}JO}Hrx2\̭YQN%o28ykljWH6yt4 >O d! vP b:,7.gY>8NJU+twkwq Z~$A2\7U}QD^r,c;,O9fy'x{~^}6ȇAeTc8R 6c9jY^Oe?b^k܃iG*R5_>(*E7 2򅀍arr?پ]p&a3'A<5k2X$sF\ q&6 q2&gz2"U*0جm"%{y^-OU' bN3b9\ǰ,&ʎ&^Uuwo+y [aIڐ$xn@g8,CسYJɠJcqBHc UVK1Ar_fTɕ1ouTSE>7PK)"߈%aKwx0D!8=9ȲXǿÞiSu\|]Ul4.8?JXܷ4gԙd6f;Ed3,^X\+C/®NdZ`Shs6<-"?~&ODz3"QKհ٦GrWF2h7B/M qZչKZ+fZsRWF5 GZ_ .I|)ERHXi"2FDV/Ƌ0a}?O8XECDJnLUL]E8Lݡ"r96=zGm!|}F_mEWX~Si#D{3BRdU;Ӣ8 *FQ ʹ]Vr}1܎E( g`ް2E$6ܹı?98@ZEj+u)9!3<(1hfk~,±Xw*\("E֠b>v[SbxZm TeyX'Lv ?(_}ZUFngߏ5&iAasĄ918,OvV~yL}S0S,CZA7b t35WXb/ ~ng$8:떰>!U}GDV>lv"CJ YkcAWMm.f˸n쮘:],܁7|{arXc ]l$v*njlG}{$鉉}AK1 E ]q9x {Ҫ9c|,mg&F/Ȏ Fj3}*cRpr %N6`cM^(_A*yFlj7R/DW3%~)"'c6>:7E)f`9.&3S~$9[Y[RmQU{!HI ݦ=V/y%DTu:5e4~=9\%^~EUS'[t7=88x3zbL<Q_ñ4UMm,k5ѣU= u?yI% U {1AɎiðVLMEJ!Nn{n1'l\1N5RACU QO;tcS||G./Ę\J;M0 S3v -A2:HN} _ bA0hH7_ Uihqa&u_el[%`U}:x7'Fm`U4(wyE蠰s",iӫ:HfY 7X a-&Y|!t94͎ IVuXhIm\,դ.lG:4SivhjUqVomT%NmfQ.pU{֪QNˉ;Ē3ڳץz Υ=" ?ge;k5<=!Hmn73t4FiA *>ȶ_aTP+mh_ &!.6"" m{tѾ6vbW|:F̾ӌk_t(l*! I|e*__/[ *LH;tf><~aǕw{Ut e\03,zߤz(*LHER0;lF?&mSZquD߱`jN؎VOW݁:ZAݫ]G$iC{.BMw/=ؐu ?"NwBˈ.#VU%+3Y#Z%>2ef\1?Zj(ݎtE| [Fў1v:C.q~N81O2v! WlU>^hĢ\ ʚ ڊt.UDdTۢZ_Vef 5"LA/cU0oF֢ vaz!2fM٩춳+"m݋CnHmhž*fd)BJ.&i")\t0,FGh  0V}[۵#Cn vh:l/xݗ8# +H svucv޴Bz$ƆV%!H7 aZSpHH nq"e]ҡF$ )F%WTa ڵǪȊ2rNǨ,9OXSBwR. B:8"rd7tK|=o|=%4 4[U~(f;= kcȚWԥ5mެK.emgF]A:=b:_ƂX G+H&Gś/^̱<NE%O༘V:Haocy;͕4jDV NKLYm̆sW#-*m<ǜ<[8vM螾dMʎ• i|V6Q6y <R1'[".!:]q؈eBcwz9}Jq !ysޮ\2T๝:Ǭ2<˰q)TuM}flRUˀc"B}Yi("?qT1}&P^GSmȝ^-"ylc m&f sWjc?;`6ugm)wG/a*u0΃wRjJg7D{b|[MW'D[>y˨Eh1iĒTW/F``h}VH{l7]/ﰓJuG*](cdDuRu"eNyU},RbZV]Gk!k/dEd6U*C}켮KyGꂤ$"6.muƾF,}\~*ǩCD\k,RUORAFR*TCQj: u3ZϭVsႤ%cWN \TCQֳZFt9&KGԶGUuF/4Ⴄ 2TU"u3,eDdGOngiI5/%HE[>"qIDe`)O6rV&'$V `~ XC-h%Znds\j-ma8< ײf|2Yq: 4(mŅZI,1!3Um%6"AL[}EUd`aYXu&zsiU[YrU=}\? aUS,ZX~96 x>3bnjTk՘F YҒ|)5cӇj"Ԭ LUk't~qR\TK0RݘnqR\TKL' VQ{l9S).H% 8^[""oz>[w~!nRd!0#ǩTH[[c qIRuRUzZT if'yJUB7ͼ8Nde|B 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.31/docs/api_notes.rst0000644000175000017500000000522212574061222016771 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.31/docs/conf.py0000644000175000017500000000146512574061222015562 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os import sys dir_ = os.path.dirname(os.path.realpath(__file__)) sys.path.insert(0, dir_) sys.path.insert(0, os.path.abspath(os.path.join(dir_, ".."))) import mutagen extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'ext'] intersphinx_mapping = {'python': ('http://docs.python.org/2.7', None)} source_suffix = '.rst' master_doc = 'index' project = 'mutagen' copyright = u'2014, Joe Wreschnig, Michael Urman, Lukáš Lalinský, ' \ u'Christoph Reiter, Ben Ockmore & others' version = mutagen.version_string release = mutagen.version_string exclude_patterns = ['_build'] bug_url_template = "http://bitbucket.org/lazka/mutagen/issue/%s" pr_url_template = "http://bitbucket.org/lazka/mutagen/pull-request/%s" autodoc_member_order = "bysource" default_role = "obj" mutagen-1.31/docs/bugs.rst0000644000175000017500000000357412574061222015760 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.31/docs/api/0000755000175000017500000000000012574256060015034 5ustar lazkalazka00000000000000mutagen-1.31/docs/api/id3.rst0000644000175000017500000000132512574061222016240 0ustar lazkalazka00000000000000ID3 === .. automodule:: mutagen.id3 ID3 Frames ---------- .. toctree:: :titlesonly: id3_frames .. autoclass:: mutagen.id3.PictureType :members: :member-order: bysource .. autoclass:: mutagen.id3.Encoding :members: :member-order: bysource 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 mutagen-1.31/docs/api/base.rst0000644000175000017500000000151612574061222016475 0ustar lazkalazka00000000000000Main Module ----------- .. automodule:: mutagen :members: File, version, version_string Base Classes ~~~~~~~~~~~~ .. class:: text() This type only exists for documentation purposes. It represents :obj:`unicode` under Python 2 and :obj:`str` under Python 3. .. autoclass:: mutagen.FileType(filename) :members: pprint, add_tags, mime :show-inheritance: .. automethod:: delete() .. automethod:: save() .. autoclass:: mutagen.Metadata .. automethod:: delete() .. automethod:: save() .. autoclass:: mutagen.StreamInfo :members: pprint .. autoclass:: mutagen.PaddingInfo() :members: .. autoclass:: mutagen.MutagenError Internal Classes ~~~~~~~~~~~~~~~~ .. automodule:: mutagen._util .. autoclass:: mutagen._util.DictMixin .. autoclass:: mutagen._util.DictProxy :show-inheritance: mutagen-1.31/docs/api/asf.rst0000644000175000017500000000252212574061222016332 0ustar lazkalazka00000000000000ASF === .. automodule:: mutagen.asf .. autoclass:: mutagen.asf.ASF(filename) :show-inheritance: :members: .. autoclass:: mutagen.asf.ASFInfo :show-inheritance: :members: :undoc-members: .. autoclass:: mutagen.asf.ASFTags :show-inheritance: :members: :undoc-members: .. autoclass:: mutagen.asf.error :show-inheritance: :members: :undoc-members: .. autoclass:: mutagen.asf.ASFBaseAttribute() :members: .. autoclass:: mutagen.asf.ASFBoolAttribute(value) :members: :bases: `ASFBaseAttribute` .. autoclass:: mutagen.asf.ASFGUIDAttribute(value) :members: :bases: `ASFBaseAttribute` .. autoclass:: mutagen.asf.ASFWordAttribute(value) :members: :bases: `ASFBaseAttribute` .. autoclass:: mutagen.asf.ASFDWordAttribute(value) :members: :bases: `ASFBaseAttribute` .. autoclass:: mutagen.asf.ASFQWordAttribute(value) :members: :bases: `ASFBaseAttribute` .. autoclass:: mutagen.asf.ASFBoolAttribute(value) :members: :bases: `ASFBaseAttribute` .. autoclass:: mutagen.asf.ASFByteArrayAttribute(value) :members: :bases: `ASFBaseAttribute` .. autoclass:: mutagen.asf.ASFUnicodeAttribute(value) :members: :bases: `ASFBaseAttribute` .. autoclass:: mutagen.asf.ASFUnicodeAttribute(value) :members: :bases: `ASFBaseAttribute` mutagen-1.31/docs/api/oggvorbis.rst0000644000175000017500000000054712574061222017567 0ustar lazkalazka00000000000000Ogg Vorbis ---------- .. automodule:: mutagen.oggvorbis .. autoexception:: mutagen.oggvorbis.error :show-inheritance: .. autoexception:: mutagen.oggvorbis.OggVorbisHeaderError :show-inheritance: .. autoclass:: mutagen.oggvorbis.OggVorbis(filename) :show-inheritance: :members: .. autoclass:: mutagen.oggvorbis.OggVorbisInfo :members: mutagen-1.31/docs/api/ogg.rst0000644000175000017500000000030612574061222016333 0ustar lazkalazka00000000000000OGG === .. automodule:: mutagen.ogg .. autoexception:: mutagen.ogg.error .. autoclass:: mutagen.ogg.OggFileType(filename) :show-inheritance: .. autoclass:: mutagen.ogg.OggPage :members: mutagen-1.31/docs/api/vcomment.rst0000644000175000017500000000024612574061222017412 0ustar lazkalazka00000000000000Vorbis Comment ============== .. autoclass:: mutagen._vorbis.VComment() :show-inheritance: .. autoclass:: mutagen._vorbis.VCommentDict() :show-inheritance: mutagen-1.31/docs/api/oggspeex.rst0000644000175000017500000000053512574061222017404 0ustar lazkalazka00000000000000Ogg Speex ========= .. automodule:: mutagen.oggspeex .. autoexception:: mutagen.oggspeex.error :show-inheritance: .. autoexception:: mutagen.oggspeex.OggSpeexHeaderError :show-inheritance: .. autoclass:: mutagen.oggspeex.OggSpeex(filename) :show-inheritance: :members: .. autoclass:: mutagen.oggspeex.OggSpeexInfo :members: mutagen-1.31/docs/api/flac.rst0000644000175000017500000000075612574061222016475 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.31/docs/api/index.rst0000644000175000017500000000037712574061222016676 0ustar lazkalazka00000000000000API === .. toctree:: base aac aiff ape asf flac id3 monkeysaudio mp3 mp4 musepack ogg oggflac oggopus oggspeex oggtheora oggvorbis optimfrog trueaudio vcomment wavpack mutagen-1.31/docs/api/oggflac.rst0000644000175000017500000000054412574061222017165 0ustar lazkalazka00000000000000Ogg 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() :show-inheritance: :members: mutagen-1.31/docs/api/mp3.rst0000644000175000017500000000054212574061222016260 0ustar lazkalazka00000000000000MP3 === .. automodule:: mutagen.mp3 .. autoclass:: mutagen.mp3.MP3(filename, ID3=None) :show-inheritance: :members: .. autoclass:: mutagen.mp3.MPEGInfo() :members: .. autoclass:: mutagen.mp3.BitrateMode() :members: .. autoclass:: mutagen.mp3.EasyMP3(filename, ID3=None) :show-inheritance: :members: :exclude-members: ID3 mutagen-1.31/docs/api/wavpack.rst0000644000175000017500000000027012574061222017213 0ustar lazkalazka00000000000000WavPack ======= .. automodule:: mutagen.wavpack .. autoclass:: mutagen.wavpack.WavPack :show-inheritance: :members: .. autoclass:: mutagen.wavpack.WavPackInfo :members: mutagen-1.31/docs/api/oggtheora.rst0000644000175000017500000000054712574061222017545 0ustar lazkalazka00000000000000Ogg Theora ========== .. automodule:: mutagen.oggtheora .. autoexception:: mutagen.oggtheora.error :show-inheritance: .. autoexception:: mutagen.oggtheora.OggTheoraHeaderError :show-inheritance: .. autoclass:: mutagen.oggtheora.OggTheora(filename) :show-inheritance: :members: .. autoclass:: mutagen.oggtheora.OggTheoraInfo :members: mutagen-1.31/docs/api/ape.rst0000644000175000017500000000065112574061222016327 0ustar lazkalazka00000000000000APEv2 ===== .. automodule:: mutagen.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: mutagen-1.31/docs/api/id3_frames.rst0000644000175000017500000004117312574061222017602 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.TDES(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.TGID(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.WFED(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.31/docs/api/aiff.rst0000644000175000017500000000025712574061222016471 0ustar lazkalazka00000000000000AIFF ---- .. automodule:: mutagen.aiff .. autoclass:: mutagen.aiff.AIFF(filename) :show-inheritance: :members: .. autoclass:: mutagen.aiff.AIFFInfo() :members: mutagen-1.31/docs/api/monkeysaudio.rst0000644000175000017500000000033712574061222020272 0ustar lazkalazka00000000000000Monkey's Audio ============== .. automodule:: mutagen.monkeysaudio .. autoclass:: mutagen.monkeysaudio.MonkeysAudio :show-inheritance: :members: .. autoclass:: mutagen.monkeysaudio.MonkeysAudioInfo :members: mutagen-1.31/docs/api/aac.rst0000644000175000017500000000035012574061222016302 0ustar lazkalazka00000000000000AAC === .. automodule:: mutagen.aac .. autoexception:: mutagen.aac.AACError .. autoclass:: mutagen.aac.AAC(filename) :show-inheritance: :members: .. autoclass:: mutagen.aac.AACInfo() :show-inheritance: :members: mutagen-1.31/docs/api/mp4.rst0000644000175000017500000000131712574061222016262 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.31/docs/api/musepack.rst0000644000175000017500000000027712574061222017376 0ustar lazkalazka00000000000000Musepack ======== .. automodule:: mutagen.musepack .. autoclass:: mutagen.musepack.Musepack :show-inheritance: :members: .. autoclass:: mutagen.musepack.MusepackInfo :members: mutagen-1.31/docs/api/optimfrog.rst0000644000175000017500000000030612574061222017565 0ustar lazkalazka00000000000000OptimFROG ========= .. automodule:: mutagen.optimfrog .. autoclass:: mutagen.optimfrog.OptimFROG :show-inheritance: :members: .. autoclass:: mutagen.optimfrog.OptimFROGInfo :members: mutagen-1.31/docs/api/oggopus.rst0000644000175000017500000000052312574061222017243 0ustar lazkalazka00000000000000Ogg Opus ======== .. automodule:: mutagen.oggopus .. autoexception:: mutagen.oggopus.error :show-inheritance: .. autoexception:: mutagen.oggopus.OggOpusHeaderError :show-inheritance: .. autoclass:: mutagen.oggopus.OggOpus(filename) :show-inheritance: :members: .. autoclass:: mutagen.oggopus.OggOpusInfo :members: mutagen-1.31/docs/api/trueaudio.rst0000644000175000017500000000053712574061222017566 0ustar lazkalazka00000000000000TrueAudio ========= .. 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.31/docs/ext.py0000644000175000017500000000151412574061222015430 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- from docutils import nodes def bug_role(name, rawtext, text, lineno, inliner, *args, **kwargs): app = inliner.document.settings.env.app url_tmpl = app.config.bug_url_template or "missing/%s" node = nodes.reference( rawtext, "[%s]" % text, refuri=url_tmpl % text) return [node], [] def pr_role(name, rawtext, text, lineno, inliner, *args, **kwargs): app = inliner.document.settings.env.app url_tmpl = app.config.pr_url_template or "missing/%s" node = nodes.reference( rawtext, "[pr-%s]" % text, refuri=url_tmpl % text) return [node], [] def setup(app): app.add_role('bug', bug_role) app.add_config_value('bug_url_template', None, 'env') app.add_role('pr', pr_role) app.add_config_value('pr_url_template', None, 'env') mutagen-1.31/mutagen/0000755000175000017500000000000012574256060014773 5ustar lazkalazka00000000000000mutagen-1.31/mutagen/monkeysaudio.py0000644000175000017500000000541212574061222020050 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 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 ._compat import endswith from mutagen import StreamInfo from mutagen.apev2 import APEv2File, error, delete from mutagen._util import cdata class MonkeysAudioHeaderError(error): pass class MonkeysAudioInfo(StreamInfo): """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(b"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 u"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(b"MAC ") + endswith(filename.lower(), ".ape") Open = MonkeysAudio mutagen-1.31/mutagen/_util.py0000644000175000017500000003633412574061222016464 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 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 import codecs from fnmatch import fnmatchcase from ._compat import chr_, PY2, iteritems, iterbytes, integer_types, xrange, \ izip class MutagenError(Exception): """Base class for all custom exceptions in mutagen .. versionadded:: 1.25 """ __module__ = "mutagen" def total_ordering(cls): assert "__eq__" in cls.__dict__ assert "__lt__" in cls.__dict__ cls.__le__ = lambda self, other: self == other or self < other cls.__gt__ = lambda self, other: not (self == other or self < other) cls.__ge__ = lambda self, other: not self < other cls.__ne__ = lambda self, other: not self.__eq__(other) return cls def hashable(cls): """Makes sure the class is hashable. Needs a working __eq__ and __hash__ and will add a __ne__. """ # py2 assert "__hash__" in cls.__dict__ # py3 assert cls.__dict__["__hash__"] is not None assert "__eq__" in cls.__dict__ cls.__ne__ = lambda self, other: not self.__eq__(other) return cls def enum(cls): assert cls.__bases__ == (object,) d = dict(cls.__dict__) new_type = type(cls.__name__, (int,), d) new_type.__module__ = cls.__module__ map_ = {} for key, value in iteritems(d): if key.upper() == key and isinstance(value, integer_types): value_instance = new_type(value) setattr(new_type, key, value_instance) map_[value] = key def str_(self): if self in map_: return "%s.%s" % (type(self).__name__, map_[self]) return "%d" % int(self) def repr_(self): if self in map_: return "<%s.%s: %d>" % (type(self).__name__, map_[self], int(self)) return "%d" % int(self) setattr(new_type, "__repr__", repr_) setattr(new_type, "__str__", str_) return new_type @total_ordering 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 if PY2: has_key = __has_key __contains__ = __has_key if PY2: iterkeys = lambda self: iter(self.keys()) def values(self): return [self[k] for k in self.keys()] if PY2: itervalues = lambda self: iter(self.values()) def items(self): return list(izip(self.keys(), self.values())) if PY2: iteritems = lambda s: iter(s.items()) def clear(self): for key in list(self.keys()): self.__delitem__(key) 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): for key in self.keys(): break else: raise KeyError("dictionary is empty") return key, self.pop(key) def update(self, other=None, **kwargs): if other is None: self.update(kwargs) other = {} try: for key, value in other.items(): self.__setitem__(key, value) 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 __eq__(self, other): return dict(self.items()) == other def __lt__(self, other): return 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() def _fill_cdata(cls): """Add struct pack/unpack functions""" funcs = {} for key, name in [("b", "char"), ("h", "short"), ("i", "int"), ("q", "longlong")]: for echar, esuffix in [("<", "le"), (">", "be")]: esuffix = "_" + esuffix for unsigned in [True, False]: s = struct.Struct(echar + (key.upper() if unsigned else key)) get_wrapper = lambda f: lambda *a, **k: f(*a, **k)[0] unpack = get_wrapper(s.unpack) unpack_from = get_wrapper(s.unpack_from) def get_unpack_from(s): def unpack_from(data, offset=0): return s.unpack_from(data, offset)[0], offset + s.size return unpack_from unpack_from = get_unpack_from(s) pack = s.pack prefix = "u" if unsigned else "" if s.size == 1: esuffix = "" bits = str(s.size * 8) funcs["%s%s%s" % (prefix, name, esuffix)] = unpack funcs["%sint%s%s" % (prefix, bits, esuffix)] = unpack funcs["%s%s%s_from" % (prefix, name, esuffix)] = unpack_from funcs["%sint%s%s_from" % (prefix, bits, esuffix)] = unpack_from funcs["to_%s%s%s" % (prefix, name, esuffix)] = pack funcs["to_%sint%s%s" % (prefix, bits, esuffix)] = pack for key, func in iteritems(funcs): setattr(cls, key, staticmethod(func)) class cdata(object): """C character buffer to Python numeric type conversions. For each size/sign/endianness: uint32_le(data)/to_uint32_le(num)/uint32_le_from(data, offset=0) """ from struct import error error = error bitswap = b''.join( chr_(sum(((val >> i) & 1) << (7 - i) for i in xrange(8))) for val in xrange(256)) test_bit = staticmethod(lambda value, n: bool((value >> n) & 1)) _fill_cdata(cdata) def get_size(fileobj): """Returns the size of the file object. The position when passed in will be preserved if no error occurs. In case of an error raises IOError. """ old_pos = fileobj.tell() try: fileobj.seek(0, 2) return fileobj.tell() finally: fileobj.seek(old_pos, 0) 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 fobj.seek(0, 2) filesize = fobj.tell() movesize = filesize - offset fobj.write(b'\x00' * size) fobj.flush() try: import mmap file_map = mmap.mmap(fobj.fileno(), filesize + size) try: file_map.move(offset + size, offset, movesize) finally: file_map.close() except (ValueError, EnvironmentError, ImportError, AttributeError): # handle broken mmap scenarios, BytesIO() 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(b"\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() 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. """ assert 0 < size assert 0 <= offset fobj.seek(0, 2) filesize = fobj.tell() movesize = filesize - offset - size assert 0 <= movesize if movesize > 0: fobj.flush() try: import mmap file_map = mmap.mmap(fobj.fileno(), filesize) try: file_map.move(offset, offset + size, movesize) finally: file_map.close() except (ValueError, EnvironmentError, ImportError, AttributeError): # handle broken mmap scenarios, BytesIO() 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() def resize_bytes(fobj, old_size, new_size, offset): """Resize an area in a file adding and deleting at the end of it. Does nothing if no resizing is needed. """ if new_size < old_size: delete_size = old_size - new_size delete_at = offset + new_size delete_bytes(fobj, delete_size, delete_at) elif new_size > old_size: insert_size = new_size - old_size insert_at = offset + old_size insert_bytes(fobj, insert_size, insert_at) def dict_match(d, key, default=None): """Like __getitem__ but works as if the keys() are all filename patterns. Returns the value of any dict key that matches the passed key. """ if key in d and "[" not in key: return d[key] else: for pattern, value in iteritems(d): if fnmatchcase(key, pattern): return value return default def decode_terminated(data, encoding, strict=True): """Returns the decoded data until the first NULL terminator and all data after it. In case the data can't be decoded raises UnicodeError. In case the encoding is not found raises LookupError. In case the data isn't null terminated (even if it is encoded correctly) raises ValueError except if strict is False, then the decoded string will be returned anyway. """ codec_info = codecs.lookup(encoding) # normalize encoding name so we can compare by name encoding = codec_info.name # fast path if encoding in ("utf-8", "iso8859-1"): index = data.find(b"\x00") if index == -1: # make sure we raise UnicodeError first, like in the slow path res = data.decode(encoding), b"" if strict: raise ValueError("not null terminated") else: return res return data[:index].decode(encoding), data[index + 1:] # slow path decoder = codec_info.incrementaldecoder() r = [] for i, b in enumerate(iterbytes(data)): c = decoder.decode(b) if c == u"\x00": return u"".join(r), data[i + 1:] r.append(c) else: # make sure the decoder is finished r.append(decoder.decode(b"", True)) if strict: raise ValueError("not null terminated") return u"".join(r), b"" class BitReaderError(Exception): pass class BitReader(object): def __init__(self, fileobj): self._fileobj = fileobj self._buffer = 0 self._bits = 0 self._pos = fileobj.tell() def bits(self, count): """Reads `count` bits and returns an uint, MSB read first. May raise BitReaderError if not enough data could be read or IOError by the underlying file object. """ if count < 0: raise ValueError if count > self._bits: n_bytes = (count - self._bits + 7) // 8 data = self._fileobj.read(n_bytes) if len(data) != n_bytes: raise BitReaderError("not enough data") for b in bytearray(data): self._buffer = (self._buffer << 8) | b self._bits += n_bytes * 8 self._bits -= count value = self._buffer >> self._bits self._buffer &= (1 << self._bits) - 1 assert self._bits < 8 return value def bytes(self, count): """Returns a bytearray of length `count`. Works unaligned.""" if count < 0: raise ValueError # fast path if self._bits == 0: data = self._fileobj.read(count) if len(data) != count: raise BitReaderError("not enough data") return data return bytes(bytearray(self.bits(8) for _ in xrange(count))) def skip(self, count): """Skip `count` bits. Might raise BitReaderError if there wasn't enough data to skip, but might also fail on the next bits() instead. """ if count < 0: raise ValueError if count <= self._bits: self.bits(count) else: count -= self.align() n_bytes = count // 8 self._fileobj.seek(n_bytes, 1) count -= n_bytes * 8 self.bits(count) def get_position(self): """Returns the amount of bits read or skipped so far""" return (self._fileobj.tell() - self._pos) * 8 - self._bits def align(self): """Align to the next byte, returns the amount of bits skipped""" bits = self._bits self._buffer = 0 self._bits = 0 return bits def is_aligned(self): """If we are currently aligned to bytes and nothing is buffered""" return self._bits == 0 mutagen-1.31/mutagen/musepack.py0000644000175000017500000002143512574061222017154 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2006 Lukas Lalinsky # Copyright (C) 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 ._compat import endswith, xrange from mutagen import StreamInfo 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 c = bytearray(c) num = (num << 7) | (c[0] & 0x7F) if not c[0] & 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(StreamInfo): """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] == b"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(b"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 = [b"SH", b"RG"] def check_frame_key(key): if ((len(frame_type) != key_size) or (not b'AA' <= frame_type <= b'ZZ')): raise MusepackHeaderError("Invalid frame key.") frame_type = fileobj.read(key_size) check_frame_key(frame_type) while frame_type not in (b"AP", b"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 # packets can be at maximum data_size big and are padded with zeros if frame_type == b"SH": mandatory_packets.remove(frame_type) self.__parse_stream_header(fileobj, data_size) elif frame_type == b"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(map(repr, mandatory_packets))) self.length = float(self.samples) / self.sample_rate self.bitrate = 0 def __parse_stream_header(self, fileobj, data_size): # skip CRC fileobj.seek(4, 1) remaining_size = data_size - 4 try: self.version = bytearray(fileobj.read(1))[0] except TypeError: raise MusepackHeaderError("SH packet ended unexpectedly.") remaining_size -= 1 try: samples, l1 = _parse_sv8_int(fileobj) samples_skip, l2 = _parse_sv8_int(fileobj) except (EOFError, ValueError): raise MusepackHeaderError( "SH packet: Invalid sample counts.") self.samples = samples - samples_skip remaining_size -= l1 + l2 data = fileobj.read(remaining_size) if len(data) != remaining_size: raise MusepackHeaderError("SH packet ended unexpectedly.") self.sample_rate = RATES[bytearray(data)[0] >> 5] self.channels = (bytearray(data)[1] >> 4) + 1 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(b"MP+"): self.version = bytearray(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(u"%+0.2f (title)" % self.title_gain) if hasattr(self, "album_gain"): rg_data.append(u"%+0.2f (album)" % self.album_gain) rg_data = (rg_data and ", Gain: " + ", ".join(rg_data)) or "" return u"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): filename = filename.lower() return (header.startswith(b"MP+") + header.startswith(b"MPCK") + endswith(filename, b".mpc")) Open = Musepack mutagen-1.31/mutagen/id3/0000755000175000017500000000000012574256060015452 5ustar lazkalazka00000000000000mutagen-1.31/mutagen/id3/_util.py0000644000175000017500000001033612574061222017135 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2005 Michael Urman # 2013 Christoph Reiter # 2014 Ben Ockmore # # 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 .._compat import long_, integer_types, PY3 from .._util import MutagenError class error(MutagenError): pass class ID3NoHeaderError(error, ValueError): pass class ID3UnsupportedVersionError(error, NotImplementedError): pass class ID3EncryptionUnsupportedError(error, NotImplementedError): pass class ID3JunkFrameError(error, ValueError): pass class unsynch(object): @staticmethod def decode(value): fragments = bytearray(value).split(b'\xff') if len(fragments) > 1 and not fragments[-1]: raise ValueError('string ended unsafe') for f in fragments[1:]: if (not f) or (f[0] >= 0xE0): raise ValueError('invalid sync-safe string') if f[0] == 0x00: del f[0] return bytes(bytearray(b'\xff').join(fragments)) @staticmethod def encode(value): fragments = bytearray(value).split(b'\xff') for f in fragments[1:]: if (not f) or (f[0] >= 0xE0) or (f[0] == 0x00): f.insert(0, 0x00) return bytes(bytearray(b'\xff').join(fragments)) 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, b"\x00") if bigendian: bytes_.reverse() return bytes(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, integer_types): while value: if value & mask: return False value >>= 8 elif isinstance(value, bytes): for byte in bytearray(value): if 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, integer_types): while value: numeric_value += (value & mask) << shift value >>= 8 shift += bits elif isinstance(value, bytes): if bigendian: value = reversed(value) for byte in bytearray(value): numeric_value += (byte & mask) << shift shift += bits else: raise TypeError if isinstance(numeric_value, int): self = int.__new__(BitPaddedInt, numeric_value) else: self = long_.__new__(BitPaddedLong, numeric_value) self.bits = bits self.bigendian = bigendian return self if PY3: BitPaddedLong = BitPaddedInt else: class BitPaddedLong(long_, _BitPaddedMixin): pass class ID3BadUnsynchData(error, ValueError): """Deprecated""" class ID3BadCompressedData(error, ValueError): """Deprecated""" class ID3TagError(error, ValueError): """Deprecated""" class ID3Warning(error, UserWarning): """Deprecated""" mutagen-1.31/mutagen/id3/__init__.py0000644000175000017500000010507112574061222017561 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # 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 import errno from struct import unpack, pack, error as StructError import mutagen from mutagen._util import insert_bytes, delete_bytes, DictProxy, enum from mutagen._tags import PaddingInfo from .._compat import chr_, PY3 from ._util import * from ._frames import * from ._specs import * @enum class ID3v1SaveOptions(object): REMOVE = 0 """ID3v1 tags will be removed""" UPDATE = 1 """ID3v1 tags will be updated but not added""" CREATE = 2 """ID3v1 tags will be created and/or updated""" def _fullread(fileobj, size): """Read a certain number of bytes from the source file. Raises ValueError on invalid size input or EOFError/IOError. """ if size < 0: raise ValueError('Requested bytes (%s) less than zero' % size) data = fileobj.read(size) if len(data) != size: raise EOFError("Not enough data to read") return data class ID3Header(object): _V24 = (2, 4, 0) _V23 = (2, 3, 0) _V22 = (2, 2, 0) _V11 = (1, 1) 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)) def __init__(self, fileobj=None): """Raises ID3NoHeaderError, ID3UnsupportedVersionError or error""" if fileobj is None: # for testing self._flags = 0 return fn = getattr(fileobj, "name", "") try: data = _fullread(fileobj, 10) except EOFError: raise ID3NoHeaderError("%s: too small" % fn) id3, vmaj, vrev, flags, size = unpack('>3sBBB4s', data) self._flags = flags self.size = BitPaddedInt(size) + 10 self.version = (2, vmaj, vrev) if id3 != b'ID3': raise ID3NoHeaderError("%r doesn't start with an ID3 tag" % fn) if vmaj not in [2, 3, 4]: raise ID3UnsupportedVersionError("%r ID3v2.%d not supported" % (fn, vmaj)) if not BitPaddedInt.has_valid_padding(size): raise error("Header size not synchsafe") if (self.version >= self._V24) and (flags & 0x0f): raise error( "%r has invalid flags %#02x" % (fn, flags)) elif (self._V23 <= self.version < self._V24) and (flags & 0x1f): raise error( "%r has invalid flags %#02x" % (fn, flags)) if self.f_extended: try: extsize_data = _fullread(fileobj, 4) except EOFError: raise error("%s: too small" % fn) if PY3: frame_id = extsize_data.decode("ascii", "replace") else: frame_id = extsize_data if frame_id 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 extsize = 0 fileobj.seek(-4, 1) 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." extsize = BitPaddedInt(extsize_data) - 4 if not BitPaddedInt.has_valid_padding(extsize_data): raise error( "Extended header size not synchsafe") else: # "Where the 'Extended header size', currently 6 or 10 bytes, # excludes itself." extsize = unpack('>L', extsize_data)[0] try: self._extdata = _fullread(fileobj, extsize) except EOFError: raise error("%s: too small" % fn) 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 """ __module__ = "mutagen.id3" PEDANTIC = True """Deprecated. Doesn't have any effect""" filename = None def __init__(self, *args, **kwargs): self.unknown_frames = [] self.__unknown_version = None self._header = None self._version = (2, 4, 0) super(ID3, self).__init__(*args, **kwargs) @property def version(self): """ID3 tag version as a tuple (of the loaded file)""" if self._header is not None: return self._header.version return self._version @version.setter def version(self, value): self._version = value @property def f_unsynch(self): if self._header is not None: return self._header.f_unsynch return False @property def f_extended(self): if self._header is not None: return self._header.f_extended return False @property def size(self): if self._header is not None: return self._header.size return 0 def _pre_load_header(self, fileobj): # XXX: for aiff to adjust the offset.. pass 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 v2_version not in (3, 4): raise ValueError("Only 3 and 4 possible for v2_version") self.filename = filename self.unknown_frames = [] self.__known_frames = known_frames self._header = None self._padding = 0 # for testing with open(filename, 'rb') as fileobj: self._pre_load_header(fileobj) try: self._header = ID3Header(fileobj) except (ID3NoHeaderError, ID3UnsupportedVersionError): frames, offset = _find_id3v1(fileobj) if frames is None: raise self.version = ID3Header._V11 for v in frames.values(): self.add(v) else: frames = self.__known_frames if frames is None: if self.version >= ID3Header._V23: frames = Frames elif self.version >= ID3Header._V22: frames = Frames_2_2 try: data = _fullread(fileobj, self.size - 10) except (ValueError, EOFError, IOError) as e: raise error(e) 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[:2] 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 list(self.keys()): if k.startswith(key): 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 = sorted(Frame.pprint(s) for s in self.values()) 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 __read_frames(self, data, frames): assert self.version >= ID3Header._V22 if self.version < ID3Header._V24 and self.f_unsynch: try: data = unsynch.decode(data) except ValueError: pass if self.version >= ID3Header._V23: if self.version < ID3Header._V24: bpi = int else: bpi = _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(b'\x00') == b'': return size = bpi(size) framedata = data[10:10 + size] data = data[10 + size:] self._padding = len(data) if size == 0: continue # drop empty frames if PY3: try: name = name.decode('ascii') except UnicodeDecodeError: continue try: # someone writes 2.3 frames with 2.2 names if name[-1] == "\x00": tag = Frames_2_2[name[:-1]] name = tag.__base__.__name__ tag = frames[name] except KeyError: if is_valid_frame_id(name): yield header + framedata else: try: yield tag._fromData(self._header, flags, framedata) except NotImplementedError: yield header + framedata except ID3JunkFrameError: pass elif self.version >= ID3Header._V22: while data: header = data[0:6] try: name, size = unpack('>3s3s', header) except struct.error: return # not enough header size, = struct.unpack('>L', b'\x00' + size) if name.strip(b'\x00') == b'': return framedata = data[6:6 + size] data = data[6 + size:] self._padding = len(data) if size == 0: continue # drop empty frames if PY3: try: name = name.decode('ascii') except UnicodeDecodeError: continue try: tag = frames[name] except KeyError: if is_valid_frame_id(name): yield header + framedata else: try: yield tag._fromData(self._header, 0, framedata) except (ID3EncryptionUnsupportedError, NotImplementedError): yield header + framedata except ID3JunkFrameError: pass def _prepare_data(self, fileobj, start, available, v2_version, v23_sep, pad_func): if v2_version == 3: version = ID3Header._V23 elif v2_version == 4: version = ID3Header._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((b, a) for a, b in enumerate(order)) last = len(order) frames = sorted(self.items(), key=lambda a: (order.get(a[0][:4], last), a[0])) 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[:2]: framedata.extend(data for data in self.unknown_frames if len(data) > 10) needed = sum(map(len, framedata)) + 10 fileobj.seek(0, 2) trailing_size = fileobj.tell() - start info = PaddingInfo(available - needed, trailing_size) new_padding = info._get_padding(pad_func) if new_padding < 0: raise error("invalid padding") new_size = needed + new_padding new_framesize = BitPaddedInt.to_str(new_size - 10, width=4) header = pack('>3sBBB4s', b'ID3', v2_version, 0, 0, new_framesize) data = bytearray(header) for frame in framedata: data += frame assert new_size >= len(data) data += (new_size - len(data)) * b'\x00' assert new_size == len(data) return data def save(self, filename=None, v1=1, v2_version=4, v23_sep='/', padding=None): """Save changes to a file. Args: filename: Filename to save the tag to. If no filename is given, the one most recently loaded is used. v1 (ID3v1SaveOptions): 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 (int): version of ID3v2 tags (3 or 4). v23_sep (str): 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. padding (function): A function taking a PaddingInfo which should return the amount of padding to use. If None (default) will default to something reasonable. 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. The lack of a way to update only an ID3v1 tag is intentional. Can raise id3.error. """ if filename is None: filename = self.filename try: f = open(filename, 'rb+') except IOError as err: from errno import ENOENT if err.errno != ENOENT: raise f = open(filename, 'ab') # create, then reopen f = open(filename, 'rb+') try: try: header = ID3Header(f) except ID3NoHeaderError: old_size = 0 else: old_size = header.size data = self._prepare_data( f, 0, old_size, v2_version, v23_sep, padding) new_size = len(data) if (old_size < new_size): insert_bytes(f, new_size - old_size, old_size) elif (old_size > new_size): delete_bytes(f, old_size - new_size, new_size) f.seek(0) f.write(data) self.__save_v1(f, v1) finally: f.close() def __save_v1(self, f, v1): tag, offset = _find_id3v1(f) has_v1 = tag is not None f.seek(offset, 2) if v1 == ID3v1SaveOptions.UPDATE and has_v1 or \ v1 == ID3v1SaveOptions.CREATE: f.write(MakeID3v1(self)) else: f.truncate() 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=ID3Header._V24, v23_sep=None): flags = 0 if isinstance(frame, TextFrame): if len(str(frame)) == 0: return b'' if version == ID3Header._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 == ID3Header._V24: bits = 7 elif version == ID3Header._V23: bits = 8 else: raise ValueError datasize = BitPaddedInt.to_str(len(framedata), width=4, bits=bits) if name is not None: assert isinstance(name, bytes) frame_name = name else: frame_name = type(frame).__name__ if PY3: frame_name = frame_name.encode("ascii") header = pack('>4s4sH', 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 # ID3v2.2 LNK frames are just way too different to upgrade. for frame in self.getall("LINK"): if len(frame.frameid) != 4: del self[frame.HashKey] mimes = {"PNG": "image/png", "JPG": "image/jpeg"} for pic in self.getall("APIC"): if pic.mime in mimes: newpic = APIC( encoding=pic.encoding, mime=mimes[pic.mime], type=pic.type, desc=pic.desc, data=pic.data) self.add(newpic) 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): # 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]) except struct.error: continue try: frame = BinaryFrame._fromData( self._header, flags, frame[10:]) except (error, NotImplementedError): continue converted.append(self.__save_frame(frame, name=name)) self.unknown_frames[:] = converted self.__unknown_version = (2, 4) # TDAT, TYER, and TIME have been turned into TDRC. try: date = text_type(self.get("TYER", "")) if date.strip(u"\x00"): self.pop("TYER") dat = text_type(self.get("TDAT", "")) if dat.strip("\x00"): self.pop("TDAT") date = "%s-%s-%s" % (date, dat[2:], dat[:2]) time = text_type(self.get("TIME", "")) if time.strip("\x00"): 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 """ with open(filename, 'rb+') as f: if delete_v1: tag, offset = _find_id3v1(f) if tag is not None: f.seek(offset, 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 = b'', -1 insize = BitPaddedInt(insize) if id3 == b'ID3' and insize >= 0: delete_bytes(f, insize + 10, 0) # support open(filename) as interface Open = ID3 def _determine_bpi(data, frames, EMPTY=b"\x00" * 10): """Takes id3v2.4 frame data and determines if ints or bitpaddedints should be used for parsing. Needed because iTunes used to write normal ints for frame sizes. """ # 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 PY3: try: name = name.decode("ascii") except UnicodeDecodeError: continue 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 PY3: try: name = name.decode("ascii") except UnicodeDecodeError: continue 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 _find_id3v1(fileobj): """Returns a tuple of (id3tag, offset_to_end) or (None, 0) offset mainly because we used to write too short tags in some cases and we need the offset to delete them. """ # id3v1 is always at the end (after apev2) extra_read = b"APETAGEX".index(b"TAG") try: fileobj.seek(-128 - extra_read, 2) except IOError as e: if e.errno == errno.EINVAL: # If the file is too small, might be ok since we wrote too small # tags at some point. let's see how the parsing goes.. fileobj.seek(0, 0) else: raise data = fileobj.read(128 + extra_read) try: idx = data.index(b"TAG") except ValueError: return (None, 0) else: # FIXME: make use of the apev2 parser here # if TAG is part of APETAGEX assume this is an APEv2 tag try: ape_idx = data.index(b"APETAGEX") except ValueError: pass else: if idx == ape_idx + extra_read: return (None, 0) tag = ParseID3v1(data[idx:]) if tag is None: return (None, 0) offset = idx - len(data) return (tag, offset) # ID3v1.1 support. def ParseID3v1(data): """Parse an ID3v1 tag, returning a list of ID3v2.4 frames. Returns a {frame_name: frame} dict or None. """ try: data = data[data.index(b"TAG"):] except ValueError: return None if 128 < len(data) or len(data) < 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(data) - 124) try: tag, title, artist, album, year, comment, track, genre = unpack( unpack_fmt, data) except StructError: return None if tag != b"TAG": return None def fix(data): return data.split(b"\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 (data[-3] == b'\x00'[0])): 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 = b"" v1[name] = text + (b"\x00" * (30 - len(text))) if "COMM" in id3: cmnt = id3["COMM"].text[0].encode('latin1', 'replace')[:28] else: cmnt = b"" v1["comment"] = cmnt + (b"\x00" * (29 - len(cmnt))) if "TRCK" in id3: try: v1["track"] = chr_(+id3["TRCK"]) except ValueError: v1["track"] = b"\x00" else: v1["track"] = b"\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"] = b"\xff" if "TDRC" in id3: year = text_type(id3["TDRC"]).encode('ascii') elif "TYER" in id3: year = text_type(id3["TYER"]).encode('ascii') else: year = b"" v1["year"] = (year + b"\x00\x00\x00\x00")[:4] return ( b"TAG" + v1["title"] + v1["artist"] + v1["album"] + v1["year"] + v1["comment"] + v1["track"] + v1["genre"] ) class ID3FileType(mutagen.FileType): """An unknown type of file with ID3 tags.""" ID3 = ID3 class _Info(mutagen.StreamInfo): length = 0 def __init__(self, fileobj, offset): pass @staticmethod def pprint(): return "Unknown format with ID3 tag" @staticmethod def score(filename, fileobj, header_data): return header_data.startswith(b"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 ID3NoHeaderError: self.tags = None if self.tags is not None: try: offset = self.tags.size except AttributeError: offset = None else: offset = None with open(filename, "rb") as fileobj: self.info = self._Info(fileobj, offset) mutagen-1.31/mutagen/id3/_specs.py0000644000175000017500000004161512574061222017301 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # 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 .._compat import text_type, chr_, PY3, swap_to_string, string_types, \ xrange from .._util import total_ordering, decode_terminated, enum, izip from ._util import BitPaddedInt @enum class PictureType(object): """Enumeration of image types defined by the ID3 standard for the APIC frame, but also reused in WMA/FLAC/VorbisComment. """ OTHER = 0 """Other""" FILE_ICON = 1 """32x32 pixels 'file icon' (PNG only)""" OTHER_FILE_ICON = 2 """Other file icon""" COVER_FRONT = 3 """Cover (front)""" COVER_BACK = 4 """Cover (back)""" LEAFLET_PAGE = 5 """Leaflet page""" MEDIA = 6 """Media (e.g. label side of CD)""" LEAD_ARTIST = 7 """Lead artist/lead performer/soloist""" ARTIST = 8 """Artist/performer""" CONDUCTOR = 9 """Conductor""" BAND = 10 """Band/Orchestra""" COMPOSER = 11 """Composer""" LYRICIST = 12 """Lyricist/text writer""" RECORDING_LOCATION = 13 """Recording Location""" DURING_RECORDING = 14 """During recording""" DURING_PERFORMANCE = 15 """During performance""" SCREEN_CAPTURE = 16 """Movie/video screen capture""" FISH = 17 """A bright coloured fish""" ILLUSTRATION = 18 """Illustration""" BAND_LOGOTYPE = 19 """Band/artist logotype""" PUBLISHER_LOGOTYPE = 20 """Publisher/Studio logotype""" class SpecError(Exception): pass 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 def read(self, frame, data): """Returns the (value, left_data) or raises SpecError""" raise NotImplementedError def write(self, frame, value): raise NotImplementedError def validate(self, frame, value): """Returns the validated data or raises ValueError/TypeError""" raise NotImplementedError class ByteSpec(Spec): def read(self, frame, data): return bytearray(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)), b'' 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 @enum class Encoding(object): """Text Encoding""" LATIN1 = 0 """ISO-8859-1""" UTF16 = 1 """UTF-16 with BOM""" UTF16BE = 2 """UTF-16BE without BOM""" UTF8 = 3 """UTF-8""" class EncodingSpec(ByteSpec): def read(self, frame, data): enc, data = super(EncodingSpec, self).read(frame, data) if enc not in (Encoding.LATIN1, Encoding.UTF16, Encoding.UTF16BE, Encoding.UTF8): raise SpecError('Invalid Encoding: %r' % enc) return enc, data def validate(self, frame, value): if value is None: return None if value not in (Encoding.LATIN1, Encoding.UTF16, Encoding.UTF16BE, Encoding.UTF8): raise ValueError('Invalid Encoding: %r' % value) return value def _validate23(self, frame, value, **kwargs): # only 0, 1 are valid in v2.3, default to utf-16 if value not in (Encoding.LATIN1, Encoding.UTF16): value = Encoding.UTF16 return value class StringSpec(Spec): """A fixed size ASCII only payload.""" def __init__(self, name, length): super(StringSpec, self).__init__(name) self.len = length def read(s, frame, data): chunk = data[:s.len] try: ascii = chunk.decode("ascii") except UnicodeDecodeError: raise SpecError("not ascii") else: if PY3: chunk = ascii return chunk, data[s.len:] def write(s, frame, value): if value is None: return b'\x00' * s.len else: if PY3: value = value.encode("ascii") return (bytes(value) + b'\x00' * s.len)[:s.len] def validate(s, frame, value): if value is None: return None if PY3: if not isinstance(value, str): raise TypeError("%s has to be str" % s.name) value.encode("ascii") else: if not isinstance(value, bytes): value = value.encode("ascii") if 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, b'' def write(self, frame, value): if value is None: return b"" if isinstance(value, bytes): return value value = text_type(value).encode("ascii") return value def validate(self, frame, value): if value is None: return None if isinstance(value, bytes): return value elif PY3: raise TypeError("%s has to be bytes" % self.name) value = text_type(value).encode("ascii") return value class EncodedTextSpec(Spec): _encodings = { Encoding.LATIN1: ('latin1', b'\x00'), Encoding.UTF16: ('utf16', b'\x00\x00'), Encoding.UTF16BE: ('utf_16_be', b'\x00\x00'), Encoding.UTF8: ('utf8', b'\x00'), } def read(self, frame, data): enc, term = self._encodings[frame.encoding] try: # allow missing termination return decode_terminated(data, enc, strict=False) except ValueError: # utf-16 termination with missing BOM, or single NULL if not data[:len(term)].strip(b"\x00"): return u"", data[len(term):] # utf-16 data with single NULL, see issue 169 try: return decode_terminated(data + b"\x00", enc) except ValueError: raise SpecError("Decoding error") def write(self, frame, value): enc, term = self._encodings[frame.encoding] return value.encode(enc) + term def validate(self, frame, value): return text_type(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 izip(record, self.specs): data.append(s.write(frame, v)) return b''.join(data) def validate(self, frame, value): if value is None: return [] if self.sep and isinstance(value, string_types): 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 izip(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 izip(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 b'\x00' in data: data, ret = data.split(b'\x00', 1) else: ret = b'' return data.decode('latin1'), ret def write(self, data, value): return value.encode('latin1') + b'\x00' def validate(self, frame, value): return text_type(value) @swap_to_string @total_ordering 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 elif not isinstance(text, text_type): if PY3: raise TypeError("not a str") text = text.decode("utf-8") 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(parts): if part is None: break 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 __bytes__(self): return self.text.encode("utf-8") def __repr__(self): return repr(self.text) def __eq__(self, other): return self.text == other.text def __lt__(self, other): return 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) = xrange(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): number = int(round(value * 512)) # pack only fails in 2.7, do it manually in 2.6 if not -32768 <= number <= 32767: raise SpecError("not in range") return pack('>h', number) def validate(self, frame, value): if value is not None: try: self.write(frame, value) except SpecError: 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 data_array = bytearray(data) bits = data_array[0] vol_bytes = min(4, (bits + 7) >> 3) # not enough frame data if vol_bytes + 1 > len(data): raise SpecError("not enough frame data") shift = ((8 - (bits & 7)) & 7) + (4 - vol_bytes) * 8 for i in xrange(1, vol_bytes + 1): peak *= 256 peak += data_array[i] peak *= 2 ** shift return (float(peak) / (2 ** 31 - 1)), data[1 + vol_bytes:] def write(self, frame, value): number = int(round(value * 32768)) # pack only fails in 2.7, do it manually in 2.6 if not 0 <= number <= 65535: raise SpecError("not in range") # always write as 16 bits for sanity. return b"\x10" + pack('>H', number) def validate(self, frame, value): if value is not None: try: self.write(frame, value) except SpecError: raise ValueError("out of range") return value class SynchronizedTextSpec(EncodedTextSpec): def read(self, frame, data): texts = [] encoding, term = self._encodings[frame.encoding] while data: try: value, data = decode_terminated(data, encoding) except ValueError: raise SpecError("decoding error") if len(data) < 4: raise SpecError("not enough data") time, = struct.unpack(">I", data[:4]) texts.append((value, time)) data = data[4:] return texts, b"" def write(self, frame, value): data = [] encoding, term = self._encodings[frame.encoding] for text, time in value: text = text.encode(encoding) + term data.append(text + struct.pack(">I", time)) return b"".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 b"".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 = sorted(adjustments.items()) return adjustments, data def write(self, frame, value): value.sort() return b"".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: raise SpecError("invalid bit count in ASPI (%d)" % frame.b) indexes = data[:frame.N * size] data = data[frame.N * size:] try: return list(struct.unpack(">" + format * frame.N, indexes)), data except struct.error as e: raise SpecError(e) def write(self, frame, values): if frame.b == 16: format = "H" elif frame.b == 8: format = "B" else: raise SpecError("frame.b must be 8 or 16") try: return struct.pack(">" + format * frame.N, *values) except struct.error as e: raise SpecError(e) def validate(self, frame, values): return values mutagen-1.31/mutagen/id3/_frames.py0000644000175000017500000012276212574061222017444 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # 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 zlib from struct import unpack from ._util import ID3JunkFrameError, ID3EncryptionUnsupportedError, unsynch from ._specs import ( BinaryDataSpec, StringSpec, Latin1TextSpec, EncodedTextSpec, ByteSpec, EncodingSpec, ASPIIndexSpec, SizedIntegerSpec, IntegerSpec, VolumeAdjustmentsSpec, VolumePeakSpec, VolumeAdjustmentSpec, ChannelSpec, MultiSpec, SynchronizedTextSpec, KeyEventSpec, TimeStampSpec, EncodedNumericPartTextSpec, EncodedNumericTextSpec, SpecError) from .._compat import text_type, string_types, swap_to_string, iteritems, izip def is_valid_frame_id(frame_id): return frame_id.isalnum() and frame_id.isupper() def _bytes2key(b): assert isinstance(b, bytes) return b.decode("latin1") 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] # ask the sub class to fill in our data other._to_other(self) else: for checker, val in izip(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: raise ValueError("%s: %s" % (checker.name, e)) setattr(self, checker.name, validated) def _to_other(self, other): # this impl covers subclasses with the same framespec if other._framespec is not self._framespec: raise ValueError for checker in other._framespec: setattr(other, checker.name, getattr(self, checker.name)) 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: # so repr works during __init__ if hasattr(self, attr.name): kw.append('%s=%r' % (attr.name, getattr(self, attr.name))) return '%s(%s)' % (type(self).__name__, ', '.join(kw)) def _readData(self, data): """Raises ID3JunkFrameError; Returns leftover data""" for reader in self._framespec: if len(data): try: value, data = reader.read(self, data) except SpecError as e: raise ID3JunkFrameError(e) else: raise ID3JunkFrameError("no data left") setattr(self, reader.name, value) return data def _writeData(self): data = [] for writer in self._framespec: data.append(writer.write(self, getattr(self, writer.name))) return b''.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. Raises: ID3JunkFrameError in case parsing failed NotImplementedError in case parsing isn't implemented ID3EncryptionUnsupportedError in case the frame is encrypted. """ if id3.version >= id3._V24: 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: # Some things write synch-unsafe data with either the frame # or global unsynch flag set. Try to load them as is. # https://bitbucket.org/lazka/mutagen/issue/210 # https://bitbucket.org/lazka/mutagen/issue/223 pass if tflags & Frame.FLAG24_ENCRYPT: raise ID3EncryptionUnsupportedError if tflags & Frame.FLAG24_COMPRESS: try: data = zlib.decompress(data) except zlib.error as 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 = zlib.decompress(data) except zlib.error as err: raise ID3JunkFrameError( 'zlib: %s: %r' % (err, data)) elif id3.version >= id3._V23: 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 = zlib.decompress(data) except zlib.error as err: raise ID3JunkFrameError('zlib: %s: %r' % (err, data)) frame = cls() 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 _to_other(self, other): super(FrameOpt, self)._to_other(other) # this impl covers subclasses with the same optionalspec if other._optionalspec is not self._optionalspec: raise ValueError for checker in other._optionalspec: if hasattr(self, checker.name): setattr(other, checker.name, getattr(self, checker.name)) def _readData(self, data): """Raises ID3JunkFrameError; Returns leftover data""" for reader in self._framespec: if len(data): try: value, data = reader.read(self, data) except SpecError as e: raise ID3JunkFrameError(e) else: raise ID3JunkFrameError("no data left") setattr(self, reader.name, value) if data: for reader in self._optionalspec: if len(data): try: value, data = reader.read(self, data) except SpecError as e: raise ID3JunkFrameError(e) else: break setattr(self, reader.name, value) return data 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 b''.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)) @swap_to_string 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 __bytes__(self): return text_type(self).encode('utf-8') def __str__(self): return u'\u0000'.join(self.text) def __eq__(self, other): if isinstance(other, bytes): return bytes(self) == other elif isinstance(other, text_type): return text_type(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]) @swap_to_string 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 __bytes__(self): return text_type(self).encode('utf-8') def __str__(self): return u','.join([stamp.text for stamp in self.text]) def _pprint(self): return u" / ".join([stamp.text for stamp in self.text]) @swap_to_string 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 __bytes__(self): return self.url.encode('utf-8') def __str__(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 = text_type(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, string_types): genres = [genres] self.text = [self.__decode(g) for g in genres] def __decode(self, value): if isinstance(value, bytes): 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__ @swap_to_string 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:%s' % (self.FrameID, self.desc, self.lang) def __bytes__(self): return self.text.encode('utf-8') def __str__(self): return self.text def __eq__(self, other): return self.text == other __hash__ = Frame.__hash__ @swap_to_string 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:%s' % (self.FrameID, self.desc, self.lang) def __eq__(self, other): return str(self) == other __hash__ = Frame.__hash__ def __str__(self): return u"".join(text for (text, time) in self.text) def __bytes__(self): return text_type(self).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:%s' % (self.FrameID, self.desc, self.lang) def _pprint(self): return "%s=%s=%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 _validate_from_22(self, other, checker): if checker.name == "mime": self.mime = other.mime.decode("ascii", "ignore") else: super(APIC, self)._validate_from_22(other, checker) 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 text_type(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 @swap_to_string 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 __bytes__(self): return self.owner.encode('utf-8') def __str__(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:%s" % ( self.FrameID, self.frameid, self.url, _bytes2key(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): return "%s=%r" % (self.owner, self.data) @swap_to_string 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:%s' % (self.FrameID, self.lang) def __bytes__(self): return self.text.encode('utf-8') def __str__(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) @swap_to_string class OWNE(Frame): """Ownership frame.""" _framespec = [ EncodingSpec('encoding'), Latin1TextSpec('price'), StringSpec('date', 8), EncodedTextSpec('seller'), ] def __bytes__(self): return self.seller.encode('utf-8') def __str__(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, _bytes2key(self._writeData())) def __eq__(self, other): return self._writeData() == other._writeData() __hash__ = FrameOpt.__hash__ @swap_to_string 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 __bytes__(self): return self.data def __eq__(self, other): return self.data == other __hash__ = Frame.__hash__ @swap_to_string 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 __bytes__(self): return self.owner.encode('utf-8') def __str__(self): return self.owner def __eq__(self, other): return self.owner == other or self.group == other __hash__ = FrameOpt.__hash__ @swap_to_string class PRIV(Frame): """Private frame.""" _framespec = [ Latin1TextSpec('owner'), BinaryDataSpec('data'), ] @property def HashKey(self): return '%s:%s:%s' % ( self.FrameID, self.owner, _bytes2key(self.data)) def __bytes__(self): return self.data def __eq__(self, other): return self.data == other def _pprint(self): return "%s=%r" % (self.owner, self.data) __hash__ = Frame.__hash__ @swap_to_string class SIGN(Frame): """Signature frame.""" _framespec = [ ByteSpec('group'), BinaryDataSpec('sig'), ] @property def HashKey(self): return '%s:%s:%s' % (self.FrameID, self.group, _bytes2key(self.sig)) def __bytes__(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__ # 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') ] def _to_other(self, other): if not isinstance(other, APIC): raise TypeError other.encoding = self.encoding other.mime = self.mime other.type = self.type other.desc = self.desc other.data = self.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')] def _to_other(self, other): if not isinstance(other, LINK): raise TypeError other.frameid = self.frameid other.url = self.url if hasattr(self, "data"): other.data = self.data Frames = {} """All supported ID3v2.3/4 frames, keyed by frame name.""" Frames_2_2 = {} """All supported ID3v2.2 frames, keyed by frame name.""" k, v = None, None for k, v in iteritems(globals()): if isinstance(v, type) and issubclass(v, Frame): v.__module__ = "mutagen.id3" if len(k) == 3: Frames_2_2[k] = v elif len(k) == 4: Frames[k] = v try: del k del v except NameError: pass mutagen-1.31/mutagen/flac.py0000644000175000017500000007167612574061222016265 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 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 ._vorbis import VCommentDict import mutagen from ._compat import cBytesIO, endswith, chr_, xrange from mutagen._util import resize_bytes, MutagenError, get_size from mutagen._tags import PaddingInfo from mutagen.id3 import BitPaddedInt from functools import reduce class error(IOError, MutagenError): pass class FLACNoHeaderError(error): pass class FLACVorbisError(ValueError, error): pass def to_int_be(data): """Convert an arbitrarily-long string to a long using big-endian byte order.""" return reduce(lambda a, b: (a << 8) + b, bytearray(data), 0) 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 """For block types setting this, we don't trust the size field and use the size of the content instead.""" _invalid_overflow_size = -1 """In case the real size was bigger than what is representable by the 24 bit size field, we save the wrong specified size here. This can only be set if _distrust_size is True""" _MAX_SIZE = 2 ** 24 - 1 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, bytes): data = cBytesIO(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 @classmethod def _writeblock(cls, block, is_last=False): """Returns the block content + header. Raises error. """ data = bytearray() code = (block.code | 128) if is_last else block.code datum = block.write() size = len(datum) if size > cls._MAX_SIZE: if block._distrust_size and block._invalid_overflow_size != -1: # The original size of this block was (1) wrong and (2) # the real size doesn't allow us to save the file # according to the spec (too big for 24 bit uint). Instead # simply write back the original wrong size.. at least # we don't make the file more "broken" as it is. size = block._invalid_overflow_size else: raise error("block is too long to write") assert not size > cls._MAX_SIZE length = struct.pack(">I", size)[-3:] data.append(code) data += length data += datum return data @classmethod def _writeblocks(cls, blocks, available, cont_size, padding_func): """Render metadata block as a byte string.""" # write everything except padding data = bytearray() for block in blocks: if isinstance(block, Padding): continue data += cls._writeblock(block) blockssize = len(data) # take the padding overhead into account. we always add one # to make things simple. padding_block = Padding() blockssize += len(cls._writeblock(padding_block)) # finally add a padding block info = PaddingInfo(available - blockssize, cont_size) padding_block.length = min(info._get_padding(padding_func), cls._MAX_SIZE) data += cls._writeblock(padding_block, is_last=True) return data class StreamInfo(MetadataBlock, mutagen.StreamInfo): """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 & 0xFFFFFFFFF self.length = self.total_samples / float(self.sample_rate) self.md5_signature = to_int_be(data.read(16)) def write(self): f = cBytesIO() 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 & 0xFFFFFFFF)) # MD5 signature sig = self.md5_signature f.write(struct.pack( ">4I", (sig >> 96) & 0xFFFFFFFF, (sig >> 64) & 0xFFFFFFFF, (sig >> 32) & 0xFFFFFFFF, sig & 0xFFFFFFFF)) return f.getvalue() def pprint(self): return u"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 = cBytesIO() 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 = b'' 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(b'\0') self.lead_in_samples = lead_in_samples self.compact_disc = bool(flags & 0x80) self.tracks = [] for i in xrange(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(b'\0') type_ = (flags & 0x80) >> 7 pre_emphasis = bool(flags & 0x40) val = CueSheetTrack( track_number, start_offset, isrc, type_, pre_emphasis) for j in xrange(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 = cBytesIO() 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 To create a picture from file (in order to add to a FLAC file), instantiate this object without passing anything to the constructor and then set the properties manually:: p = Picture() with open("Folder.jpg", "rb") as f: pic.data = f.read() pic.type = id3.PictureType.COVER_FRONT pic.mime = u"image/jpeg" pic.width = 500 pic.height = 500 pic.depth = 16 # color depth """ 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 = b'' 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 = cBytesIO() 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. """ code = 1 def __init__(self, data=b""): super(Padding, self).__init__(data) def load(self, data): self.length = len(data.read()) def write(self): try: return b"\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(mutagen.FileType): """A FLAC audio file. Attributes: * cuesheet -- CueSheet object, if any * seektable -- SeekTable object, if any * pictures -- list of embedded pictures """ _mimes = ["audio/x-flac", "application/x-flac"] info = None """A `StreamInfo`""" tags = None """A `VCommentDict`""" METADATA_BLOCKS = [StreamInfo, Padding, None, SeekTable, VCFLACDict, CueSheet, Picture] """Known metadata block types, indexed by ID.""" @staticmethod def score(filename, fileobj, header_data): return (header_data.startswith(b"fLaC") + endswith(filename.lower(), ".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 start = fileobj.tell() block = block_type(fileobj) real_size = fileobj.tell() - start if real_size > MetadataBlock._MAX_SIZE: block._invalid_overflow_size = size 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 if self.tags is not None: self.metadata_blocks.remove(self.tags) self.save(padding=lambda x: 0) self.metadata_blocks.append(self.tags) self.tags.clear() 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.""" blocks = [b for b in self.metadata_blocks if b.code != Picture.code] self.metadata_blocks = blocks @property def pictures(self): """List of embedded pictures""" return [b for b in self.metadata_blocks if b.code == Picture.code] def save(self, filename=None, deleteid3=False, padding=None): """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 with open(filename, 'rb+') as f: header = self.__check_header(f) audio_offset = self.__find_audio_offset(f) # "fLaC" and maybe ID3 available = audio_offset - header # Delete ID3v2 if deleteid3 and header > 4: available += header - 4 header = 4 content_size = get_size(f) - audio_offset assert content_size >= 0 data = MetadataBlock._writeblocks( self.metadata_blocks, available, content_size, padding) data_size = len(data) resize_bytes(f, available, data_size, header) f.seek(header - 4) f.write(b"fLaC") f.write(data) # Delete ID3v1 if deleteid3: try: f.seek(-128, 2) except IOError: pass else: if f.read(3) == b"TAG": f.seek(-128, 2) f.truncate() 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): """Returns the offset of the flac block start (skipping id3 tags if found). The passed fileobj will be advanced to that offset as well. """ size = 4 header = fileobj.read(4) if header != b"fLaC": size = None if header[:3] == b"ID3": size = 14 + BitPaddedInt(fileobj.read(6)[2:]) fileobj.seek(size - 4) if fileobj.read(4) != b"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.31/mutagen/trueaudio.py0000644000175000017500000000425012574061222017341 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 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 ._compat import endswith from mutagen import StreamInfo from mutagen.id3 import ID3FileType, delete from mutagen._util import cdata, MutagenError class error(RuntimeError, MutagenError): pass class TrueAudioHeaderError(error, IOError): pass class TrueAudioInfo(StreamInfo): """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(b"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 u"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(b"ID3") + header.startswith(b"TTA") + endswith(filename.lower(), b".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.31/mutagen/mp4/0000755000175000017500000000000012574256060015473 5ustar lazkalazka00000000000000mutagen-1.31/mutagen/mp4/_util.py0000644000175000017500000000113112574061222017147 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2014 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. from mutagen._util import cdata def parse_full_atom(data): """Some atoms are versioned. Split them up in (version, flags, payload). Can raise ValueError. """ if len(data) < 4: raise ValueError("not enough data") version = ord(data[0:1]) flags = cdata.uint_be(b"\x00" + data[1:4]) return version, flags, data[4:] mutagen-1.31/mutagen/mp4/_as_entry.py0000644000175000017500000004061012574061222020023 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2014 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. from mutagen._compat import cBytesIO, xrange from mutagen.aac import ProgramConfigElement from mutagen._util import BitReader, BitReaderError, cdata from mutagen._compat import text_type from ._util import parse_full_atom from ._atom import Atom, AtomError class ASEntryError(Exception): pass class AudioSampleEntry(object): """Parses an AudioSampleEntry atom. Private API. Attrs: channels (int): number of channels sample_size (int): sample size in bits sample_rate (int): sample rate in Hz bitrate (int): bits per second (0 means unknown) codec (string): audio codec, either 'mp4a[.*][.*]' (rfc6381) or 'alac' codec_description (string): descriptive codec name e.g. "AAC LC+SBR" Can raise ASEntryError. """ channels = 0 sample_size = 0 sample_rate = 0 bitrate = 0 codec = None codec_description = None def __init__(self, atom, fileobj): ok, data = atom.read(fileobj) if not ok: raise ASEntryError("too short %r atom" % atom.name) fileobj = cBytesIO(data) r = BitReader(fileobj) try: # SampleEntry r.skip(6 * 8) # reserved r.skip(2 * 8) # data_ref_index # AudioSampleEntry r.skip(8 * 8) # reserved self.channels = r.bits(16) self.sample_size = r.bits(16) r.skip(2 * 8) # pre_defined r.skip(2 * 8) # reserved self.sample_rate = r.bits(32) >> 16 except BitReaderError as e: raise ASEntryError(e) assert r.is_aligned() try: extra = Atom(fileobj) except AtomError as e: raise ASEntryError(e) self.codec = atom.name.decode("latin-1") self.codec_description = None if atom.name == b"mp4a" and extra.name == b"esds": self._parse_esds(extra, fileobj) elif atom.name == b"alac" and extra.name == b"alac": self._parse_alac(extra, fileobj) elif atom.name == b"ac-3" and extra.name == b"dac3": self._parse_dac3(extra, fileobj) if self.codec_description is None: self.codec_description = self.codec.upper() def _parse_dac3(self, atom, fileobj): # ETSI TS 102 366 assert atom.name == b"dac3" ok, data = atom.read(fileobj) if not ok: raise ASEntryError("truncated %s atom" % atom.name) fileobj = cBytesIO(data) r = BitReader(fileobj) # sample_rate in AudioSampleEntry covers values in # fscod2 and not just fscod, so ignore fscod here. try: r.skip(2 + 5 + 3) # fscod, bsid, bsmod acmod = r.bits(3) lfeon = r.bits(1) bit_rate_code = r.bits(5) r.skip(5) # reserved except BitReaderError as e: raise ASEntryError(e) self.channels = [2, 1, 2, 3, 3, 4, 4, 5][acmod] + lfeon try: self.bitrate = [ 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640][bit_rate_code] * 1000 except IndexError: pass def _parse_alac(self, atom, fileobj): # https://alac.macosforge.org/trac/browser/trunk/ # ALACMagicCookieDescription.txt assert atom.name == b"alac" ok, data = atom.read(fileobj) if not ok: raise ASEntryError("truncated %s atom" % atom.name) try: version, flags, data = parse_full_atom(data) except ValueError as e: raise ASEntryError(e) if version != 0: raise ASEntryError("Unsupported version %d" % version) fileobj = cBytesIO(data) r = BitReader(fileobj) try: # for some files the AudioSampleEntry values default to 44100/2chan # and the real info is in the alac cookie, so prefer it r.skip(32) # frameLength compatibleVersion = r.bits(8) if compatibleVersion != 0: return self.sample_size = r.bits(8) r.skip(8 + 8 + 8) self.channels = r.bits(8) r.skip(16 + 32) self.bitrate = r.bits(32) self.sample_rate = r.bits(32) except BitReaderError as e: raise ASEntryError(e) def _parse_esds(self, esds, fileobj): assert esds.name == b"esds" ok, data = esds.read(fileobj) if not ok: raise ASEntryError("truncated %s atom" % esds.name) try: version, flags, data = parse_full_atom(data) except ValueError as e: raise ASEntryError(e) if version != 0: raise ASEntryError("Unsupported version %d" % version) fileobj = cBytesIO(data) r = BitReader(fileobj) try: tag = r.bits(8) if tag != ES_Descriptor.TAG: raise ASEntryError("unexpected descriptor: %d" % tag) assert r.is_aligned() except BitReaderError as e: raise ASEntryError(e) try: decSpecificInfo = ES_Descriptor.parse(fileobj) except DescriptorError as e: raise ASEntryError(e) dec_conf_desc = decSpecificInfo.decConfigDescr self.bitrate = dec_conf_desc.avgBitrate self.codec += dec_conf_desc.codec_param self.codec_description = dec_conf_desc.codec_desc decSpecificInfo = dec_conf_desc.decSpecificInfo if decSpecificInfo is not None: if decSpecificInfo.channels != 0: self.channels = decSpecificInfo.channels if decSpecificInfo.sample_rate != 0: self.sample_rate = decSpecificInfo.sample_rate class DescriptorError(Exception): pass class BaseDescriptor(object): TAG = None @classmethod def _parse_desc_length_file(cls, fileobj): """May raise ValueError""" value = 0 for i in xrange(4): try: b = cdata.uint8(fileobj.read(1)) except cdata.error as e: raise ValueError(e) value = (value << 7) | (b & 0x7f) if not b >> 7: break else: raise ValueError("invalid descriptor length") return value @classmethod def parse(cls, fileobj): """Returns a parsed instance of the called type. The file position is right after the descriptor after this returns. Raises DescriptorError """ try: length = cls._parse_desc_length_file(fileobj) except ValueError as e: raise DescriptorError(e) pos = fileobj.tell() instance = cls(fileobj, length) left = length - (fileobj.tell() - pos) if left < 0: raise DescriptorError("descriptor parsing read too much data") fileobj.seek(left, 1) return instance class ES_Descriptor(BaseDescriptor): TAG = 0x3 def __init__(self, fileobj, length): """Raises DescriptorError""" r = BitReader(fileobj) try: self.ES_ID = r.bits(16) self.streamDependenceFlag = r.bits(1) self.URL_Flag = r.bits(1) self.OCRstreamFlag = r.bits(1) self.streamPriority = r.bits(5) if self.streamDependenceFlag: self.dependsOn_ES_ID = r.bits(16) if self.URL_Flag: URLlength = r.bits(8) self.URLstring = r.bytes(URLlength) if self.OCRstreamFlag: self.OCR_ES_Id = r.bits(16) tag = r.bits(8) except BitReaderError as e: raise DescriptorError(e) if tag != DecoderConfigDescriptor.TAG: raise DescriptorError("unexpected DecoderConfigDescrTag %d" % tag) assert r.is_aligned() self.decConfigDescr = DecoderConfigDescriptor.parse(fileobj) class DecoderConfigDescriptor(BaseDescriptor): TAG = 0x4 decSpecificInfo = None """A DecoderSpecificInfo, optional""" def __init__(self, fileobj, length): """Raises DescriptorError""" r = BitReader(fileobj) try: self.objectTypeIndication = r.bits(8) self.streamType = r.bits(6) self.upStream = r.bits(1) self.reserved = r.bits(1) self.bufferSizeDB = r.bits(24) self.maxBitrate = r.bits(32) self.avgBitrate = r.bits(32) if (self.objectTypeIndication, self.streamType) != (0x40, 0x5): return # all from here is optional if length * 8 == r.get_position(): return tag = r.bits(8) except BitReaderError as e: raise DescriptorError(e) if tag == DecoderSpecificInfo.TAG: assert r.is_aligned() self.decSpecificInfo = DecoderSpecificInfo.parse(fileobj) @property def codec_param(self): """string""" param = u".%X" % self.objectTypeIndication info = self.decSpecificInfo if info is not None: param += u".%d" % info.audioObjectType return param @property def codec_desc(self): """string or None""" info = self.decSpecificInfo desc = None if info is not None: desc = info.description return desc class DecoderSpecificInfo(BaseDescriptor): TAG = 0x5 _TYPE_NAMES = [ None, "AAC MAIN", "AAC LC", "AAC SSR", "AAC LTP", "SBR", "AAC scalable", "TwinVQ", "CELP", "HVXC", None, None, "TTSI", "Main synthetic", "Wavetable synthesis", "General MIDI", "Algorithmic Synthesis and Audio FX", "ER AAC LC", None, "ER AAC LTP", "ER AAC scalable", "ER Twin VQ", "ER BSAC", "ER AAC LD", "ER CELP", "ER HVXC", "ER HILN", "ER Parametric", "SSC", "PS", "MPEG Surround", None, "Layer-1", "Layer-2", "Layer-3", "DST", "ALS", "SLS", "SLS non-core", "ER AAC ELD", "SMR Simple", "SMR Main", "USAC", "SAOC", "LD MPEG Surround", "USAC" ] _FREQS = [ 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350, ] @property def description(self): """string or None if unknown""" name = None try: name = self._TYPE_NAMES[self.audioObjectType] except IndexError: pass if name is None: return if self.sbrPresentFlag == 1: name += "+SBR" if self.psPresentFlag == 1: name += "+PS" return text_type(name) @property def sample_rate(self): """0 means unknown""" if self.sbrPresentFlag == 1: return self.extensionSamplingFrequency elif self.sbrPresentFlag == 0: return self.samplingFrequency else: # these are all types that support SBR aot_can_sbr = (1, 2, 3, 4, 6, 17, 19, 20, 22) if self.audioObjectType not in aot_can_sbr: return self.samplingFrequency # there shouldn't be SBR for > 48KHz if self.samplingFrequency > 24000: return self.samplingFrequency # either samplingFrequency or samplingFrequency * 2 return 0 @property def channels(self): """channel count or 0 for unknown""" # from ProgramConfigElement() if hasattr(self, "pce_channels"): return self.pce_channels conf = getattr( self, "extensionChannelConfiguration", self.channelConfiguration) if conf == 1: if self.psPresentFlag == -1: return 0 elif self.psPresentFlag == 1: return 2 else: return 1 elif conf == 7: return 8 elif conf > 7: return 0 else: return conf def _get_audio_object_type(self, r): """Raises BitReaderError""" audioObjectType = r.bits(5) if audioObjectType == 31: audioObjectTypeExt = r.bits(6) audioObjectType = 32 + audioObjectTypeExt return audioObjectType def _get_sampling_freq(self, r): """Raises BitReaderError""" samplingFrequencyIndex = r.bits(4) if samplingFrequencyIndex == 0xf: samplingFrequency = r.bits(24) else: try: samplingFrequency = self._FREQS[samplingFrequencyIndex] except IndexError: samplingFrequency = 0 return samplingFrequency def __init__(self, fileobj, length): """Raises DescriptorError""" r = BitReader(fileobj) try: self._parse(r, length) except BitReaderError as e: raise DescriptorError(e) def _parse(self, r, length): """Raises BitReaderError""" def bits_left(): return length * 8 - r.get_position() self.audioObjectType = self._get_audio_object_type(r) self.samplingFrequency = self._get_sampling_freq(r) self.channelConfiguration = r.bits(4) self.sbrPresentFlag = -1 self.psPresentFlag = -1 if self.audioObjectType in (5, 29): self.extensionAudioObjectType = 5 self.sbrPresentFlag = 1 if self.audioObjectType == 29: self.psPresentFlag = 1 self.extensionSamplingFrequency = self._get_sampling_freq(r) self.audioObjectType = self._get_audio_object_type(r) if self.audioObjectType == 22: self.extensionChannelConfiguration = r.bits(4) else: self.extensionAudioObjectType = 0 if self.audioObjectType in (1, 2, 3, 4, 6, 7, 17, 19, 20, 21, 22, 23): try: GASpecificConfig(r, self) except NotImplementedError: # unsupported, (warn?) return else: # unsupported return if self.audioObjectType in ( 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 39): epConfig = r.bits(2) if epConfig in (2, 3): # unsupported return if self.extensionAudioObjectType != 5 and bits_left() >= 16: syncExtensionType = r.bits(11) if syncExtensionType == 0x2b7: self.extensionAudioObjectType = self._get_audio_object_type(r) if self.extensionAudioObjectType == 5: self.sbrPresentFlag = r.bits(1) if self.sbrPresentFlag == 1: self.extensionSamplingFrequency = \ self._get_sampling_freq(r) if bits_left() >= 12: syncExtensionType = r.bits(11) if syncExtensionType == 0x548: self.psPresentFlag = r.bits(1) if self.extensionAudioObjectType == 22: self.sbrPresentFlag = r.bits(1) if self.sbrPresentFlag == 1: self.extensionSamplingFrequency = \ self._get_sampling_freq(r) self.extensionChannelConfiguration = r.bits(4) def GASpecificConfig(r, info): """Reads GASpecificConfig which is needed to get the data after that (there is no length defined to skip it) and to read program_config_element which can contain channel counts. May raise BitReaderError on error or NotImplementedError if some reserved data was set. """ assert isinstance(info, DecoderSpecificInfo) r.skip(1) # frameLengthFlag dependsOnCoreCoder = r.bits(1) if dependsOnCoreCoder: r.skip(14) extensionFlag = r.bits(1) if not info.channelConfiguration: pce = ProgramConfigElement(r) info.pce_channels = pce.channels if info.audioObjectType == 6 or info.audioObjectType == 20: r.skip(3) if extensionFlag: if info.audioObjectType == 22: r.skip(5 + 11) if info.audioObjectType in (17, 19, 20, 23): r.skip(1 + 1 + 1) extensionFlag3 = r.bits(1) if extensionFlag3 != 0: raise NotImplementedError("extensionFlag3 set") mutagen-1.31/mutagen/mp4/_atom.py0000644000175000017500000001424612574061222017145 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 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. import struct from mutagen._compat import PY2 # This is not an exhaustive list of container atoms, but just the # ones this module needs to peek inside. _CONTAINERS = [b"moov", b"udta", b"trak", b"mdia", b"meta", b"ilst", b"stbl", b"minf", b"moof", b"traf"] _SKIP_SIZE = {b"meta": 4} class AtomError(Exception): pass 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 datalength = -- length of this atom without length, 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): """May raise AtomError""" self.offset = fileobj.tell() try: self.length, self.name = struct.unpack(">I4s", fileobj.read(8)) except struct.error: raise AtomError("truncated data") self._dataoffset = self.offset + 8 if self.length == 1: try: self.length, = struct.unpack(">Q", fileobj.read(8)) except struct.error: raise AtomError("truncated data") self._dataoffset += 8 if self.length < 16: raise AtomError( "64 bit atom length can only be 16 and higher") elif self.length == 0: if level != 0: raise AtomError( "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 AtomError( "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) @property def datalength(self): return self.length - (self._dataoffset - self.offset) def read(self, fileobj): """Return if all data could be read and the atom payload""" fileobj.seek(self._dataoffset, 0) data = fileobj.read(self.datalength) return len(data) == self.datalength, data @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): cls = self.__class__.__name__ if self.children is None: return "<%s name=%r length=%r offset=%r>" % ( cls, 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>" % ( cls, 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 PY2: if isinstance(names, basestring): names = names.split(b".") else: if isinstance(names, bytes): names = names.split(b".") for child in self.atoms: if child.name == names[0]: return child[names[1:]] else: raise KeyError("%r not found" % names[0]) def __repr__(self): return "\n".join([repr(child) for child in self.atoms]) mutagen-1.31/mutagen/mp4/__init__.py0000644000175000017500000010133012574061222017574 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 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, StreamInfo, PaddingInfo from mutagen._constants import GENRES from mutagen._util import (cdata, insert_bytes, DictProxy, MutagenError, hashable, enum, get_size, resize_bytes) from mutagen._compat import (reraise, PY2, string_types, text_type, chr_, iteritems, PY3, cBytesIO, izip, xrange) from ._atom import Atoms, Atom, AtomError from ._util import parse_full_atom from ._as_entry import AudioSampleEntry, ASEntryError class error(IOError, MutagenError): pass class MP4MetadataError(error): pass class MP4StreamInfoError(error): pass class MP4MetadataValueError(ValueError, MP4MetadataError): pass __all__ = ['MP4', 'Open', 'delete', 'MP4Cover', 'MP4FreeForm', 'AtomDataType'] @enum class AtomDataType(object): """Enum for `dataformat` attribute of MP4FreeForm. .. versionadded:: 1.25 """ IMPLICIT = 0 """for use with tags for which no type needs to be indicated because only one type is allowed""" UTF8 = 1 """without any count or null terminator""" UTF16 = 2 """also known as UTF-16BE""" SJIS = 3 """deprecated unless it is needed for special Japanese characters""" HTML = 6 """the HTML file header specifies which HTML version""" XML = 7 """the XML header must identify the DTD or schemas""" UUID = 8 """also known as GUID; stored as 16 bytes in binary (valid as an ID)""" ISRC = 9 """stored as UTF-8 text (valid as an ID)""" MI3P = 10 """stored as UTF-8 text (valid as an ID)""" GIF = 12 """(deprecated) a GIF image""" JPEG = 13 """a JPEG image""" PNG = 14 """PNG image""" URL = 15 """absolute, in UTF-8 characters""" DURATION = 16 """in milliseconds, 32-bit integer""" DATETIME = 17 """in UTC, counting seconds since midnight, January 1, 1904; 32 or 64-bits""" GENRES = 18 """a list of enumerated values""" INTEGER = 21 """a signed big-endian integer with length one of { 1,2,3,4,8 } bytes""" RIAA_PA = 24 """RIAA parental advisory; { -1=no, 1=yes, 0=unspecified }, 8-bit ingteger""" UPC = 25 """Universal Product Code, in text UTF-8 format (valid as an ID)""" BMP = 27 """Windows bitmap image""" @hashable class MP4Cover(bytes): """A cover artwork. Attributes: * imageformat -- format of the image (either FORMAT_JPEG or FORMAT_PNG) """ FORMAT_JPEG = AtomDataType.JPEG FORMAT_PNG = AtomDataType.PNG def __new__(cls, data, *args, **kwargs): return bytes.__new__(cls, data) def __init__(self, data, imageformat=FORMAT_JPEG): self.imageformat = imageformat __hash__ = bytes.__hash__ def __eq__(self, other): if not isinstance(other, MP4Cover): return bytes(self) == other return (bytes(self) == bytes(other) and self.imageformat == other.imageformat) def __ne__(self, other): return not self.__eq__(other) def __repr__(self): return "%s(%r, %r)" % ( type(self).__name__, bytes(self), AtomDataType(self.imageformat)) @hashable class MP4FreeForm(bytes): """A freeform value. Attributes: * dataformat -- format of the data (see AtomDataType) """ FORMAT_DATA = AtomDataType.IMPLICIT # deprecated FORMAT_TEXT = AtomDataType.UTF8 # deprecated def __new__(cls, data, *args, **kwargs): return bytes.__new__(cls, data) def __init__(self, data, dataformat=AtomDataType.UTF8, version=0): self.dataformat = dataformat self.version = version __hash__ = bytes.__hash__ def __eq__(self, other): if not isinstance(other, MP4FreeForm): return bytes(self) == other return (bytes(self) == bytes(other) and self.dataformat == other.dataformat and self.version == other.version) def __ne__(self, other): return not self.__eq__(other) def __repr__(self): return "%s(%r, %r)" % ( type(self).__name__, bytes(self), AtomDataType(self.dataformat)) def _name2key(name): if PY2: return name return name.decode("latin-1") def _key2name(key): if PY2: return key return key.encode("latin-1") def _find_padding(atom_path): # Check for padding "free" atom # XXX: we only use them if they are adjacent to ilst, and only one. # and there also is a top level free atom which we could use maybe..? meta, ilst = atom_path[-2:] assert meta.name == b"meta" and ilst.name == b"ilst" index = meta.children.index(ilst) try: prev = meta.children[index - 1] if prev.name == b"free": return prev except IndexError: pass try: next_ = meta.children[index + 1] if next_.name == b"free": return next_ except IndexError: pass 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 and tags that failed to parse will be written back as is. """ def __init__(self, *args, **kwargs): self._failed_atoms = {} super(MP4Tags, self).__init__(*args, **kwargs) def load(self, atoms, fileobj): try: path = atoms.path(b"moov", b"udta", b"meta", b"ilst") except KeyError as key: raise MP4MetadataError(key) free = _find_padding(path) self._padding = free.datalength if free is not None else 0 ilst = path[-1] for atom in ilst.children: ok, data = atom.read(fileobj) if not ok: raise MP4MetadataError("Not enough data") try: if atom.name in self.__atoms: info = self.__atoms[atom.name] info[0](self, atom, data) else: # unknown atom, try as text self.__parse_text(atom, data, implicit=False) except MP4MetadataError: # parsing failed, save them so we can write them back key = _name2key(atom.name) self._failed_atoms.setdefault(key, []).append(data) def __setitem__(self, key, value): if not isinstance(key, str): raise TypeError("key has to be str") super(MP4Tags, self).__setitem__(key, value) @classmethod def _can_load(cls, atoms): return b"moov.udta.meta.ilst" in atoms @staticmethod def _key_sort(item): (key, v) = item # 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(izip(order, xrange(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 (order.get(key[:4], last), len(repr(v)), repr(v)) def save(self, filename, padding=None): """Save the metadata to the given filename.""" values = [] items = sorted(self.items(), key=self._key_sort) for key, value in items: atom_name = _key2name(key)[:4] if atom_name in self.__atoms: render_func = self.__atoms[atom_name][1] else: render_func = type(self).__render_text try: values.append(render_func(self, key, value)) except (TypeError, ValueError) as s: reraise(MP4MetadataValueError, s, sys.exc_info()[2]) for key, failed in iteritems(self._failed_atoms): # don't write atoms back if we have added a new one with # the same name, this excludes freeform which can have # multiple atoms with the same key (most parsers seem to be able # to handle that) if key in self: assert _key2name(key) != b"----" continue for data in failed: values.append(Atom.render(_key2name(key), data)) data = Atom.render(b"ilst", b"".join(values)) # Find the old atoms. with open(filename, "rb+") as fileobj: try: atoms = Atoms(fileobj) except AtomError as err: reraise(error, err, sys.exc_info()[2]) self.__save(fileobj, atoms, data, padding) def __save(self, fileobj, atoms, data, padding): try: path = atoms.path(b"moov", b"udta", b"meta", b"ilst") except KeyError: self.__save_new(fileobj, atoms, data, padding) else: self.__save_existing(fileobj, atoms, path, data, padding) def __pad_ilst(self, data, length=None): if length is None: length = ((len(data) + 1023) & ~1023) - len(data) return Atom.render(b"free", b"\x00" * length) def __save_new(self, fileobj, atoms, ilst_data, padding_func): hdlr = Atom.render(b"hdlr", b"\x00" * 8 + b"mdirappl" + b"\x00" * 9) meta_data = b"\x00\x00\x00\x00" + hdlr + ilst_data try: path = atoms.path(b"moov", b"udta") except KeyError: path = atoms.path(b"moov") offset = path[-1]._dataoffset # ignoring some atom overhead... but we don't have padding left anyway # and padding_size is guaranteed to be less than zero content_size = get_size(fileobj) - offset padding_size = -len(meta_data) assert padding_size < 0 info = PaddingInfo(padding_size, content_size) new_padding = info._get_padding(padding_func) new_padding = min(0xFFFFFFFF, new_padding) free = Atom.render(b"free", b"\x00" * new_padding) meta = Atom.render(b"meta", meta_data + free) if path[-1].name != b"udta": # moov.udta not found -- create one data = Atom.render(b"udta", meta) else: data = meta insert_bytes(fileobj, len(data), offset) fileobj.seek(offset) fileobj.write(data) self.__update_parents(fileobj, path, len(data)) self.__update_offsets(fileobj, atoms, len(data), offset) def __save_existing(self, fileobj, atoms, path, ilst_data, padding_func): # Replace the old ilst atom. ilst = path[-1] offset = ilst.offset length = ilst.length # Use adjacent free atom if there is one free = _find_padding(path) if free is not None: offset = min(offset, free.offset) length += free.length # Always add a padding atom to make things easier padding_overhead = len(Atom.render(b"free", b"")) content_size = get_size(fileobj) - (offset + length) padding_size = length - (len(ilst_data) + padding_overhead) info = PaddingInfo(padding_size, content_size) new_padding = info._get_padding(padding_func) # Limit padding size so we can be sure the free atom overhead is as we # calculated above (see Atom.render) new_padding = min(0xFFFFFFFF, new_padding) ilst_data += Atom.render(b"free", b"\x00" * new_padding) resize_bytes(fileobj, length, len(ilst_data), offset) delta = len(ilst_data) - length fileobj.seek(offset) fileobj.write(ilst_data) self.__update_parents(fileobj, path[:-1], delta) self.__update_offsets(fileobj, atoms, delta, offset) def __update_parents(self, fileobj, path, delta): """Update all parent atoms with the new size.""" if delta == 0: return 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(b"\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[b"moov"] for atom in moov.findall(b'stco', True): self.__update_offset_table(fileobj, ">%dI", atom, delta, offset) for atom in moov.findall(b'co64', True): self.__update_offset_table(fileobj, ">%dQ", atom, delta, offset) try: for atom in atoms[b"moof"].findall(b'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: head = data[pos:pos + 12] if len(head) != 12: raise MP4MetadataError("truncated atom % r" % atom.name) length, name = struct.unpack(">I4s", head[:8]) version = ord(head[8:9]) flags = struct.unpack(">I", b"\x00" + head[9:12])[0] if name != b"data": raise MP4MetadataError( "unexpected atom %r inside %r" % (name, atom.name)) chunk = data[pos + 16:pos + length] if len(chunk) != length - 16: raise MP4MetadataError("truncated atom % r" % atom.name) yield version, flags, chunk pos += length def __add(self, key, value, single=False): assert isinstance(key, str) if single: self[key] = value else: self.setdefault(key, []).extend(value) def __render_data(self, key, version, flags, value): return Atom.render(_key2name(key), b"".join([ Atom.render( b"data", struct.pack(">2I", version << 24 | 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 != b"data": raise MP4MetadataError( "unexpected atom %r inside %r" % (atom_name, atom.name)) version = ord(data[pos + 8:pos + 8 + 1]) flags = struct.unpack(">I", b"\x00" + data[pos + 9:pos + 12])[0] value.append(MP4FreeForm(data[pos + 16:pos + length], dataformat=flags, version=version)) pos += length key = _name2key(atom.name + b":" + mean + b":" + name) self.__add(key, value) def __render_freeform(self, key, value): if isinstance(value, bytes): value = [value] dummy, mean, name = _key2name(key).split(b":", 2) mean = struct.pack(">I4sI", len(mean) + 12, b"mean", 0) + mean name = struct.pack(">I4sI", len(name) + 12, b"name", 0) + name data = b"" for v in value: flags = AtomDataType.UTF8 version = 0 if isinstance(v, MP4FreeForm): flags = v.dataformat version = v.version data += struct.pack( ">I4s2I", len(v) + 16, b"data", version << 24 | flags, 0) data += v return Atom.render(b"----", mean + name + data) def __parse_pair(self, atom, data): key = _name2key(atom.name) values = [struct.unpack(">2H", d[2:6]) for version, flags, d in self.__parse_data(atom, data)] self.__add(key, values) 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, AtomDataType.IMPLICIT, 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, AtomDataType.IMPLICIT, data) def __parse_genre(self, atom, data): values = [] for version, flags, data in self.__parse_data(atom, data): # version = 0, flags = 0 if len(data) != 2: raise MP4MetadataValueError("invalid genre") genre = cdata.short_be(data) # Translate to a freeform genre. try: genre = GENRES[genre - 1] except IndexError: # this will make us write it back at least raise MP4MetadataValueError("unknown genre") values.append(genre) key = _name2key(b"\xa9gen") self.__add(key, values) def __parse_tempo(self, atom, data): values = [] for version, flags, data in self.__parse_data(atom, data): # version = 0, flags = 0 or 21 if len(data) != 2: raise MP4MetadataValueError("invalid tempo") values.append(cdata.ushort_be(data)) key = _name2key(atom.name) self.__add(key, values) def __render_tempo(self, key, value): try: if len(value) == 0: return self.__render_data(key, 0, AtomDataType.INTEGER, b"") 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 = [cdata.to_ushort_be(v) for v in value] return self.__render_data(key, 0, AtomDataType.INTEGER, values) def __parse_bool(self, atom, data): for version, flags, data in self.__parse_data(atom, data): if len(data) != 1: raise MP4MetadataValueError("invalid bool") value = bool(ord(data)) key = _name2key(atom.name) self.__add(key, value, single=True) def __render_bool(self, key, value): return self.__render_data( key, 0, AtomDataType.INTEGER, [chr_(bool(value))]) def __parse_cover(self, atom, data): values = [] pos = 0 while pos < atom.length - 8: length, name, imageformat = struct.unpack(">I4sI", data[pos:pos + 12]) if name != b"data": if name == b"name": pos += length continue raise MP4MetadataError( "unexpected atom %r inside 'covr'" % name) if imageformat not in (MP4Cover.FORMAT_JPEG, MP4Cover.FORMAT_PNG): # Sometimes AtomDataType.IMPLICIT or simply wrong. # In all cases it was jpeg, so default to it imageformat = MP4Cover.FORMAT_JPEG cover = MP4Cover(data[pos + 16:pos + length], imageformat) values.append(cover) pos += length key = _name2key(atom.name) self.__add(key, values) 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( b"data", struct.pack(">2I", imageformat, 0) + cover)) return Atom.render(_key2name(key), b"".join(atom_data)) def __parse_text(self, atom, data, implicit=True): # implicit = False, for parsing unknown atoms only take utf8 ones. # For known ones we can assume the implicit are utf8 too. values = [] for version, flags, atom_data in self.__parse_data(atom, data): if implicit: if flags not in (AtomDataType.IMPLICIT, AtomDataType.UTF8): raise MP4MetadataError( "Unknown atom type %r for %r" % (flags, atom.name)) else: if flags != AtomDataType.UTF8: raise MP4MetadataError( "%r is not text, ignore" % atom.name) try: text = atom_data.decode("utf-8") except UnicodeDecodeError as e: raise MP4MetadataError("%s: %s" % (_name2key(atom.name), e)) values.append(text) key = _name2key(atom.name) self.__add(key, values) def __render_text(self, key, value, flags=AtomDataType.UTF8): if isinstance(value, string_types): value = [value] encoded = [] for v in value: if not isinstance(v, text_type): if PY3: raise TypeError("%r not str" % v) v = v.decode("utf-8") encoded.append(v.encode("utf-8")) return self.__render_data(key, 0, flags, encoded) def delete(self, filename): """Remove the metadata from the given filename.""" self._failed_atoms.clear() self.clear() self.save(filename, padding=lambda x: 0) __atoms = { b"----": (__parse_freeform, __render_freeform), b"trkn": (__parse_pair, __render_pair), b"disk": (__parse_pair, __render_pair_no_trailing), b"gnre": (__parse_genre, None), b"tmpo": (__parse_tempo, __render_tempo), b"cpil": (__parse_bool, __render_bool), b"pgap": (__parse_bool, __render_bool), b"pcst": (__parse_bool, __render_bool), b"covr": (__parse_cover, __render_cover), b"purl": (__parse_text, __render_text), b"egid": (__parse_text, __render_text), } # these allow implicit flags and parse as text for name in [b"\xa9nam", b"\xa9alb", b"\xa9ART", b"aART", b"\xa9wrt", b"\xa9day", b"\xa9cmt", b"desc", b"purd", b"\xa9grp", b"\xa9gen", b"\xa9lyr", b"catg", b"keyw", b"\xa9too", b"cprt", b"soal", b"soaa", b"soar", b"sonm", b"soco", b"sosn", b"tvsh"]: __atoms[name] = (__parse_text, __render_text) def pprint(self): def to_line(key, value): assert isinstance(key, text_type) if isinstance(value, text_type): return u"%s=%s" % (key, value) return u"%s=%r" % (key, value) values = [] for key, value in sorted(iteritems(self)): if not isinstance(key, text_type): key = key.decode("latin-1") if key == "covr": values.append(u"%s=%s" % (key, u", ".join( [u"[%d bytes of data]" % len(data) for data in value]))) elif isinstance(value, list): for v in value: values.append(to_line(key, v)) else: values.append(to_line(key, value)) return u"\n".join(values) class MP4Info(StreamInfo): """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 * codec (string): * if starting with ``"mp4a"`` uses an mp4a audio codec (see the codec parameter in rfc6381 for details e.g. ``"mp4a.40.2"``) * for everything else see a list of possible values at http://www.mp4ra.org/codecs.html e.g. ``"mp4a"``, ``"alac"``, ``"mp4a.40.2"``, ``"ac-3"`` etc. * codec_description (string): Name of the codec used (ALAC, AAC LC, AC-3...). Values might change in the future, use for display purposes only. """ bitrate = 0 channels = 0 sample_rate = 0 bits_per_sample = 0 codec = u"" codec_name = u"" def __init__(self, atoms, fileobj): try: moov = atoms[b"moov"] except KeyError: raise MP4StreamInfoError("not a MP4 file") for trak in moov.findall(b"trak"): hdlr = trak[b"mdia", b"hdlr"] ok, data = hdlr.read(fileobj) if not ok: raise MP4StreamInfoError("Not enough data") if data[8:12] == b"soun": break else: raise MP4StreamInfoError("track has no audio data") mdhd = trak[b"mdia", b"mdhd"] ok, data = mdhd.read(fileobj) if not ok: raise MP4StreamInfoError("Not enough data") try: version, flags, data = parse_full_atom(data) except ValueError as e: raise MP4StreamInfoError(e) if version == 0: offset = 8 fmt = ">2I" elif version == 1: offset = 16 fmt = ">IQ" else: raise MP4StreamInfoError("Unknown mdhd version %d" % version) end = offset + struct.calcsize(fmt) unit, length = struct.unpack(fmt, data[offset:end]) try: self.length = float(length) / unit except ZeroDivisionError: self.length = 0 try: atom = trak[b"mdia", b"minf", b"stbl", b"stsd"] except KeyError: pass else: self._parse_stsd(atom, fileobj) def _parse_stsd(self, atom, fileobj): """Sets channels, bits_per_sample, sample_rate and optionally bitrate. Can raise MP4StreamInfoError. """ assert atom.name == b"stsd" ok, data = atom.read(fileobj) if not ok: raise MP4StreamInfoError("Invalid stsd") try: version, flags, data = parse_full_atom(data) except ValueError as e: raise MP4StreamInfoError(e) if version != 0: raise MP4StreamInfoError("Unsupported stsd version") try: num_entries, offset = cdata.uint32_be_from(data, 0) except cdata.error as e: raise MP4StreamInfoError(e) if num_entries == 0: return # look at the first entry if there is one entry_fileobj = cBytesIO(data[offset:]) try: entry_atom = Atom(entry_fileobj) except AtomError as e: raise MP4StreamInfoError(e) try: entry = AudioSampleEntry(entry_atom, entry_fileobj) except ASEntryError as e: raise MP4StreamInfoError(e) else: self.channels = entry.channels self.bits_per_sample = entry.sample_size self.sample_rate = entry.sample_rate self.bitrate = entry.bitrate self.codec = entry.codec self.codec_description = entry.codec_description def pprint(self): return "MPEG-4 audio (%s), %.2f seconds, %d bps" % ( self.codec_description, 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 with open(filename, "rb") as fileobj: try: atoms = Atoms(fileobj) except AtomError as err: reraise(error, err, sys.exc_info()[2]) try: self.info = MP4Info(atoms, fileobj) except error: raise except Exception as err: reraise(MP4StreamInfoError, err, sys.exc_info()[2]) if not MP4Tags._can_load(atoms): self.tags = None self._padding = 0 else: try: self.tags = self.MP4Tags(atoms, fileobj) except error: raise except Exception as err: reraise(MP4MetadataError, err, sys.exc_info()[2]) else: self._padding = self.tags._padding 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_data): return (b"ftyp" in header_data) + (b"mp4" in header_data) Open = MP4 def delete(filename): """Remove tags from a file.""" MP4(filename).delete() mutagen-1.31/mutagen/oggspeex.py0000644000175000017500000001101412574061222017155 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # 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 import StreamInfo from mutagen._vorbis import VCommentDict from mutagen.ogg import OggPage, OggFileType, error as OggError from mutagen._util import cdata, get_size from mutagen._tags import PaddingInfo class error(OggError): pass class OggSpeexHeaderError(error): pass class OggSpeexInfo(StreamInfo): """Ogg Speex stream information.""" length = 0 """file length in seconds, as a float""" channels = 0 """number of channels""" bitrate = 0 """nominal bitrate in bits per second. The reference encoder does not set the bitrate; in this case, the bitrate will be 0. """ def __init__(self, fileobj): page = OggPage(fileobj) while not page.packets[0].startswith(b"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 u"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] super(OggSpeexVComment, self).__init__(data, framing=False) self._padding = len(data) - self._size def _inject(self, fileobj, padding_func): """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(b"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) content_size = get_size(fileobj) - len(packets[0]) # approx vcomment_data = self.write(framing=False) padding_left = len(packets[0]) - len(vcomment_data) info = PaddingInfo(padding_left, content_size) new_padding = info._get_padding(padding_func) # Set the new comment packet. packets[0] = vcomment_data + b"\x00" * new_padding new_pages = OggPage._from_packets_try_preserve(packets, old_pages) OggPage.replace(fileobj, old_pages, new_pages) class OggSpeex(OggFileType): """An Ogg Speex file.""" _Info = OggSpeexInfo _Tags = OggSpeexVComment _Error = OggSpeexHeaderError _mimes = ["audio/x-speex"] info = None """A `OggSpeexInfo`""" tags = None """A `VCommentDict`""" @staticmethod def score(filename, fileobj, header): return (header.startswith(b"OggS") * (b"Speex " in header)) Open = OggSpeex def delete(filename): """Remove tags from a file.""" OggSpeex(filename).delete() mutagen-1.31/mutagen/asf/0000755000175000017500000000000012574256060015544 5ustar lazkalazka00000000000000mutagen-1.31/mutagen/asf/_util.py0000644000175000017500000002475112574061222017235 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2005-2006 Joe Wreschnig # Copyright (C) 2006-2007 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. import struct from mutagen._util import MutagenError class error(IOError, MutagenError): """Error raised by :mod:`mutagen.asf`""" class ASFError(error): pass class ASFHeaderError(error): pass def guid2bytes(s): """Converts a GUID to the serialized bytes representation""" assert isinstance(s, str) assert len(s) == 36 p = struct.pack return b"".join([ p("H", int(s[19:23], 16)), p(">Q", int(s[24:], 16))[2:], ]) def bytes2guid(s): """Converts a serialized GUID to a text GUID""" assert isinstance(s, bytes) u = struct.unpack v = [] v.extend(u("HQ", s[8:10] + b"\x00\x00" + s[10:])) return "%08X-%04X-%04X-%04X-%012X" % tuple(v) # Names from http://windows.microsoft.com/en-za/windows7/c00d10d1-[0-9A-F]{1,4} CODECS = { 0x0000: u"Unknown Wave Format", 0x0001: u"Microsoft PCM Format", 0x0002: u"Microsoft ADPCM Format", 0x0003: u"IEEE Float", 0x0004: u"Compaq Computer VSELP", 0x0005: u"IBM CVSD", 0x0006: u"Microsoft CCITT A-Law", 0x0007: u"Microsoft CCITT u-Law", 0x0008: u"Microsoft DTS", 0x0009: u"Microsoft DRM", 0x000A: u"Windows Media Audio 9 Voice", 0x000B: u"Windows Media Audio 10 Voice", 0x000C: u"OGG Vorbis", 0x000D: u"FLAC", 0x000E: u"MOT AMR", 0x000F: u"Nice Systems IMBE", 0x0010: u"OKI ADPCM", 0x0011: u"Intel IMA ADPCM", 0x0012: u"Videologic MediaSpace ADPCM", 0x0013: u"Sierra Semiconductor ADPCM", 0x0014: u"Antex Electronics G.723 ADPCM", 0x0015: u"DSP Solutions DIGISTD", 0x0016: u"DSP Solutions DIGIFIX", 0x0017: u"Dialogic OKI ADPCM", 0x0018: u"MediaVision ADPCM", 0x0019: u"Hewlett-Packard CU codec", 0x001A: u"Hewlett-Packard Dynamic Voice", 0x0020: u"Yamaha ADPCM", 0x0021: u"Speech Compression SONARC", 0x0022: u"DSP Group True Speech", 0x0023: u"Echo Speech EchoSC1", 0x0024: u"Ahead Inc. Audiofile AF36", 0x0025: u"Audio Processing Technology APTX", 0x0026: u"Ahead Inc. AudioFile AF10", 0x0027: u"Aculab Prosody 1612", 0x0028: u"Merging Technologies S.A. LRC", 0x0030: u"Dolby Labs AC2", 0x0031: u"Microsoft GSM 6.10", 0x0032: u"Microsoft MSNAudio", 0x0033: u"Antex Electronics ADPCME", 0x0034: u"Control Resources VQLPC", 0x0035: u"DSP Solutions Digireal", 0x0036: u"DSP Solutions DigiADPCM", 0x0037: u"Control Resources CR10", 0x0038: u"Natural MicroSystems VBXADPCM", 0x0039: u"Crystal Semiconductor IMA ADPCM", 0x003A: u"Echo Speech EchoSC3", 0x003B: u"Rockwell ADPCM", 0x003C: u"Rockwell DigiTalk", 0x003D: u"Xebec Multimedia Solutions", 0x0040: u"Antex Electronics G.721 ADPCM", 0x0041: u"Antex Electronics G.728 CELP", 0x0042: u"Intel G.723", 0x0043: u"Intel G.723.1", 0x0044: u"Intel G.729 Audio", 0x0045: u"Sharp G.726 Audio", 0x0050: u"Microsoft MPEG-1", 0x0052: u"InSoft RT24", 0x0053: u"InSoft PAC", 0x0055: u"MP3 - MPEG Layer III", 0x0059: u"Lucent G.723", 0x0060: u"Cirrus Logic", 0x0061: u"ESS Technology ESPCM", 0x0062: u"Voxware File-Mode", 0x0063: u"Canopus Atrac", 0x0064: u"APICOM G.726 ADPCM", 0x0065: u"APICOM G.722 ADPCM", 0x0066: u"Microsoft DSAT", 0x0067: u"Microsoft DSAT Display", 0x0069: u"Voxware Byte Aligned", 0x0070: u"Voxware AC8", 0x0071: u"Voxware AC10", 0x0072: u"Voxware AC16", 0x0073: u"Voxware AC20", 0x0074: u"Voxware RT24 MetaVoice", 0x0075: u"Voxware RT29 MetaSound", 0x0076: u"Voxware RT29HW", 0x0077: u"Voxware VR12", 0x0078: u"Voxware VR18", 0x0079: u"Voxware TQ40", 0x007A: u"Voxware SC3", 0x007B: u"Voxware SC3", 0x0080: u"Softsound", 0x0081: u"Voxware TQ60", 0x0082: u"Microsoft MSRT24", 0x0083: u"AT&T Labs G.729A", 0x0084: u"Motion Pixels MVI MV12", 0x0085: u"DataFusion Systems G.726", 0x0086: u"DataFusion Systems GSM610", 0x0088: u"Iterated Systems ISIAudio", 0x0089: u"Onlive", 0x008A: u"Multitude FT SX20", 0x008B: u"Infocom ITS ACM G.721", 0x008C: u"Convedia G.729", 0x008D: u"Congruency Audio", 0x0091: u"Siemens Business Communications SBC24", 0x0092: u"Sonic Foundry Dolby AC3 SPDIF", 0x0093: u"MediaSonic G.723", 0x0094: u"Aculab Prosody 8KBPS", 0x0097: u"ZyXEL ADPCM", 0x0098: u"Philips LPCBB", 0x0099: u"Studer Professional Audio AG Packed", 0x00A0: u"Malden Electronics PHONYTALK", 0x00A1: u"Racal Recorder GSM", 0x00A2: u"Racal Recorder G720.a", 0x00A3: u"Racal Recorder G723.1", 0x00A4: u"Racal Recorder Tetra ACELP", 0x00B0: u"NEC AAC", 0x00FF: u"CoreAAC Audio", 0x0100: u"Rhetorex ADPCM", 0x0101: u"BeCubed Software IRAT", 0x0111: u"Vivo G.723", 0x0112: u"Vivo Siren", 0x0120: u"Philips CELP", 0x0121: u"Philips Grundig", 0x0123: u"Digital G.723", 0x0125: u"Sanyo ADPCM", 0x0130: u"Sipro Lab Telecom ACELP.net", 0x0131: u"Sipro Lab Telecom ACELP.4800", 0x0132: u"Sipro Lab Telecom ACELP.8V3", 0x0133: u"Sipro Lab Telecom ACELP.G.729", 0x0134: u"Sipro Lab Telecom ACELP.G.729A", 0x0135: u"Sipro Lab Telecom ACELP.KELVIN", 0x0136: u"VoiceAge AMR", 0x0140: u"Dictaphone G.726 ADPCM", 0x0141: u"Dictaphone CELP68", 0x0142: u"Dictaphone CELP54", 0x0150: u"Qualcomm PUREVOICE", 0x0151: u"Qualcomm HALFRATE", 0x0155: u"Ring Zero Systems TUBGSM", 0x0160: u"Windows Media Audio Standard", 0x0161: u"Windows Media Audio 9 Standard", 0x0162: u"Windows Media Audio 9 Professional", 0x0163: u"Windows Media Audio 9 Lossless", 0x0164: u"Windows Media Audio Pro over SPDIF", 0x0170: u"Unisys NAP ADPCM", 0x0171: u"Unisys NAP ULAW", 0x0172: u"Unisys NAP ALAW", 0x0173: u"Unisys NAP 16K", 0x0174: u"Sycom ACM SYC008", 0x0175: u"Sycom ACM SYC701 G725", 0x0176: u"Sycom ACM SYC701 CELP54", 0x0177: u"Sycom ACM SYC701 CELP68", 0x0178: u"Knowledge Adventure ADPCM", 0x0180: u"Fraunhofer IIS MPEG-2 AAC", 0x0190: u"Digital Theater Systems DTS", 0x0200: u"Creative Labs ADPCM", 0x0202: u"Creative Labs FastSpeech8", 0x0203: u"Creative Labs FastSpeech10", 0x0210: u"UHER informatic GmbH ADPCM", 0x0215: u"Ulead DV Audio", 0x0216: u"Ulead DV Audio", 0x0220: u"Quarterdeck", 0x0230: u"I-link Worldwide ILINK VC", 0x0240: u"Aureal Semiconductor RAW SPORT", 0x0249: u"Generic Passthru", 0x0250: u"Interactive Products HSX", 0x0251: u"Interactive Products RPELP", 0x0260: u"Consistent Software CS2", 0x0270: u"Sony SCX", 0x0271: u"Sony SCY", 0x0272: u"Sony ATRAC3", 0x0273: u"Sony SPC", 0x0280: u"Telum Audio", 0x0281: u"Telum IA Audio", 0x0285: u"Norcom Voice Systems ADPCM", 0x0300: u"Fujitsu TOWNS SND", 0x0350: u"Micronas SC4 Speech", 0x0351: u"Micronas CELP833", 0x0400: u"Brooktree BTV Digital", 0x0401: u"Intel Music Coder", 0x0402: u"Intel Audio", 0x0450: u"QDesign Music", 0x0500: u"On2 AVC0 Audio", 0x0501: u"On2 AVC1 Audio", 0x0680: u"AT&T Labs VME VMPCM", 0x0681: u"AT&T Labs TPC", 0x08AE: u"ClearJump Lightwave Lossless", 0x1000: u"Olivetti GSM", 0x1001: u"Olivetti ADPCM", 0x1002: u"Olivetti CELP", 0x1003: u"Olivetti SBC", 0x1004: u"Olivetti OPR", 0x1100: u"Lernout & Hauspie", 0x1101: u"Lernout & Hauspie CELP", 0x1102: u"Lernout & Hauspie SBC8", 0x1103: u"Lernout & Hauspie SBC12", 0x1104: u"Lernout & Hauspie SBC16", 0x1400: u"Norris Communication", 0x1401: u"ISIAudio", 0x1500: u"AT&T Labs Soundspace Music Compression", 0x1600: u"Microsoft MPEG ADTS AAC", 0x1601: u"Microsoft MPEG RAW AAC", 0x1608: u"Nokia MPEG ADTS AAC", 0x1609: u"Nokia MPEG RAW AAC", 0x181C: u"VoxWare MetaVoice RT24", 0x1971: u"Sonic Foundry Lossless", 0x1979: u"Innings Telecom ADPCM", 0x1FC4: u"NTCSoft ALF2CD ACM", 0x2000: u"Dolby AC3", 0x2001: u"DTS", 0x4143: u"Divio AAC", 0x4201: u"Nokia Adaptive Multi-Rate", 0x4243: u"Divio G.726", 0x4261: u"ITU-T H.261", 0x4263: u"ITU-T H.263", 0x4264: u"ITU-T H.264", 0x674F: u"Ogg Vorbis Mode 1", 0x6750: u"Ogg Vorbis Mode 2", 0x6751: u"Ogg Vorbis Mode 3", 0x676F: u"Ogg Vorbis Mode 1+", 0x6770: u"Ogg Vorbis Mode 2+", 0x6771: u"Ogg Vorbis Mode 3+", 0x7000: u"3COM NBX Audio", 0x706D: u"FAAD AAC Audio", 0x77A1: u"True Audio Lossless Audio", 0x7A21: u"GSM-AMR CBR 3GPP Audio", 0x7A22: u"GSM-AMR VBR 3GPP Audio", 0xA100: u"Comverse Infosys G723.1", 0xA101: u"Comverse Infosys AVQSBC", 0xA102: u"Comverse Infosys SBC", 0xA103: u"Symbol Technologies G729a", 0xA104: u"VoiceAge AMR WB", 0xA105: u"Ingenient Technologies G.726", 0xA106: u"ISO/MPEG-4 Advanced Audio Coding (AAC)", 0xA107: u"Encore Software Ltd's G.726", 0xA108: u"ZOLL Medical Corporation ASAO", 0xA109: u"Speex Voice", 0xA10A: u"Vianix MASC Speech Compression", 0xA10B: u"Windows Media 9 Spectrum Analyzer Output", 0xA10C: u"Media Foundation Spectrum Analyzer Output", 0xA10D: u"GSM 6.10 (Full-Rate) Speech", 0xA10E: u"GSM 6.20 (Half-Rate) Speech", 0xA10F: u"GSM 6.60 (Enchanced Full-Rate) Speech", 0xA110: u"GSM 6.90 (Adaptive Multi-Rate) Speech", 0xA111: u"GSM Adaptive Multi-Rate WideBand Speech", 0xA112: u"Polycom G.722", 0xA113: u"Polycom G.728", 0xA114: u"Polycom G.729a", 0xA115: u"Polycom Siren", 0xA116: u"Global IP Sound ILBC", 0xA117: u"Radio Time Time Shifted Radio", 0xA118: u"Nice Systems ACA", 0xA119: u"Nice Systems ADPCM", 0xA11A: u"Vocord Group ITU-T G.721", 0xA11B: u"Vocord Group ITU-T G.726", 0xA11C: u"Vocord Group ITU-T G.722.1", 0xA11D: u"Vocord Group ITU-T G.728", 0xA11E: u"Vocord Group ITU-T G.729", 0xA11F: u"Vocord Group ITU-T G.729a", 0xA120: u"Vocord Group ITU-T G.723.1", 0xA121: u"Vocord Group LBC", 0xA122: u"Nice G.728", 0xA123: u"France Telecom G.729 ACM Audio", 0xA124: u"CODIAN Audio", 0xCC12: u"Intel YUV12 Codec", 0xCFCC: u"Digital Processing Systems Perception Motion JPEG", 0xD261: u"DEC H.261", 0xD263: u"DEC H.263", 0xFFFE: u"Extensible Wave Format", 0xFFFF: u"Unregistered", } mutagen-1.31/mutagen/asf/_attrs.py0000644000175000017500000002341512574061222017411 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2005-2006 Joe Wreschnig # Copyright (C) 2006-2007 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. import sys import struct from mutagen._compat import swap_to_string, text_type, PY2, reraise from mutagen._util import total_ordering from ._util import ASFError class ASFBaseAttribute(object): """Generic attribute.""" TYPE = None _TYPES = {} value = None """The Python value of this attribute (type depends on the class)""" language = None """Language""" stream = None """Stream""" 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: if value is None: # we used to support not passing any args and instead assign # them later, keep that working.. self.value = None else: self.value = self._validate(value) @classmethod def _register(cls, other): cls._TYPES[other.TYPE] = other return other @classmethod def _get_type(cls, type_): """Raises KeyError""" return cls._TYPES[type_] def _validate(self, value): """Raises TypeError or ValueError in case the user supplied value isn't valid. """ return 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") + b"\x00\x00" data = self._render() return (struct.pack(" 0xFFFF or value.TYPE == GUID) can_cont_desc = value.TYPE == UNICODE if library_only or value.language is not None: self.to_metadata_library.append((name, value)) elif value.stream is not None: if name not in self.to_metadata: self.to_metadata[name] = value else: self.to_metadata_library.append((name, value)) elif name in ContentDescriptionObject.NAMES: if name not in self.to_content_description and can_cont_desc: self.to_content_description[name] = value else: self.to_metadata_library.append((name, value)) else: if name not in self.to_extended_content_description: self.to_extended_content_description[name] = value else: self.to_metadata_library.append((name, value)) # Add missing objects header = self._header if header.get_child(ContentDescriptionObject.GUID) is None: header.objects.append(ContentDescriptionObject()) if header.get_child(ExtendedContentDescriptionObject.GUID) is None: header.objects.append(ExtendedContentDescriptionObject()) header_ext = header.get_child(HeaderExtensionObject.GUID) if header_ext is None: header_ext = HeaderExtensionObject() header.objects.append(header_ext) if header_ext.get_child(MetadataObject.GUID) is None: header_ext.objects.append(MetadataObject()) if header_ext.get_child(MetadataLibraryObject.GUID) is None: header_ext.objects.append(MetadataLibraryObject()) # Render to file with open(self.filename, "rb+") as fileobj: old_size = header.parse_size(fileobj)[0] data = header.render_full(self, fileobj, old_size, padding) size = len(data) resize_bytes(fileobj, old_size, size, 0) fileobj.seek(0) fileobj.write(data) def add_tags(self): raise ASFError def delete(self, filename=None): if filename is not None and filename != self.filename: raise ValueError("saving to another file not supported atm") self.tags.clear() self.save(padding=lambda x: 0) @staticmethod def score(filename, fileobj, header): return header.startswith(HeaderObject.GUID) * 2 Open = ASF mutagen-1.31/mutagen/asf/_objects.py0000644000175000017500000003334612574061222017711 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2005-2006 Joe Wreschnig # Copyright (C) 2006-2007 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. import struct from mutagen._util import cdata, get_size from mutagen._compat import text_type, xrange, izip from mutagen._tags import PaddingInfo from ._util import guid2bytes, bytes2guid, CODECS, ASFError, ASFHeaderError from ._attrs import ASFBaseAttribute, ASFUnicodeAttribute class BaseObject(object): """Base ASF object.""" GUID = None _TYPES = {} def __init__(self): self.objects = [] self.data = b"" def parse(self, asf, data): self.data = data def render(self, asf): data = self.GUID + struct.pack("" % ( type(self).__name__, bytes2guid(self.GUID), self.objects) def pprint(self): l = [] l.append("%s(%s)" % (type(self).__name__, bytes2guid(self.GUID))) for o in self.objects: for e in o.pprint().splitlines(): l.append(" " + e) return "\n".join(l) class UnknownObject(BaseObject): """Unknown ASF object.""" def __init__(self, guid): super(UnknownObject, self).__init__() assert isinstance(guid, bytes) self.GUID = guid @BaseObject._register class HeaderObject(BaseObject): """ASF header.""" GUID = guid2bytes("75B22630-668E-11CF-A6D9-00AA0062CE6C") @classmethod def parse_full(cls, asf, fileobj): """Raises ASFHeaderError""" header = cls() size, num_objects = cls.parse_size(fileobj) for i in xrange(num_objects): guid, size = struct.unpack("<16sQ", fileobj.read(24)) obj = BaseObject._get_object(guid) data = fileobj.read(size - 24) obj.parse(asf, data) header.objects.append(obj) return header @classmethod def parse_size(cls, fileobj): """Returns (size, num_objects) Raises ASFHeaderError """ header = fileobj.read(30) if len(header) != 30 or header[:16] != HeaderObject.GUID: raise ASFHeaderError("Not an ASF file.") return struct.unpack("= 0 info = PaddingInfo(available - needed_size, content_size) # add padding padding = info._get_padding(padding_func) padding_obj.parse(asf, b"\x00" * padding) data += padding_obj.render(asf) num_objects += 1 data = (HeaderObject.GUID + struct.pack(" 0: texts.append(data[pos:end].decode("utf-16-le").strip(u"\x00")) else: texts.append(None) pos = end for key, value in izip(self.NAMES, texts): if value is not None: value = ASFUnicodeAttribute(value=value) asf._tags.setdefault(self.GUID, []).append((key, value)) def render(self, asf): def render_text(name): value = asf.to_content_description.get(name) if value is not None: return text_type(value).encode("utf-16-le") + b"\x00\x00" else: return b"" texts = [render_text(x) for x in self.NAMES] data = struct.pack("= 0 asf.info.length = max((length / 10000000.0) - (preroll / 1000.0), 0.0) @BaseObject._register class StreamPropertiesObject(BaseObject): """Stream properties.""" GUID = guid2bytes("B7DC0791-A9B7-11CF-8EE6-00C00C205365") def parse(self, asf, data): super(StreamPropertiesObject, self).parse(asf, data) channels, sample_rate, bitrate = struct.unpack(" (3, 96): patch = u".0" else: patch = u".0+" elif flag == b"" or flag == b".": patch = u".0+" else: flag_string = u" (?)" # extended header, seek back to 9 bytes for the caller fileobj.seek(-11, 1) return u"%d.%d%s%s" % (major, minor, patch, flag_string), True class XingHeaderError(Exception): pass class XingHeaderFlags(object): FRAMES = 0x1 BYTES = 0x2 TOC = 0x4 VBR_SCALE = 0x8 class XingHeader(object): frames = -1 """Number of frames, -1 if unknown""" bytes = -1 """Number of bytes, -1 if unknown""" toc = [] """List of 100 file offsets in percent encoded as 0-255. E.g. entry 50 contains the file offset in percent at 50% play time. Empty if unknown. """ vbr_scale = -1 """VBR quality indicator 0-100. -1 if unknown""" lame_header = None """A LAMEHeader instance or None""" lame_version = u"" """The version of the LAME encoder e.g. '3.99.0'. Empty if unknown""" is_info = False """If the header started with 'Info' and not 'Xing'""" def __init__(self, fileobj): """Parses the Xing header or raises XingHeaderError. The file position after this returns is undefined. """ data = fileobj.read(8) if len(data) != 8 or data[:4] not in (b"Xing", b"Info"): raise XingHeaderError("Not a Xing header") self.is_info = (data[:4] == b"Info") flags = cdata.uint32_be_from(data, 4)[0] if flags & XingHeaderFlags.FRAMES: data = fileobj.read(4) if len(data) != 4: raise XingHeaderError("Xing header truncated") self.frames = cdata.uint32_be(data) if flags & XingHeaderFlags.BYTES: data = fileobj.read(4) if len(data) != 4: raise XingHeaderError("Xing header truncated") self.bytes = cdata.uint32_be(data) if flags & XingHeaderFlags.TOC: data = fileobj.read(100) if len(data) != 100: raise XingHeaderError("Xing header truncated") self.toc = list(bytearray(data)) if flags & XingHeaderFlags.VBR_SCALE: data = fileobj.read(4) if len(data) != 4: raise XingHeaderError("Xing header truncated") self.vbr_scale = cdata.uint32_be(data) try: self.lame_version, has_header = LAMEHeader.parse_version(fileobj) if has_header: self.lame_header = LAMEHeader(self, fileobj) except LAMEError: pass @classmethod def get_offset(cls, info): """Calculate the offset to the Xing header from the start of the MPEG header including sync based on the MPEG header's content. """ assert info.layer == 3 if info.version == 1: if info.mode != 3: return 36 else: return 21 else: if info.mode != 3: return 21 else: return 13 class VBRIHeaderError(Exception): pass class VBRIHeader(object): version = 0 """VBRI header version""" quality = 0 """Quality indicator""" bytes = 0 """Number of bytes""" frames = 0 """Number of frames""" toc_scale_factor = 0 """Scale factor of TOC entries""" toc_frames = 0 """Number of frames per table entry""" toc = [] """TOC""" def __init__(self, fileobj): """Reads the VBRI header or raises VBRIHeaderError. The file position is undefined after this returns """ data = fileobj.read(26) if len(data) != 26 or not data.startswith(b"VBRI"): raise VBRIHeaderError("Not a VBRI header") offset = 4 self.version, offset = cdata.uint16_be_from(data, offset) if self.version != 1: raise VBRIHeaderError( "Unsupported header version: %r" % self.version) offset += 2 # float16.. can't do self.quality, offset = cdata.uint16_be_from(data, offset) self.bytes, offset = cdata.uint32_be_from(data, offset) self.frames, offset = cdata.uint32_be_from(data, offset) toc_num_entries, offset = cdata.uint16_be_from(data, offset) self.toc_scale_factor, offset = cdata.uint16_be_from(data, offset) toc_entry_size, offset = cdata.uint16_be_from(data, offset) self.toc_frames, offset = cdata.uint16_be_from(data, offset) toc_size = toc_entry_size * toc_num_entries toc_data = fileobj.read(toc_size) if len(toc_data) != toc_size: raise VBRIHeaderError("VBRI header truncated") self.toc = [] if toc_entry_size == 2: unpack = partial(cdata.uint16_be_from, toc_data) elif toc_entry_size == 4: unpack = partial(cdata.uint32_be_from, toc_data) else: raise VBRIHeaderError("Invalid TOC entry size") self.toc = [unpack(i)[0] for i in xrange(0, toc_size, toc_entry_size)] @classmethod def get_offset(cls, info): """Offset in bytes from the start of the MPEG header including sync""" assert info.layer == 3 return 36 mutagen-1.31/mutagen/apev2.py0000644000175000017500000005073512574126636016401 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2005 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. """APEv2 reading and writing. The APEv2 format is most commonly used with Musepack files, but is also the format of choice for WavPack and other formats. Some MP3s also have APEv2 tags, but this can cause problems with many MP3 decoders and taggers. APEv2 tags, like Vorbis comments, are freeform key=value pairs. APEv2 keys can be any ASCII string with characters from 0x20 to 0x7E, between 2 and 255 characters long. Keys are case-sensitive, but readers are recommended to be case insensitive, and it is forbidden to multiple keys which differ only in case. Keys are usually stored title-cased (e.g. 'Artist' rather than 'artist'). APEv2 values are slightly more structured than Vorbis comments; values are flagged as one of text, binary, or an external reference (usually a URI). Based off the format specification found at http://wiki.hydrogenaudio.org/index.php?title=APEv2_specification. """ __all__ = ["APEv2", "APEv2File", "Open", "delete"] import sys import struct from collections import MutableSequence from ._compat import (cBytesIO, PY3, text_type, PY2, reraise, swap_to_string, xrange) from mutagen import Metadata, FileType, StreamInfo from mutagen._util import (DictMixin, cdata, delete_bytes, total_ordering, MutagenError) def is_valid_apev2_key(key): if not isinstance(key, text_type): if PY3: raise TypeError("APEv2 key must be str") try: key = key.decode('ascii') except UnicodeDecodeError: return False # PY26 - Change to set literal syntax (since set is faster than list here) return ((2 <= len(key) <= 255) and (min(key) >= u' ') and (max(key) <= u'~') and (key not in [u"OggS", u"TAG", u"ID3", u"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 = xrange(3) HAS_HEADER = 1 << 31 HAS_NO_FOOTER = 1 << 30 IS_HEADER = 1 << 29 class error(IOError, MutagenError): 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) if self.header is None: self.metadata = self.footer elif self.footer is None: self.metadata = self.header else: 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) == b"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) == b"TAG": fileobj.seek(-35, 1) # "TAG" + header length if fileobj.read(8) == b"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) == b'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) == b"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) == b"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) == b"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) == b"APETAGEX": fileobj.seek(-8, 1) start = fileobj.tell() else: break self.start = start class _CIDictProxy(DictMixin): def __init__(self, *args, **kwargs): self.__casemap = {} self.__dict = {} super(_CIDictProxy, 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 __getitem__(self, key): return self.__dict[key.lower()] def __setitem__(self, key, value): lower = key.lower() self.__casemap[lower] = key self.__dict[lower] = value def __delitem__(self, key): lower = key.lower() del(self.__casemap[lower]) del(self.__dict[lower]) def keys(self): return [self.__casemap.get(key, key) for key in self.__dict.keys()] class APEv2(_CIDictProxy, Metadata): """A file with an APEv2 tag. ID3v1 tags are silently ignored and overwritten. """ filename = None def pprint(self): """Return tag key=value pairs in a human-readable format.""" items = sorted(self.items()) return u"\n".join(u"%s=%s" % (k, v.pprint()) for k, v in items) def load(self, filename): """Load tags from a filename.""" self.filename = filename with open(filename, "rb") as fileobj: data = _APEv2Data(fileobj) if data.tag: self.clear() self.__parse_tag(data.tag, data.items) else: raise APENoHeaderError("No APE tag found") def __parse_tag(self, tag, count): fileobj = cBytesIO(tag) for i in xrange(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:] != b'\x00' and value: value = fileobj.read(1) key += value if key[-1:] == b"\x00": key = key[:-1] if PY3: try: key = key.decode("ascii") except UnicodeError as err: reraise(APEBadItemError, err, sys.exc_info()[2]) value = fileobj.read(size) value = _get_value_type(kind)._new(value) self[key] = value def __getitem__(self, key): if not is_valid_apev2_key(key): raise KeyError("%r is not a valid APEv2 key" % key) if PY2: key = key.encode('ascii') return super(APEv2, self).__getitem__(key) def __delitem__(self, key): if not is_valid_apev2_key(key): raise KeyError("%r is not a valid APEv2 key" % key) if PY2: key = key.encode('ascii') super(APEv2, self).__delitem__(key) 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. Python 3: all bytes will be assumed to be a byte value, even if they are valid utf-8. 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) if PY2: 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, text_type): # unicode? we've got to be text. value = APEValue(value, TEXT) elif isinstance(value, list): items = [] for v in value: if not isinstance(v, text_type): if PY3: raise TypeError("item in list not str") v = v.decode("utf-8") items.append(v) # list? text. value = APEValue(u"\0".join(items), TEXT) else: if PY3: value = APEValue(value, BINARY) 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) super(APEv2, self).__setitem__(key, value) 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) tags = [] for key, value in self.items(): # Packed format for an item: # 4B: Value length # 4B: Value type # Key name # 1B: Null # Key value value_data = value._write() if not isinstance(key, bytes): key = key.encode("utf-8") tag_data = bytearray() tag_data += struct.pack("<2I", len(value_data), value.kind << 1) tag_data += key + b"\0" + value_data tags.append(bytes(tag_data)) # "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.sort(key=len) num_tags = len(tags) tags = b"".join(tags) header = bytearray(b"APETAGEX") # version, tag size, item count, flags header += struct.pack("<4I", 2000, len(tags) + 32, num_tags, HAS_HEADER | IS_HEADER) header += b"\0" * 8 fileobj.write(header) fileobj.write(tags) footer = bytearray(b"APETAGEX") footer += struct.pack("<4I", 2000, len(tags) + 32, num_tags, HAS_HEADER) footer += b"\0" * 8 fileobj.write(footer) fileobj.close() def delete(self, filename=None): """Remove tags from a file.""" filename = filename or self.filename with open(filename, "r+b") as fileobj: data = _APEv2Data(fileobj) if data.start is not None and data.size is not None: delete_bytes(fileobj, data.end - data.start, data.start) self.clear() Open = APEv2 def delete(filename): """Remove tags from a file.""" try: APEv2(filename).delete() except APENoHeaderError: pass def _get_value_type(kind): """Returns a _APEValue subclass or raises ValueError""" if kind == TEXT: return APETextValue elif kind == BINARY: return APEBinaryValue elif kind == EXTERNAL: return APEExtValue raise ValueError("unknown kind %r" % kind) 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__. """ try: type_ = _get_value_type(kind) except ValueError: raise ValueError("kind must be TEXT, BINARY, or EXTERNAL") else: return type_(value) class _APEValue(object): kind = None value = None def __init__(self, value, kind=None): # kind kwarg is for backwards compat if kind is not None and kind != self.kind: raise ValueError self.value = self._validate(value) @classmethod def _new(cls, data): instance = cls.__new__(cls) instance._parse(data) return instance def _parse(self, data): """Sets value or raises APEBadItemError""" raise NotImplementedError def _write(self): """Returns bytes""" raise NotImplementedError def _validate(self, value): """Returns validated value or raises TypeError/ValueErrr""" raise NotImplementedError def __repr__(self): return "%s(%r, %d)" % (type(self).__name__, self.value, self.kind) @swap_to_string @total_ordering class _APEUtf8Value(_APEValue): def _parse(self, data): try: self.value = data.decode("utf-8") except UnicodeDecodeError as e: reraise(APEBadItemError, e, sys.exc_info()[2]) def _validate(self, value): if not isinstance(value, text_type): if PY3: raise TypeError("value not str") else: value = value.decode("utf-8") return value def _write(self): return self.value.encode("utf-8") def __len__(self): return len(self.value) def __bytes__(self): return self._write() def __eq__(self, other): return self.value == other def __lt__(self, other): return self.value < other def __str__(self): return self.value class APETextValue(_APEUtf8Value, MutableSequence): """An APEv2 text value. Text values are Unicode/UTF-8 strings. They can be accessed like strings (with a null separating the values), or arrays of strings. """ kind = TEXT def __iter__(self): """Iterate over the strings of the value (not the characters)""" return iter(self.value.split(u"\0")) def __getitem__(self, index): return self.value.split(u"\0")[index] def __len__(self): return self.value.count(u"\0") + 1 def __setitem__(self, index, value): if not isinstance(value, text_type): if PY3: raise TypeError("value not str") else: value = value.decode("utf-8") values = list(self) values[index] = value self.value = u"\0".join(values) def insert(self, index, value): if not isinstance(value, text_type): if PY3: raise TypeError("value not str") else: value = value.decode("utf-8") values = list(self) values.insert(index, value) self.value = u"\0".join(values) def __delitem__(self, index): values = list(self) del values[index] self.value = u"\0".join(values) def pprint(self): return u" / ".join(self) @swap_to_string @total_ordering class APEBinaryValue(_APEValue): """An APEv2 binary value.""" kind = BINARY def _parse(self, data): self.value = data def _write(self): return self.value def _validate(self, value): if not isinstance(value, bytes): raise TypeError("value not bytes") return bytes(value) def __len__(self): return len(self.value) def __bytes__(self): return self._write() def __eq__(self, other): return self.value == other def __lt__(self, other): return self.value < other def pprint(self): return u"[%d bytes]" % len(self) class APEExtValue(_APEUtf8Value): """An APEv2 external value. External values are usually URI or IRI strings. """ kind = EXTERNAL def pprint(self): return u"[External] %s" % self.value class APEv2File(FileType): class _Info(StreamInfo): length = 0 bitrate = 0 def __init__(self, fileobj): pass @staticmethod def pprint(): return u"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 APENoHeaderError: self.tags = None def add_tags(self): if self.tags is None: self.tags = APEv2() else: raise error("%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() return ((b"APETAGEX" in footer) - header.startswith(b"ID3")) mutagen-1.31/mutagen/_tags.py0000644000175000017500000000552712574253503016452 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # 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. class PaddingInfo(object): """Abstract padding information object. This will be passed to the callback function that can be used for saving tags. :: def my_callback(info: PaddingInfo): return info.get_default_padding() The callback should return the amount of padding to use (>= 0) based on the content size and the padding of the file after saving. The actual used amount of padding might vary depending on the file format (due to alignment etc.) The default implementation can be accessed using the :meth:`get_default_padding` method in the callback. """ padding = 0 """The amount of padding left after saving in bytes (can be negative if more data needs to be added as padding is available) """ size = 0 """The amount of data following the padding""" def __init__(self, padding, size): self.padding = padding self.size = size def get_default_padding(self): """The default implementation which tries to select a reasonable amount of padding and which might change in future versions. :return: Amount of padding after saving :rtype: int """ high = 1024 * 10 + self.size // 100 # 10 KiB + 1% of trailing data low = 1024 + self.size // 1000 # 1 KiB + 0.1% of trailing data if self.padding >= 0: # enough padding left if self.padding > high: # padding too large, reduce return low # just use existing padding as is return self.padding else: # not enough padding, add some return low def _get_padding(self, user_func): if user_func is None: return self.get_default_padding() else: return user_func(self) def __repr__(self): return "<%s size=%d padding=%d>" % ( type(self).__name__, self.size, self.padding) class Metadata(object): """An abstract dict-like object. Metadata is the base class for many of the tag objects in Mutagen. """ __module__ = "mutagen" def __init__(self, *args, **kwargs): if args or kwargs: self.load(*args, **kwargs) def load(self, *args, **kwargs): raise NotImplementedError def save(self, filename=None): """Save changes to a file.""" raise NotImplementedError def delete(self, filename=None): """Remove tags from a file. In most cases this means any traces of the tag will be removed from the file. """ raise NotImplementedError mutagen-1.31/mutagen/_vorbis.py0000644000175000017500000002330012574061222017000 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2005-2006 Joe Wreschnig # 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. """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 import mutagen from ._compat import reraise, BytesIO, text_type, xrange, PY3, PY2 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 '='. Takes str/unicode in Python 2, unicode in Python 3 """ if PY3 and isinstance(key, bytes): raise TypeError("needs to be str not bytes") 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): self._size = 0 # 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, bytes): data = BytesIO(data) elif not hasattr(data, 'read'): raise TypeError("VComment requires bytes or a file-like") start = data.tell() self.load(data, *args, **kwargs) self._size = data.tell() - start 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 as err: if errors == "ignore": continue elif errors == "replace": tag, value = u"unknown%d" % i, string else: reraise(VorbisEncodingError, err, sys.exc_info()[2]) try: tag = tag.encode('ascii', errors) except UnicodeEncodeError: raise VorbisEncodingError("invalid tag name %r" % tag) else: # string keys in py3k if PY3: tag = tag.decode("ascii") if is_valid_key(tag): self.append((tag, value)) if framing and not bytearray(fileobj.read(1))[0] & 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. In Python 3 all keys and values have to be a string. """ if not isinstance(self.vendor, text_type): if PY3: raise ValueError("vendor needs to be str") 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 TypeError: raise ValueError("%r is not a valid key" % key) if not isinstance(value, text_type): if PY3: raise ValueError("%r needs to be str" % key) try: value.decode("utf-8") except: raise ValueError("%r is not a valid value" % value) return True def clear(self): """Clear all keys from the comment.""" for i in list(self): self.remove(i) 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() def _encode(value): if not isinstance(value, bytes): return value.encode('utf-8') return value f = BytesIO() vendor = _encode(self.vendor) f.write(cdata.to_uint_le(len(vendor))) f.write(vendor) f.write(cdata.to_uint_le(len(self))) for tag, value in self: tag = _encode(tag) value = _encode(value) comment = tag + b"=" + value f.write(cdata.to_uint_le(len(comment))) f.write(comment) if framing: f.write(b"\x01") return f.getvalue() def pprint(self): def _decode(value): if not isinstance(value, text_type): return value.decode('utf-8', 'replace') return value tags = [u"%s=%s" % (_decode(k), _decode(v)) for k, v in self] return u"\n".join(tags) 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. """ # PY3 only if isinstance(key, slice): return VComment.__getitem__(self, key) if not is_valid_key(key): raise ValueError key = key.lower() 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.""" # PY3 only if isinstance(key, slice): return VComment.__delitem__(self, key) if not is_valid_key(key): raise ValueError key = key.lower() to_delete = [x for x in self if x[0].lower() == key] if not to_delete: raise KeyError(key) else: for item in to_delete: self.remove(item) def __contains__(self, key): """Return true if the key has any values.""" if not is_valid_key(key): raise ValueError key = key.lower() 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. """ # PY3 only if isinstance(key, slice): return VComment.__setitem__(self, key, values) if not is_valid_key(key): raise ValueError if not isinstance(values, list): values = [values] try: del(self[key]) except KeyError: pass if PY2: key = key.encode('ascii') for value in values: self.append((key, value)) def keys(self): """Return all keys in the comment.""" return 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.31/mutagen/_compat.py0000644000175000017500000000401712574061222016763 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 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. import sys PY2 = sys.version_info[0] == 2 PY3 = not PY2 if PY2: from StringIO import StringIO BytesIO = StringIO from cStringIO import StringIO as cBytesIO from itertools import izip long_ = long integer_types = (int, long) string_types = (str, unicode) text_type = unicode xrange = xrange cmp = cmp chr_ = chr def endswith(text, end): return text.endswith(end) iteritems = lambda d: d.iteritems() itervalues = lambda d: d.itervalues() iterkeys = lambda d: d.iterkeys() iterbytes = lambda b: iter(b) exec("def reraise(tp, value, tb):\n raise tp, value, tb") def swap_to_string(cls): if "__str__" in cls.__dict__: cls.__unicode__ = cls.__str__ if "__bytes__" in cls.__dict__: cls.__str__ = cls.__bytes__ return cls elif PY3: from io import StringIO StringIO = StringIO from io import BytesIO cBytesIO = BytesIO long_ = int integer_types = (int,) string_types = (str,) text_type = str izip = zip xrange = range cmp = lambda a, b: (a > b) - (a < b) chr_ = lambda x: bytes([x]) def endswith(text, end): # usefull for paths which can be both, str and bytes if isinstance(text, str): if not isinstance(end, str): end = end.decode("ascii") else: if not isinstance(end, bytes): end = end.encode("ascii") return text.endswith(end) iteritems = lambda d: iter(d.items()) itervalues = lambda d: iter(d.values()) iterkeys = lambda d: iter(d.keys()) iterbytes = lambda b: (bytes([v]) for v in b) def reraise(tp, value, tb): raise tp(value).with_traceback(tb) def swap_to_string(cls): return cls mutagen-1.31/mutagen/optimfrog.py0000644000175000017500000000432312574061222017347 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 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 ._compat import endswith from mutagen import StreamInfo from mutagen.apev2 import APEv2File, error, delete class OptimFROGHeaderError(error): pass class OptimFROGInfo(StreamInfo): """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(b"OFR ") or struct.unpack("BBH4s", page.packets[0][5:13]) if flac != b"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 = cBytesIO(page.packets[0][17:]) try: flac_info = FLACStreamInfo(stringobj) except FLACError as e: raise OggFLACHeaderError(e) for attr in ["min_blocksize", "max_blocksize", "sample_rate", "channels", "bits_per_sample", "total_samples", "length"]: setattr(self, attr, getattr(flac_info, attr)) 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 u"Ogg FLAC, %.2f seconds, %d Hz" % ( self.length, self.sample_rate) class OggFLACVComment(VCommentDict): def __init__(self, fileobj, info): # data should be pointing at the start of an Ogg page, after # the first FLAC page. 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) comment = cBytesIO(OggPage.to_packets(pages)[0][4:]) super(OggFLACVComment, self).__init__(comment, framing=False) def _inject(self, fileobj, padding_func): """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(b"\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(framing=False) data = packets[0][:1] + 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"] info = None """A `OggFLACStreamInfo`""" tags = None """A `VCommentDict`""" def save(self, filename=None): return super(OggFLAC, self).save(filename) @staticmethod def score(filename, fileobj, header): return (header.startswith(b"OggS") * ( (b"FLAC" in header) + (b"fLaC" in header))) Open = OggFLAC def delete(filename): """Remove tags from a file.""" OggFLAC(filename).delete() mutagen-1.31/mutagen/oggtheora.py0000644000175000017500000001061012574061222017314 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # 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 import StreamInfo from mutagen._vorbis import VCommentDict from mutagen._util import cdata, get_size from mutagen._tags import PaddingInfo from mutagen.ogg import OggPage, OggFileType, error as OggError class error(OggError): pass class OggTheoraHeaderError(error): pass class OggTheoraInfo(StreamInfo): """Ogg Theora stream information.""" length = 0 """File length in seconds, as a float""" fps = 0 """Video frames per second, as a float""" bitrate = 0 """Bitrate in bps (int)""" def __init__(self, fileobj): page = OggPage(fileobj) while not page.packets[0].startswith(b"\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(b"\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 u"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, framing=False) self._padding = len(data) - self._size def _inject(self, fileobj, padding_func): """Write tag data into the Theora comment packet/page.""" fileobj.seek(0) page = OggPage(fileobj) while not page.packets[0].startswith(b"\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) content_size = get_size(fileobj) - len(packets[0]) # approx vcomment_data = b"\x81theora" + self.write(framing=False) padding_left = len(packets[0]) - len(vcomment_data) info = PaddingInfo(padding_left, content_size) new_padding = info._get_padding(padding_func) packets[0] = vcomment_data + b"\x00" * new_padding new_pages = OggPage._from_packets_try_preserve(packets, old_pages) OggPage.replace(fileobj, old_pages, new_pages) class OggTheora(OggFileType): """An Ogg Theora file.""" _Info = OggTheoraInfo _Tags = OggTheoraCommentDict _Error = OggTheoraHeaderError _mimes = ["video/x-theora"] info = None """A `OggTheoraInfo`""" tags = None """A `VCommentDict`""" @staticmethod def score(filename, fileobj, header): return (header.startswith(b"OggS") * ((b"\x80theora" in header) + (b"\x81theora" in header)) * 2) Open = OggTheora def delete(filename): """Remove tags from a file.""" OggTheora(filename).delete() mutagen-1.31/mutagen/aiff.py0000644000175000017500000002424112574061222016247 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2014 Evan Purkhiser # 2014 Ben Ockmore # # 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. """AIFF audio stream information and tags.""" import sys import struct from struct import pack from ._compat import endswith, text_type, reraise from mutagen import StreamInfo, FileType from mutagen.id3 import ID3 from mutagen.id3._util import ID3NoHeaderError, error as ID3Error from mutagen._util import resize_bytes, delete_bytes, MutagenError __all__ = ["AIFF", "Open", "delete"] class error(MutagenError, RuntimeError): pass class InvalidChunk(error, IOError): pass # based on stdlib's aifc _HUGE_VAL = 1.79769313486231e+308 def is_valid_chunk_id(id): assert isinstance(id, text_type) return ((len(id) <= 4) and (min(id) >= u' ') and (max(id) <= u'~')) def read_float(data): # 10 bytes expon, himant, lomant = struct.unpack('>hLL', data) sign = 1 if expon < 0: sign = -1 expon = expon + 0x8000 if expon == himant == lomant == 0: f = 0.0 elif expon == 0x7FFF: f = _HUGE_VAL else: expon = expon - 16383 f = (himant * 0x100000000 + lomant) * pow(2.0, expon - 63) return sign * f class IFFChunk(object): """Representation of a single IFF chunk""" # Chunk headers are 8 bytes long (4 for ID and 4 for the size) HEADER_SIZE = 8 def __init__(self, fileobj, parent_chunk=None): self.__fileobj = fileobj self.parent_chunk = parent_chunk self.offset = fileobj.tell() header = fileobj.read(self.HEADER_SIZE) if len(header) < self.HEADER_SIZE: raise InvalidChunk() self.id, self.data_size = struct.unpack('>4si', header) try: self.id = self.id.decode('ascii') except UnicodeDecodeError: raise InvalidChunk() if not is_valid_chunk_id(self.id): raise InvalidChunk() self.size = self.HEADER_SIZE + self.data_size self.data_offset = fileobj.tell() def read(self): """Read the chunks data""" self.__fileobj.seek(self.data_offset) return self.__fileobj.read(self.data_size) def write(self, data): """Write the chunk data""" if len(data) > self.data_size: raise ValueError self.__fileobj.seek(self.data_offset) self.__fileobj.write(data) def delete(self): """Removes the chunk from the file""" delete_bytes(self.__fileobj, self.size, self.offset) if self.parent_chunk is not None: self.parent_chunk._update_size( self.parent_chunk.data_size - self.size) def _update_size(self, data_size): """Update the size of the chunk""" self.__fileobj.seek(self.offset + 4) self.__fileobj.write(pack('>I', data_size)) if self.parent_chunk is not None: size_diff = self.data_size - data_size self.parent_chunk._update_size( self.parent_chunk.data_size - size_diff) self.data_size = data_size self.size = data_size + self.HEADER_SIZE def resize(self, new_data_size): """Resize the file and update the chunk sizes""" resize_bytes( self.__fileobj, self.data_size, new_data_size, self.data_offset) self._update_size(new_data_size) class IFFFile(object): """Representation of a IFF file""" def __init__(self, fileobj): self.__fileobj = fileobj self.__chunks = {} # AIFF Files always start with the FORM chunk which contains a 4 byte # ID before the start of other chunks fileobj.seek(0) self.__chunks[u'FORM'] = IFFChunk(fileobj) # Skip past the 4 byte FORM id fileobj.seek(IFFChunk.HEADER_SIZE + 4) # Where the next chunk can be located. We need to keep track of this # since the size indicated in the FORM header may not match up with the # offset determined from the size of the last chunk in the file self.__next_offset = fileobj.tell() # Load all of the chunks while True: try: chunk = IFFChunk(fileobj, self[u'FORM']) except InvalidChunk: break self.__chunks[chunk.id.strip()] = chunk # Calculate the location of the next chunk, # considering the pad byte self.__next_offset = chunk.offset + chunk.size self.__next_offset += self.__next_offset % 2 fileobj.seek(self.__next_offset) def __contains__(self, id_): """Check if the IFF file contains a specific chunk""" assert isinstance(id_, text_type) if not is_valid_chunk_id(id_): raise KeyError("AIFF key must be four ASCII characters.") return id_ in self.__chunks def __getitem__(self, id_): """Get a chunk from the IFF file""" assert isinstance(id_, text_type) if not is_valid_chunk_id(id_): raise KeyError("AIFF key must be four ASCII characters.") try: return self.__chunks[id_] except KeyError: raise KeyError( "%r has no %r chunk" % (self.__fileobj.name, id_)) def __delitem__(self, id_): """Remove a chunk from the IFF file""" assert isinstance(id_, text_type) if not is_valid_chunk_id(id_): raise KeyError("AIFF key must be four ASCII characters.") self.__chunks.pop(id_).delete() def insert_chunk(self, id_): """Insert a new chunk at the end of the IFF file""" assert isinstance(id_, text_type) if not is_valid_chunk_id(id_): raise KeyError("AIFF key must be four ASCII characters.") self.__fileobj.seek(self.__next_offset) self.__fileobj.write(pack('>4si', id_.ljust(4).encode('ascii'), 0)) self.__fileobj.seek(self.__next_offset) chunk = IFFChunk(self.__fileobj, self[u'FORM']) self[u'FORM']._update_size(self[u'FORM'].data_size + chunk.size) self.__chunks[id_] = chunk self.__next_offset = chunk.offset + chunk.size class AIFFInfo(StreamInfo): """AIFF audio stream information. Information is parsed from the COMM chunk of the AIFF file Useful attributes: * length -- audio length, in seconds * bitrate -- audio bitrate, in bits per second * channels -- The number of audio channels * sample_rate -- audio sample rate, in Hz * sample_size -- The audio sample size """ length = 0 bitrate = 0 channels = 0 sample_rate = 0 def __init__(self, fileobj): iff = IFFFile(fileobj) try: common_chunk = iff[u'COMM'] except KeyError as e: raise error(str(e)) data = common_chunk.read() info = struct.unpack('>hLh10s', data[:18]) channels, frame_count, sample_size, sample_rate = info self.sample_rate = int(read_float(sample_rate)) self.sample_size = sample_size self.channels = channels self.bitrate = channels * sample_size * self.sample_rate self.length = frame_count / float(self.sample_rate) def pprint(self): return u"%d channel AIFF @ %d bps, %s Hz, %.2f seconds" % ( self.channels, self.bitrate, self.sample_rate, self.length) class _IFFID3(ID3): """A AIFF file with ID3v2 tags""" def _pre_load_header(self, fileobj): try: fileobj.seek(IFFFile(fileobj)[u'ID3'].data_offset) except (InvalidChunk, KeyError): raise ID3NoHeaderError("No ID3 chunk") def save(self, filename=None, v2_version=4, v23_sep='/', padding=None): """Save ID3v2 data to the AIFF file""" if filename is None: filename = self.filename # Unlike the parent ID3.save method, we won't save to a blank file # since we would have to construct a empty AIFF file with open(filename, 'rb+') as fileobj: iff_file = IFFFile(fileobj) if u'ID3' not in iff_file: iff_file.insert_chunk(u'ID3') chunk = iff_file[u'ID3'] try: data = self._prepare_data( fileobj, chunk.data_offset, chunk.data_size, v2_version, v23_sep, padding) except ID3Error as e: reraise(error, e, sys.exc_info()[2]) new_size = len(data) new_size += new_size % 2 # pad byte assert new_size % 2 == 0 chunk.resize(new_size) data += (new_size - len(data)) * b'\x00' assert new_size == len(data) chunk.write(data) def delete(self, filename=None): """Completely removes the ID3 chunk from the AIFF file""" if filename is None: filename = self.filename delete(filename) self.clear() def delete(filename): """Completely removes the ID3 chunk from the AIFF file""" with open(filename, "rb+") as file_: try: del IFFFile(file_)[u'ID3'] except KeyError: pass class AIFF(FileType): """An AIFF audio file. :ivar info: :class:`AIFFInfo` :ivar tags: :class:`ID3` """ _mimes = ["audio/aiff", "audio/x-aiff"] @staticmethod def score(filename, fileobj, header): filename = filename.lower() return (header.startswith(b"FORM") * 2 + endswith(filename, b".aif") + endswith(filename, b".aiff") + endswith(filename, b".aifc")) def add_tags(self): """Add an empty ID3 tag to the file.""" if self.tags is None: self.tags = _IFFID3() else: raise error("an ID3 tag already exists") def load(self, filename, **kwargs): """Load stream and tag information from a file.""" self.filename = filename try: self.tags = _IFFID3(filename, **kwargs) except ID3NoHeaderError: self.tags = None except ID3Error as e: raise error(e) with open(filename, "rb") as fileobj: self.info = AIFFInfo(fileobj) Open = AIFF mutagen-1.31/mutagen/_constants.py0000644000175000017500000000650112574061222017514 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- """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.31/mutagen/oggvorbis.py0000644000175000017500000001133012574061222017336 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # 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 import StreamInfo from mutagen._vorbis import VCommentDict from mutagen._util import get_size from mutagen._tags import PaddingInfo from mutagen.ogg import OggPage, OggFileType, error as OggError class error(OggError): pass class OggVorbisHeaderError(error): pass class OggVorbisInfo(StreamInfo): """Ogg Vorbis stream information.""" length = 0 """File length in seconds, as a float""" channels = 0 """Number of channels""" bitrate = 0 """Nominal ('average') bitrate in bits per second, as an int""" sample_rate = 0 """Sample rate in Hz""" def __init__(self, fileobj): page = OggPage(fileobj) while not page.packets[0].startswith(b"\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 u"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) self._padding = len(data) - self._size def _inject(self, fileobj, padding_func): """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(b"\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) content_size = get_size(fileobj) - len(packets[0]) # approx vcomment_data = b"\x03vorbis" + self.write() padding_left = len(packets[0]) - len(vcomment_data) info = PaddingInfo(padding_left, content_size) new_padding = info._get_padding(padding_func) # Set the new comment packet. packets[0] = vcomment_data + b"\x00" * new_padding new_pages = OggPage._from_packets_try_preserve(packets, old_pages) 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"] info = None """A `OggVorbisInfo`""" tags = None """A `VCommentDict`""" @staticmethod def score(filename, fileobj, header): return (header.startswith(b"OggS") * (b"\x01vorbis" in header)) Open = OggVorbis def delete(filename): """Remove tags from a file.""" OggVorbis(filename).delete() mutagen-1.31/mutagen/oggopus.py0000644000175000017500000001125612574061222017027 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2012, 2013 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 import StreamInfo from mutagen._compat import BytesIO from mutagen._util import get_size from mutagen._tags import PaddingInfo from mutagen._vorbis import VCommentDict from mutagen.ogg import OggPage, OggFileType, error as OggError class error(OggError): pass class OggOpusHeaderError(error): pass class OggOpusInfo(StreamInfo): """Ogg Opus stream information.""" length = 0 """File length in seconds, as a float""" channels = 0 """Number of channels""" def __init__(self, fileobj): page = OggPage(fileobj) while not page.packets[0].startswith(b"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 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 u"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(b"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 fileobj = BytesIO(data) super(OggOpusVComment, self).__init__(fileobj, framing=False) self._padding = len(data) - self._size # in case the LSB of the first byte after v-comment is 1, preserve the # following data padding_flag = fileobj.read(1) if padding_flag and ord(padding_flag) & 0x1: self._pad_data = padding_flag + fileobj.read() self._padding = 0 # we have to preserve, so no padding else: self._pad_data = b"" def _inject(self, fileobj, padding_func): fileobj.seek(0) info = OggOpusInfo(fileobj) old_pages = self.__get_comment_pages(fileobj, info) packets = OggPage.to_packets(old_pages) vcomment_data = b"OpusTags" + self.write(framing=False) if self._pad_data: # if we have padding data to preserver we can't add more padding # as long as we don't know the structure of what follows packets[0] = vcomment_data + self._pad_data else: content_size = get_size(fileobj) - len(packets[0]) # approx padding_left = len(packets[0]) - len(vcomment_data) info = PaddingInfo(padding_left, content_size) new_padding = info._get_padding(padding_func) packets[0] = vcomment_data + b"\x00" * new_padding new_pages = OggPage._from_packets_try_preserve(packets, old_pages) 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"] info = None """A `OggOpusInfo`""" tags = None """A `VCommentDict`""" @staticmethod def score(filename, fileobj, header): return (header.startswith(b"OggS") * (b"OpusHead" in header)) Open = OggOpus def delete(filename): """Remove tags from a file.""" OggOpus(filename).delete() mutagen-1.31/mutagen/mp3.py0000644000175000017500000003004212574061222016035 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 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 ._compat import endswith, xrange from ._mp3util import XingHeader, XingHeaderError, VBRIHeader, VBRIHeaderError from mutagen import StreamInfo from mutagen._util import MutagenError, enum from mutagen.id3 import ID3FileType, BitPaddedInt, delete __all__ = ["MP3", "Open", "delete", "MP3"] class error(RuntimeError, MutagenError): pass class HeaderNotFoundError(error, IOError): pass class InvalidMPEGHeader(error, IOError): pass @enum class BitrateMode(object): UNKNOWN = 0 """Probably a CBR file, but not sure""" CBR = 1 """Constant Bitrate""" VBR = 2 """Variable Bitrate""" ABR = 3 """Average Bitrate (a variant of VBR)""" def _guess_xing_bitrate_mode(xing): if xing.lame_header: lame = xing.lame_header if lame.vbr_method in (1, 8): return BitrateMode.CBR elif lame.vbr_method in (2, 9): return BitrateMode.ABR elif lame.vbr_method in (3, 4, 5, 6): return BitrateMode.VBR # everything else undefined, continue guessing # info tags get only written by lame for cbr files if xing.is_info: return BitrateMode.CBR # older lame and non-lame with some variant of vbr if xing.vbr_scale != -1 or xing.lame_version: return BitrateMode.VBR return BitrateMode.UNKNOWN # Mode values. STEREO, JOINTSTEREO, DUALCHANNEL, MONO = xrange(4) class MPEGInfo(StreamInfo): """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 * channels -- number of audio channels * bitrate -- audio bitrate, in bits per second * sketchy -- if true, the file may not be valid MPEG audio * encoder_info -- a string containing encoder name and possibly version. In case a lame tag is present this will start with ``"LAME "``, if unknown it is empty, otherwise the text format is undefined. * bitrate_mode -- a :class:`BitrateMode` * track_gain -- replaygain track gain (89db) or None * track_peak -- replaygain track peak or None * album_gain -- replaygain album gain (89db) or None 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): [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448], (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 xrange(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 encoder_info = u"" bitrate_mode = BitrateMode.UNKNOWN track_gain = track_peak = album_gain = album_peak = None 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 = b'', 0 insize = BitPaddedInt(insize) if id3 == b'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(b"\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(b"\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(b"\xff", frame_1 + 2) else: break else: raise HeaderNotFoundError("can't sync to an MPEG frame") self.channels = 1 if self.mode == MONO else 2 # 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 = int(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. if self.layer != 3: return # Xing xing_offset = XingHeader.get_offset(self) fileobj.seek(offset + frame_1 + xing_offset, 0) try: xing = XingHeader(fileobj) except XingHeaderError: pass else: lame = xing.lame_header self.sketchy = False self.bitrate_mode = _guess_xing_bitrate_mode(xing) if xing.frames != -1: samples = frame_size * xing.frames if lame is not None: samples -= lame.encoder_delay_start samples -= lame.encoder_padding_end self.length = float(samples) / self.sample_rate if xing.bytes != -1 and self.length: self.bitrate = int((xing.bytes * 8) / self.length) if xing.lame_version: self.encoder_info = u"LAME %s" % xing.lame_version if lame is not None: self.track_gain = lame.track_gain_adjustment self.track_peak = lame.track_peak self.album_gain = lame.album_gain_adjustment return # VBRI vbri_offset = VBRIHeader.get_offset(self) fileobj.seek(offset + frame_1 + vbri_offset, 0) try: vbri = VBRIHeader(fileobj) except VBRIHeaderError: pass else: self.bitrate_mode = BitrateMode.VBR self.encoder_info = u"FhG" self.sketchy = False self.length = float(frame_size * vbri.frames) / self.sample_rate if self.length: self.bitrate = int((vbri.bytes * 8) / self.length) def pprint(self): info = str(self.bitrate_mode).split(".", 1)[-1] if self.bitrate_mode == BitrateMode.UNKNOWN: info = u"CBR?" if self.encoder_info: info += ", %s" % self.encoder_info s = u"MPEG %s layer %d, %d bps (%s), %s Hz, %d chn, %.2f seconds" % ( self.version, self.layer, self.bitrate, info, self.sample_rate, self.channels, self.length) if self.sketchy: s += u" (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/mpeg", "audio/mpg", "audio/x-mpeg"] @property def mime(self): l = self.info.layer return ["audio/mp%d" % l, "audio/x-mp%d" % l] + super(MP3, self).mime @staticmethod def score(filename, fileobj, header_data): filename = filename.lower() return (header_data.startswith(b"ID3") * 2 + endswith(filename, b".mp3") + endswith(filename, b".mp2") + endswith(filename, b".mpg") + endswith(filename, b".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.31/mutagen/ogg.py0000644000175000017500000004523012574112775016131 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 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 mutagen import FileType from mutagen._util import cdata, resize_bytes, MutagenError from ._compat import cBytesIO, reraise, chr_, izip, xrange class error(IOError, MutagenError): """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 = 0 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 != b"OggS": raise error("read %r, expected %r, at 0x%x" % ( oggs, b"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 bytearray(lacing_bytes): total += c if c < 255: lacings.append(total) total = 0 if total: lacings.append(total) self.complete = False self.packets = [fileobj.read(l) for l in lacings] if [len(p) for p in 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", b"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(b"\xff" * quot + chr_(rem)) lacing_data = b"".join(lacing_data) if not self.complete and lacing_data.endswith(b"\x00"): lacing_data = lacing_data[:-1] data.append(chr_(len(lacing_data))) data.append(lacing_data) data.extend(self.packets) data = b"".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.") @staticmethod def renumber(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 @staticmethod def to_packets(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([b""]) 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 [b"".join(p) for p in packets] @classmethod def _from_packets_try_preserve(cls, packets, old_pages): """Like from_packets but in case the size and number of the packets is the same as in the given pages the layout of the pages will be copied (the page size and number will match). If the packets don't match this behaves like:: OggPage.from_packets(packets, sequence=old_pages[0].sequence) """ old_packets = cls.to_packets(old_pages) if [len(p) for p in packets] != [len(p) for p in old_packets]: # doesn't match, fall back return cls.from_packets(packets, old_pages[0].sequence) new_data = b"".join(packets) new_pages = [] for old in old_pages: new = OggPage() new.sequence = old.sequence new.complete = old.complete new.continued = old.continued new.position = old.position for p in old.packets: data, new_data = new_data[:len(p)], new_data[len(p):] new.packets.append(data) new_pages.append(new) assert not new_data return new_pages @staticmethod def from_packets(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(b"") 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 = -1 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 = b"" if page.packets: pages.append(page) return pages @classmethod def replace(cls, 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. """ if not len(old_pages) or not len(new_pages): raise ValueError("empty pages list not allowed") # Number the new pages starting from the first old page. first = old_pages[0].sequence for page, seq in izip(new_pages, xrange(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 = -1 new_data = [cls.write(p) for p in new_pages] # Add dummy data or merge the remaining data together so multiple # new pages replace an old one pages_diff = len(old_pages) - len(new_data) if pages_diff > 0: new_data.extend([b""] * pages_diff) elif pages_diff < 0: new_data[pages_diff - 1:] = [b"".join(new_data[pages_diff - 1:])] # Replace pages one by one. If the sizes match no resize happens. offset_adjust = 0 new_data_end = None assert len(old_pages) == len(new_data) for old_page, data in izip(old_pages, new_data): offset = old_page.offset + offset_adjust data_size = len(data) resize_bytes(fileobj, old_page.size, data_size, offset) fileobj.seek(offset, 0) fileobj.write(data) new_data_end = offset + data_size offset_adjust += (data_size - old_page.size) # 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 cls.renumber(fileobj, serial, sequence) @staticmethod def find_last(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(b"OggS") except ValueError: raise error("unable to find final Ogg header") bytesobj = cBytesIO(data[index:]) best_page = None try: page = OggPage(bytesobj) 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 with open(filename, "rb") as fileobj: try: self.info = self._Info(fileobj) self.tags = self._Tags(fileobj, self.info) self.info._post_tags(fileobj) except error as e: reraise(self._Error, e, sys.exc_info()[2]) except EOFError: raise self._Error("no appropriate stream found") 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() # TODO: we should delegate the deletion to the subclass and not through # _inject. with open(filename, "rb+") as fileobj: try: self.tags._inject(fileobj, lambda x: 0) except error as e: reraise(self._Error, e, sys.exc_info()[2]) except EOFError: raise self._Error("no appropriate stream found") def add_tags(self): raise self._Error def save(self, filename=None, padding=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, padding) except error as e: reraise(self._Error, e, sys.exc_info()[2]) except EOFError: raise self._Error("no appropriate stream found") finally: fileobj.close() mutagen-1.31/mutagen/easymp4.py0000644000175000017500000002102612574061222016722 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 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 from mutagen.mp4 import MP4, MP4Tags, error, delete from ._compat import PY2, text_type, PY3 __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 self._padding = self.__mp4._padding 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 list(map(text_type, tags[atomid])) def setter(tags, key, value): clamp = lambda x: int(min(max(min_value, x), max_value)) tags[atomid] = [clamp(v) for v in 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(text_type(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 = "----:" + mean + ":" + name def getter(tags, key): return [s.decode("utf-8", "replace") for s in tags[atomid]] def setter(tags, key, value): encoded = [] for v in value: if not isinstance(v, text_type): if PY3: raise TypeError("%r not str" % v) v = v.decode("utf-8") encoded.append(v.encode("utf-8")) tags[atomid] = encoded 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 PY2: if isinstance(value, basestring): value = [value] else: if isinstance(value, text_type): 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.31/mutagen/easyid3.py0000644000175000017500000003537312574061222016713 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 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 ._compat import iteritems, text_type, PY2 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 break 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)) def save(self, *args, **kwargs): # ignore v2_version until we support 2.3 here kwargs.pop("v2_version", None) self.__id3.save(*args, **kwargs) 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 PY2: if isinstance(value, basestring): value = [value] else: if isinstance(value, text_type): 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 original_date_get(id3, key): return [stamp.text for stamp in id3["TDOR"].text] def original_date_set(id3, key, value): id3.add(mutagen.id3.TDOR(encoding=3, text=value)) def original_date_delete(id3, key): del(id3["TDOR"]) 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 iteritems({ "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", "TLAN": "language", }): EasyID3.RegisterTextKey(key, frameid) EasyID3.RegisterKey("genre", genre_get, genre_set, genre_delete) EasyID3.RegisterKey("date", date_get, date_set, date_delete) EasyID3.RegisterKey("originaldate", original_date_get, original_date_set, original_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( "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 iteritems({ 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", u"CATALOGNUMBER": "catalognumber", u"MusicBrainz Release Track Id": "musicbrainz_releasetrackid", u"MusicBrainz Release Group Id": "musicbrainz_releasegroupid", u"MusicBrainz Work Id": "musicbrainz_workid", u"Acoustid Fingerprint": "acoustid_fingerprint", u"Acoustid Id": "acoustid_id", }): EasyID3.RegisterTXXXKey(key, desc) class EasyID3FileType(ID3FileType): """Like ID3FileType, but uses EasyID3 for tags.""" ID3 = EasyID3 mutagen-1.31/mutagen/wavpack.py0000644000175000017500000000745512574061222017006 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2006 Joe Wreschnig # 2014 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. """WavPack reading and writing. WavPack is a lossless format that uses APEv2 tags. Read * http://www.wavpack.com/ * http://www.wavpack.com/file_format.txt for more information. """ __all__ = ["WavPack", "Open", "delete"] from mutagen import StreamInfo 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 _WavPackHeader(object): def __init__(self, block_size, version, track_no, index_no, total_samples, block_index, block_samples, flags, crc): self.block_size = block_size self.version = version self.track_no = track_no self.index_no = index_no self.total_samples = total_samples self.block_index = block_index self.block_samples = block_samples self.flags = flags self.crc = crc @classmethod def from_fileobj(cls, fileobj): """A new _WavPackHeader or raises WavPackHeaderError""" header = fileobj.read(32) if len(header) != 32 or not header.startswith(b"wvpk"): raise WavPackHeaderError("not a WavPack header: %r" % header) block_size = cdata.uint_le(header[4:8]) version = cdata.ushort_le(header[8:10]) track_no = ord(header[10:11]) index_no = ord(header[11:12]) samples = cdata.uint_le(header[12:16]) if samples == 2 ** 32 - 1: samples = -1 block_index = cdata.uint_le(header[16:20]) block_samples = cdata.uint_le(header[20:24]) flags = cdata.uint_le(header[24:28]) crc = cdata.uint_le(header[28:32]) return _WavPackHeader(block_size, version, track_no, index_no, samples, block_index, block_samples, flags, crc) class WavPackInfo(StreamInfo): """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): try: header = _WavPackHeader.from_fileobj(fileobj) except WavPackHeaderError: raise WavPackHeaderError("not a WavPack file") self.version = header.version self.channels = bool(header.flags & 4) or 2 self.sample_rate = RATES[(header.flags >> 23) & 0xF] if header.total_samples == -1 or header.block_index != 0: # TODO: we could make this faster by using the tag size # and search backwards for the last block, then do # last.block_index + last.block_samples - initial.block_index samples = header.block_samples while 1: fileobj.seek(header.block_size - 32 + 8, 1) try: header = _WavPackHeader.from_fileobj(fileobj) except WavPackHeaderError: break samples += header.block_samples else: samples = header.total_samples self.length = float(samples) / self.sample_rate def pprint(self): return u"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(b"wvpk") * 2 Open = WavPack mutagen-1.31/mutagen/_file.py0000644000175000017500000001656412574061222016431 0ustar lazkalazka00000000000000# Copyright (C) 2005 Michael Urman # -*- coding: utf-8 -*- # # 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 warnings from mutagen._util import DictMixin from mutagen._compat import izip class FileType(DictMixin): """An abstract object wrapping tags and audio stream information. Attributes: * info -- stream information (length, bitrate, sample rate) * tags -- metadata tags, if any Each file format has different potential tags and stream information. FileTypes implement an interface very similar to Metadata; the dict interface, save, load, and delete calls on a FileType call the appropriate methods on its tag data. """ __module__ = "mutagen" info = None tags = None filename = None _mimes = ["application/octet-stream"] def __init__(self, filename=None, *args, **kwargs): if filename is None: warnings.warn("FileType constructor requires a filename", DeprecationWarning) else: self.load(filename, *args, **kwargs) def load(self, filename, *args, **kwargs): raise NotImplementedError def __getitem__(self, key): """Look up a metadata tag key. If the file has no tags at all, a KeyError is raised. """ if self.tags is None: raise KeyError(key) else: return self.tags[key] def __setitem__(self, key, value): """Set a metadata tag. If the file has no tags, an appropriate format is added (but not written until save is called). """ if self.tags is None: self.add_tags() self.tags[key] = value def __delitem__(self, key): """Delete a metadata tag key. If the file has no tags at all, a KeyError is raised. """ if self.tags is None: raise KeyError(key) else: del(self.tags[key]) def keys(self): """Return a list of keys in the metadata tag. If the file has no tags at all, an empty list is returned. """ if self.tags is None: return [] else: return self.tags.keys() def delete(self, filename=None): """Remove tags from a file. In cases where the tagging format is independent of the file type (for example `mutagen.ID3`) all traces of the tagging format will be removed. In cases where the tag is part of the file type, all tags and padding will be removed. The tags attribute will be cleared as well if there is one. Does nothing if the file has no tags. :raises mutagen.MutagenError: if deleting wasn't possible """ if self.tags is not None: if filename is None: filename = self.filename else: warnings.warn( "delete(filename=...) is deprecated, reload the file", DeprecationWarning) return self.tags.delete(filename) def save(self, filename=None, **kwargs): """Save metadata tags. :raises mutagen.MutagenError: if saving wasn't possible """ if filename is None: filename = self.filename else: warnings.warn( "save(filename=...) is deprecated, reload the file", DeprecationWarning) if self.tags is not None: return self.tags.save(filename, **kwargs) def pprint(self): """Print stream information and comment key=value pairs.""" stream = "%s (%s)" % (self.info.pprint(), self.mime[0]) try: tags = self.tags.pprint() except AttributeError: return stream else: return stream + ((tags and "\n" + tags) or "") def add_tags(self): """Adds new tags to the file. :raises mutagen.MutagenError: if tags already exist or adding is not possible. """ raise NotImplementedError @property def mime(self): """A list of mime types""" mimes = [] for Kind in type(self).__mro__: for mime in getattr(Kind, '_mimes', []): if mime not in mimes: mimes.append(mime) return mimes @staticmethod def score(filename, fileobj, header): raise NotImplementedError class StreamInfo(object): """Abstract stream information object. Provides attributes for length, bitrate, sample rate etc. See the implementations for details. """ __module__ = "mutagen" def pprint(self): """Print stream information""" raise NotImplementedError def File(filename, options=None, easy=False): """Guess the type of the file and try to open it. The file type is decided by several things, such as the first 128 bytes (which usually contains a file type identifier), the filename extension, and the presence of existing tags. If no appropriate type could be found, None is returned. :param options: Sequence of :class:`FileType` implementations, defaults to all included ones. :param easy: If the easy wrappers should be returnd if available. For example :class:`EasyMP3 ` 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 from mutagen.aiff import AIFF from mutagen.aac import AAC options = [MP3, TrueAudio, OggTheora, OggSpeex, OggVorbis, OggFLAC, FLAC, AIFF, APEv2File, MP4, ID3FileType, WavPack, Musepack, MonkeysAudio, OptimFROG, ASF, OggOpus, AAC] if not options: return None with open(filename, "rb") as fileobj: 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] results = list(izip(results, options)) results.sort() (score, name), Kind = results[-1] if score > 0: return Kind(filename) else: return None mutagen-1.31/mutagen/aac.py0000644000175000017500000002630112574061222016065 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2014 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. """ * ADTS - Audio Data Transport Stream * ADIF - Audio Data Interchange Format * See ISO/IEC 13818-7 / 14496-03 """ from mutagen import StreamInfo from mutagen._file import FileType from mutagen._util import BitReader, BitReaderError, MutagenError from mutagen._compat import endswith, xrange _FREQS = [ 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350, ] class _ADTSStream(object): """Represents a series of frames belonging to the same stream""" parsed_frames = 0 """Number of successfully parsed frames""" offset = 0 """offset in bytes at which the stream starts (the first sync word)""" @classmethod def find_stream(cls, fileobj, max_bytes): """Returns a possibly valid _ADTSStream or None. Args: max_bytes (int): maximum bytes to read """ r = BitReader(fileobj) stream = cls(r) if stream.sync(max_bytes): stream.offset = (r.get_position() - 12) // 8 return stream def sync(self, max_bytes): """Find the next sync. Returns True if found.""" # at least 2 bytes for the sync max_bytes = max(max_bytes, 2) r = self._r r.align() while max_bytes > 0: try: b = r.bytes(1) if b == b"\xff": if r.bits(4) == 0xf: return True r.align() max_bytes -= 2 else: max_bytes -= 1 except BitReaderError: return False return False def __init__(self, r): """Use _ADTSStream.find_stream to create a stream""" self._fixed_header_key = None self._r = r self.offset = -1 self.parsed_frames = 0 self._samples = 0 self._payload = 0 self._start = r.get_position() / 8 self._last = self._start @property def bitrate(self): """Bitrate of the raw aac blocks, excluding framing/crc""" assert self.parsed_frames, "no frame parsed yet" if self._samples == 0: return 0 return (8 * self._payload * self.frequency) // self._samples @property def samples(self): """samples so far""" assert self.parsed_frames, "no frame parsed yet" return self._samples @property def size(self): """bytes read in the stream so far (including framing)""" assert self.parsed_frames, "no frame parsed yet" return self._last - self._start @property def channels(self): """0 means unknown""" assert self.parsed_frames, "no frame parsed yet" b_index = self._fixed_header_key[6] if b_index == 7: return 8 elif b_index > 7: return 0 else: return b_index @property def frequency(self): """0 means unknown""" assert self.parsed_frames, "no frame parsed yet" f_index = self._fixed_header_key[4] try: return _FREQS[f_index] except IndexError: return 0 def parse_frame(self): """True if parsing was successful. Fails either because the frame wasn't valid or the stream ended. """ try: return self._parse_frame() except BitReaderError: return False def _parse_frame(self): r = self._r # start == position of sync word start = r.get_position() - 12 # adts_fixed_header id_ = r.bits(1) layer = r.bits(2) protection_absent = r.bits(1) profile = r.bits(2) sampling_frequency_index = r.bits(4) private_bit = r.bits(1) # TODO: if 0 we could parse program_config_element() channel_configuration = r.bits(3) original_copy = r.bits(1) home = r.bits(1) # the fixed header has to be the same for every frame in the stream fixed_header_key = ( id_, layer, protection_absent, profile, sampling_frequency_index, private_bit, channel_configuration, original_copy, home, ) if self._fixed_header_key is None: self._fixed_header_key = fixed_header_key else: if self._fixed_header_key != fixed_header_key: return False # adts_variable_header r.skip(2) # copyright_identification_bit/start frame_length = r.bits(13) r.skip(11) # adts_buffer_fullness nordbif = r.bits(2) # adts_variable_header end crc_overhead = 0 if not protection_absent: crc_overhead += (nordbif + 1) * 16 if nordbif != 0: crc_overhead *= 2 left = (frame_length * 8) - (r.get_position() - start) if left < 0: return False r.skip(left) assert r.is_aligned() self._payload += (left - crc_overhead) / 8 self._samples += (nordbif + 1) * 1024 self._last = r.get_position() / 8 self.parsed_frames += 1 return True class ProgramConfigElement(object): element_instance_tag = None object_type = None sampling_frequency_index = None channels = None def __init__(self, r): """Reads the program_config_element() Raises BitReaderError """ self.element_instance_tag = r.bits(4) self.object_type = r.bits(2) self.sampling_frequency_index = r.bits(4) num_front_channel_elements = r.bits(4) num_side_channel_elements = r.bits(4) num_back_channel_elements = r.bits(4) num_lfe_channel_elements = r.bits(2) num_assoc_data_elements = r.bits(3) num_valid_cc_elements = r.bits(4) mono_mixdown_present = r.bits(1) if mono_mixdown_present == 1: r.skip(4) stereo_mixdown_present = r.bits(1) if stereo_mixdown_present == 1: r.skip(4) matrix_mixdown_idx_present = r.bits(1) if matrix_mixdown_idx_present == 1: r.skip(3) elms = num_front_channel_elements + num_side_channel_elements + \ num_back_channel_elements channels = 0 for i in xrange(elms): channels += 1 element_is_cpe = r.bits(1) if element_is_cpe: channels += 1 r.skip(4) channels += num_lfe_channel_elements self.channels = channels r.skip(4 * num_lfe_channel_elements) r.skip(4 * num_assoc_data_elements) r.skip(5 * num_valid_cc_elements) r.align() comment_field_bytes = r.bits(8) r.skip(8 * comment_field_bytes) class AACError(MutagenError): pass class AACInfo(StreamInfo): """AAC 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 The length of the stream is just a guess and might not be correct. """ channels = 0 length = 0 sample_rate = 0 bitrate = 0 def __init__(self, fileobj): # skip id3v2 header start_offset = 0 header = fileobj.read(10) from mutagen.id3 import BitPaddedInt if header.startswith(b"ID3"): size = BitPaddedInt(header[6:]) start_offset = size + 10 fileobj.seek(start_offset) adif = fileobj.read(4) if adif == b"ADIF": self._parse_adif(fileobj) self._type = "ADIF" else: self._parse_adts(fileobj, start_offset) self._type = "ADTS" def _parse_adif(self, fileobj): r = BitReader(fileobj) try: copyright_id_present = r.bits(1) if copyright_id_present: r.skip(72) # copyright_id r.skip(1 + 1) # original_copy, home bitstream_type = r.bits(1) self.bitrate = r.bits(23) npce = r.bits(4) if bitstream_type == 0: r.skip(20) # adif_buffer_fullness pce = ProgramConfigElement(r) try: self.sample_rate = _FREQS[pce.sampling_frequency_index] except IndexError: pass self.channels = pce.channels # other pces.. for i in xrange(npce): ProgramConfigElement(r) r.align() except BitReaderError as e: raise AACError(e) # use bitrate + data size to guess length start = fileobj.tell() fileobj.seek(0, 2) length = fileobj.tell() - start if self.bitrate != 0: self.length = (8.0 * length) / self.bitrate def _parse_adts(self, fileobj, start_offset): max_initial_read = 512 max_resync_read = 10 max_sync_tries = 10 frames_max = 100 frames_needed = 3 # Try up to X times to find a sync word and read up to Y frames. # If more than Z frames are valid we assume a valid stream offset = start_offset for i in xrange(max_sync_tries): fileobj.seek(offset) s = _ADTSStream.find_stream(fileobj, max_initial_read) if s is None: raise AACError("sync not found") # start right after the last found offset offset += s.offset + 1 for i in xrange(frames_max): if not s.parse_frame(): break if not s.sync(max_resync_read): break if s.parsed_frames >= frames_needed: break else: raise AACError( "no valid stream found (only %d frames)" % s.parsed_frames) self.sample_rate = s.frequency self.channels = s.channels self.bitrate = s.bitrate # size from stream start to end of file fileobj.seek(0, 2) stream_size = fileobj.tell() - (offset + s.offset) # approx self.length = float(s.samples * stream_size) / (s.size * s.frequency) def pprint(self): return u"AAC (%s), %d Hz, %.2f seconds, %d channel(s), %d bps" % ( self._type, self.sample_rate, self.length, self.channels, self.bitrate) class AAC(FileType): """Load ADTS or ADIF streams containing AAC. Tagging is not supported. Use the ID3/APEv2 classes directly instead. """ _mimes = ["audio/x-aac"] def load(self, filename): self.filename = filename with open(filename, "rb") as h: self.info = AACInfo(h) def add_tags(self): raise AACError("doesn't support tags") @staticmethod def score(filename, fileobj, header): filename = filename.lower() s = endswith(filename, ".aac") or endswith(filename, ".adts") or \ endswith(filename, ".adif") s += b"ADIF" in header return s Open = AAC error = AACError __all__ = ["AAC", "Open"] mutagen-1.31/mutagen/_toolsutil.py0000644000175000017500000001463112574061222017541 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2015 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. import os import sys import signal import locale import contextlib import optparse import ctypes from ._compat import text_type, PY2, PY3, iterbytes def split_escape(string, sep, maxsplit=None, escape_char="\\"): """Like unicode/str/bytes.split but allows for the separator to be escaped If passed unicode/str/bytes will only return list of unicode/str/bytes. """ assert len(sep) == 1 assert len(escape_char) == 1 if isinstance(string, bytes): if isinstance(escape_char, text_type): escape_char = escape_char.encode("ascii") iter_ = iterbytes else: iter_ = iter if maxsplit is None: maxsplit = len(string) empty = string[:0] result = [] current = empty escaped = False for char in iter_(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 = empty else: current += char result.append(current) return result class SignalHandler(object): def __init__(self): self._interrupted = False self._nosig = False self._init = False def init(self): signal.signal(signal.SIGINT, self._handler) signal.signal(signal.SIGTERM, self._handler) if os.name != "nt": signal.signal(signal.SIGHUP, self._handler) def _handler(self, signum, frame): self._interrupted = True if not self._nosig: raise SystemExit("Aborted...") @contextlib.contextmanager def block(self): """While this context manager is active any signals for aborting the process will be queued and exit the program once the context is left. """ self._nosig = True yield self._nosig = False if self._interrupted: raise SystemExit("Aborted...") def get_win32_unicode_argv(): """Returns a unicode argv under Windows and standard sys.argv otherwise""" if os.name != "nt" or not PY2: return sys.argv import ctypes from ctypes import cdll, windll, wintypes GetCommandLineW = cdll.kernel32.GetCommandLineW GetCommandLineW.argtypes = [] GetCommandLineW.restype = wintypes.LPCWSTR CommandLineToArgvW = windll.shell32.CommandLineToArgvW CommandLineToArgvW.argtypes = [ wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_int)] CommandLineToArgvW.restype = ctypes.POINTER(wintypes.LPWSTR) LocalFree = windll.kernel32.LocalFree LocalFree.argtypes = [wintypes.HLOCAL] LocalFree.restype = wintypes.HLOCAL argc = ctypes.c_int() argv = CommandLineToArgvW(GetCommandLineW(), ctypes.byref(argc)) if not argv: return res = argv[max(0, argc.value - len(sys.argv)):argc.value] LocalFree(argv) return res def fsencoding(): """The encoding used for paths, argv, environ, stdout and stdin""" if os.name == "nt": return "" return locale.getpreferredencoding() or "utf-8" def fsnative(text=u""): """Returns the passed text converted to the preferred path type for each platform. """ assert isinstance(text, text_type) if os.name == "nt" or PY3: return text else: return text.encode(fsencoding(), "replace") return text def is_fsnative(arg): """If the passed value is of the preferred path type for each platform. Note that on Python3+linux, paths can be bytes or str but this returns False for bytes there. """ if PY3 or os.name == "nt": return isinstance(arg, text_type) else: return isinstance(arg, bytes) def print_(*objects, **kwargs): """A print which supports bytes and str+surrogates under python3. Needed so we can print anything passed to us through argv and environ. Under Windows only text_type is allowed. Arguments: objects: one or more bytes/text linesep (bool): whether a line separator should be appended sep (bool): whether objects should be printed separated by spaces """ linesep = kwargs.pop("linesep", True) sep = kwargs.pop("sep", True) file_ = kwargs.pop("file", None) if file_ is None: file_ = sys.stdout old_cp = None if os.name == "nt": # Try to force the output to cp65001 aka utf-8. # If that fails use the current one (most likely cp850, so # most of unicode will be replaced with '?') encoding = "utf-8" old_cp = ctypes.windll.kernel32.GetConsoleOutputCP() if ctypes.windll.kernel32.SetConsoleOutputCP(65001) == 0: encoding = getattr(sys.stdout, "encoding", None) or "utf-8" old_cp = None else: encoding = fsencoding() try: if linesep: objects = list(objects) + [os.linesep] parts = [] for text in objects: if isinstance(text, text_type): if PY3: try: text = text.encode(encoding, 'surrogateescape') except UnicodeEncodeError: text = text.encode(encoding, 'replace') else: text = text.encode(encoding, 'replace') parts.append(text) data = (b" " if sep else b"").join(parts) try: fileno = file_.fileno() except (AttributeError, OSError, ValueError): # for tests when stdout is replaced try: file_.write(data) except TypeError: file_.write(data.decode(encoding, "replace")) else: file_.flush() os.write(fileno, data) finally: # reset the code page to what we had before if old_cp is not None: ctypes.windll.kernel32.SetConsoleOutputCP(old_cp) class OptionParser(optparse.OptionParser): """OptionParser subclass which supports printing Unicode under Windows""" def print_help(self, file=None): print_(self.format_help(), file=file) mutagen-1.31/mutagen/m4a.py0000644000175000017500000000367412574061222016032 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # 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. """ since 1.9: mutagen.m4a is deprecated; use mutagen.mp4 instead. since 1.31: mutagen.m4a will no longer work; any operation that could fail will fail now. """ import warnings from mutagen import FileType, Metadata, StreamInfo from ._util import DictProxy, MutagenError warnings.warn( "mutagen.m4a is deprecated; use mutagen.mp4 instead.", DeprecationWarning) class error(IOError, MutagenError): pass class M4AMetadataError(error): pass class M4AStreamInfoError(error): pass class M4AMetadataValueError(ValueError, M4AMetadataError): pass __all__ = ['M4A', 'Open', 'delete', 'M4ACover'] class M4ACover(bytes): FORMAT_JPEG = 0x0D FORMAT_PNG = 0x0E def __new__(cls, data, imageformat=None): self = bytes.__new__(cls, data) if imageformat is None: imageformat = M4ACover.FORMAT_JPEG self.imageformat = imageformat return self class M4ATags(DictProxy, Metadata): def load(self, atoms, fileobj): raise error("deprecated") def save(self, filename): raise error("deprecated") def delete(self, filename): raise error("deprecated") def pprint(self): return u"" class M4AInfo(StreamInfo): bitrate = 0 def __init__(self, atoms, fileobj): raise error("deprecated") def pprint(self): return u"" class M4A(FileType): _mimes = ["audio/mp4", "audio/x-m4a", "audio/mpeg4", "audio/aac"] def load(self, filename): raise error("deprecated") def add_tags(self): self.tags = M4ATags() @staticmethod def score(filename, fileobj, header): return 0 Open = M4A def delete(filename): raise error("deprecated") mutagen-1.31/tests/0000755000175000017500000000000012574256060014475 5ustar lazkalazka00000000000000mutagen-1.31/tests/test_tools_mutagen_pony.py0000644000175000017500000000060012574061222022021 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os from tests.test_tools import _TTools from mutagen._toolsutil import fsnative as fsn class TMutagenPony(_TTools): TOOL_NAME = u"mutagen-pony" def test_basic(self): base = os.path.join(fsn(u'tests'), fsn(u'data')) res, out = self.call(base) self.failIf(res) self.failUnless("Report for %s" % base in out) mutagen-1.31/tests/test_aiff.py0000644000175000017500000002367712574113303017022 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os import shutil from tests import TestCase, DATA_DIR from mutagen._compat import cBytesIO from mutagen.aiff import AIFF, AIFFInfo, delete, IFFFile, IFFChunk from mutagen.aiff import error as AIFFError from tempfile import mkstemp class TAIFF(TestCase): silence_1 = os.path.join(DATA_DIR, '11k-1ch-2s-silence.aif') silence_2 = os.path.join(DATA_DIR, '48k-2ch-s16-silence.aif') silence_3 = os.path.join(DATA_DIR, '8k-1ch-1s-silence.aif') silence_4 = os.path.join(DATA_DIR, '8k-1ch-3.5s-silence.aif') silence_5 = os.path.join(DATA_DIR, '8k-4ch-1s-silence.aif') has_tags = os.path.join(DATA_DIR, 'with-id3.aif') no_tags = os.path.join(DATA_DIR, '8k-1ch-1s-silence.aif') def setUp(self): fd, self.filename_1 = mkstemp(suffix='.aif') os.close(fd) shutil.copy(self.has_tags, self.filename_1) fd, self.filename_2 = mkstemp(suffix='.aif') os.close(fd) shutil.copy(self.no_tags, self.filename_2) self.aiff_tmp_id3 = AIFF(self.filename_1) self.aiff_tmp_no_id3 = AIFF(self.filename_2) self.aiff_1 = AIFF(self.silence_1) self.aiff_2 = AIFF(self.silence_2) self.aiff_3 = AIFF(self.silence_3) self.aiff_4 = AIFF(self.silence_4) self.aiff_5 = AIFF(self.silence_5) def test_channels(self): self.failUnlessEqual(self.aiff_1.info.channels, 1) self.failUnlessEqual(self.aiff_2.info.channels, 2) self.failUnlessEqual(self.aiff_3.info.channels, 1) self.failUnlessEqual(self.aiff_4.info.channels, 1) self.failUnlessEqual(self.aiff_5.info.channels, 4) def test_length(self): self.failUnlessEqual(self.aiff_1.info.length, 2) self.failUnlessEqual(self.aiff_2.info.length, 0.1) self.failUnlessEqual(self.aiff_3.info.length, 1) self.failUnlessEqual(self.aiff_4.info.length, 3.5) self.failUnlessEqual(self.aiff_5.info.length, 1) def test_bitrate(self): self.failUnlessEqual(self.aiff_1.info.bitrate, 176400) self.failUnlessEqual(self.aiff_2.info.bitrate, 1536000) self.failUnlessEqual(self.aiff_3.info.bitrate, 128000) self.failUnlessEqual(self.aiff_4.info.bitrate, 128000) self.failUnlessEqual(self.aiff_5.info.bitrate, 512000) def test_sample_rate(self): self.failUnlessEqual(self.aiff_1.info.sample_rate, 11025) self.failUnlessEqual(self.aiff_2.info.sample_rate, 48000) self.failUnlessEqual(self.aiff_3.info.sample_rate, 8000) self.failUnlessEqual(self.aiff_4.info.sample_rate, 8000) self.failUnlessEqual(self.aiff_5.info.sample_rate, 8000) def test_sample_size(self): self.failUnlessEqual(self.aiff_1.info.sample_size, 16) self.failUnlessEqual(self.aiff_2.info.sample_size, 16) self.failUnlessEqual(self.aiff_3.info.sample_size, 16) self.failUnlessEqual(self.aiff_4.info.sample_size, 16) self.failUnlessEqual(self.aiff_5.info.sample_size, 16) def test_notaiff(self): self.failUnlessRaises( AIFFError, AIFF, os.path.join(DATA_DIR, 'empty.ofr')) def test_pprint(self): self.failUnless(self.aiff_1.pprint()) self.failUnless(self.aiff_tmp_id3.pprint()) def test_delete(self): self.aiff_tmp_id3.delete() self.failIf(self.aiff_tmp_id3.tags) self.failUnless(AIFF(self.filename_1).tags is None) def test_module_delete(self): delete(self.filename_1) self.failUnless(AIFF(self.filename_1).tags is None) def test_module_double_delete(self): delete(self.filename_1) delete(self.filename_1) def test_pprint_no_tags(self): self.aiff_tmp_id3.tags = None self.failUnless(self.aiff_tmp_id3.pprint()) def test_save_no_tags(self): self.aiff_tmp_id3.tags = None self.aiff_tmp_id3.save() self.assertTrue(self.aiff_tmp_id3.tags is None) def test_add_tags_already_there(self): self.failUnless(self.aiff_tmp_id3.tags) self.failUnlessRaises(Exception, self.aiff_tmp_id3.add_tags) def test_mime(self): self.failUnless("audio/aiff" in self.aiff_1.mime) self.failUnless("audio/x-aiff" in self.aiff_1.mime) def test_loaded_tags(self): self.failUnless(self.aiff_tmp_id3["TIT2"] == "AIFF title") def test_roundtrip(self): self.failUnlessEqual(self.aiff_tmp_id3["TIT2"], ["AIFF title"]) self.aiff_tmp_id3.save() new = AIFF(self.aiff_tmp_id3.filename) self.failUnlessEqual(new["TIT2"], ["AIFF title"]) def test_save_tags(self): from mutagen.id3 import TIT1 tags = self.aiff_tmp_id3.tags tags.add(TIT1(encoding=3, text="foobar")) tags.save() new = AIFF(self.aiff_tmp_id3.filename) self.failUnlessEqual(new["TIT1"], ["foobar"]) def test_save_with_ID3_chunk(self): from mutagen.id3 import TIT1 self.aiff_tmp_id3["TIT1"] = TIT1(encoding=3, text="foobar") self.aiff_tmp_id3.save() self.failUnless(AIFF(self.filename_1)["TIT1"] == "foobar") self.failUnless(self.aiff_tmp_id3["TIT2"] == "AIFF title") def test_save_without_ID3_chunk(self): from mutagen.id3 import TIT1 self.aiff_tmp_no_id3["TIT1"] = TIT1(encoding=3, text="foobar") self.aiff_tmp_no_id3.save() self.failUnless(AIFF(self.filename_2)["TIT1"] == "foobar") def test_corrupt_tag(self): with open(self.filename_1, "r+b") as h: chunk = IFFFile(h)[u'ID3'] h.seek(chunk.data_offset) h.seek(4, 1) h.write(b"\xff\xff") self.assertRaises(AIFFError, AIFF, self.filename_1) def test_padding(self): AIFF(self.filename_1).save() self.assertEqual(AIFF(self.filename_1).tags._padding, 1002) AIFF(self.filename_1).save() self.assertEqual(AIFF(self.filename_1).tags._padding, 1002) tags = AIFF(self.filename_1) tags.save(padding=lambda x: 1) self.assertEqual(AIFF(self.filename_1).tags._padding, 1) tags = AIFF(self.filename_1) tags.save(padding=lambda x: 100) self.assertEqual(AIFF(self.filename_1).tags._padding, 100) tags = AIFF(self.filename_1) self.assertRaises(AIFFError, tags.save, padding=lambda x: -1) def tearDown(self): os.unlink(self.filename_1) os.unlink(self.filename_2) class TAIFFInfo(TestCase): def test_empty(self): fileobj = cBytesIO(b"") self.failUnlessRaises(IOError, AIFFInfo, fileobj) class TIFFFile(TestCase): has_tags = os.path.join(DATA_DIR, 'with-id3.aif') no_tags = os.path.join(DATA_DIR, '8k-1ch-1s-silence.aif') def setUp(self): self.file_1 = open(self.has_tags, 'rb') self.iff_1 = IFFFile(self.file_1) self.file_2 = open(self.no_tags, 'rb') self.iff_2 = IFFFile(self.file_2) fd_1, self.tmp_1_name = mkstemp(suffix='.aif') shutil.copy(self.has_tags, self.tmp_1_name) self.file_1_tmp = open(self.tmp_1_name, 'rb+') self.iff_1_tmp = IFFFile(self.file_1_tmp) fd_2, self.tmp_2_name = mkstemp(suffix='.aif') shutil.copy(self.no_tags, self.tmp_2_name) self.file_2_tmp = open(self.tmp_2_name, 'rb+') self.iff_2_tmp = IFFFile(self.file_2_tmp) def tearDown(self): self.file_1.close() self.file_2.close() self.file_1_tmp.close() self.file_2_tmp.close() os.unlink(self.tmp_1_name) os.unlink(self.tmp_2_name) def test_has_chunks(self): self.failUnless(u'FORM' in self.iff_1) self.failUnless(u'COMM' in self.iff_1) self.failUnless(u'SSND' in self.iff_1) self.failUnless(u'ID3' in self.iff_1) self.failUnless(u'FORM' in self.iff_2) self.failUnless(u'COMM' in self.iff_2) self.failUnless(u'SSND' in self.iff_2) def test_is_chunks(self): self.failUnless(isinstance(self.iff_1[u'FORM'], IFFChunk)) self.failUnless(isinstance(self.iff_1[u'COMM'], IFFChunk)) self.failUnless(isinstance(self.iff_1[u'SSND'], IFFChunk)) self.failUnless(isinstance(self.iff_1[u'ID3'], IFFChunk)) def test_chunk_size(self): self.failUnlessEqual(self.iff_1[u'FORM'].size, 17096) self.failUnlessEqual(self.iff_2[u'FORM'].size, 16054) def test_chunk_data_size(self): self.failUnlessEqual(self.iff_1[u'FORM'].data_size, 17088) self.failUnlessEqual(self.iff_2[u'FORM'].data_size, 16046) def test_FORM_chunk_resize(self): self.iff_1_tmp[u'FORM'].resize(17000) self.failUnlessEqual( IFFFile(self.file_1_tmp)[u'FORM'].data_size, 17000) self.iff_2_tmp[u'FORM'].resize(0) self.failUnlessEqual(IFFFile(self.file_2_tmp)[u'FORM'].data_size, 0) def test_child_chunk_resize(self): self.iff_1_tmp[u'ID3'].resize(128) id3 = self.iff_1_tmp[u'ID3'] id3.write(b"\xff" * 128) self.assertEqual(id3.read(), b"\xff" * 128) self.failUnlessEqual(IFFFile(self.file_1_tmp)[u'ID3'].data_size, 128) self.failUnlessEqual( IFFFile(self.file_1_tmp)[u'FORM'].data_size, 16182) def test_chunk_delete(self): del self.iff_1_tmp[u'ID3'] self.failIf(u'ID3' in self.iff_1_tmp) self.failIf(u'ID3' in IFFFile(self.file_1_tmp)) self.failUnlessEqual(IFFFile(self.file_1_tmp)[u'FORM'].size, 16054) del self.iff_2_tmp[u'SSND'] self.failIf(u'SSND' in self.iff_2_tmp) self.failIf(u'SSND' in IFFFile(self.file_2_tmp)) self.failUnlessEqual(IFFFile(self.file_2_tmp)[u'FORM'].size, 38) def test_insert_chunk(self): self.iff_2_tmp.insert_chunk(u'ID3') new_iff = IFFFile(self.file_2_tmp) self.failUnless(u'ID3' in new_iff) self.failUnless(isinstance(new_iff[u'ID3'], IFFChunk)) self.failUnlessEqual(new_iff[u'FORM'].size, 16062) self.failUnlessEqual(new_iff[u'FORM'].data_size, 16054) self.failUnlessEqual(new_iff[u'ID3'].size, 8) self.failUnlessEqual(new_iff[u'ID3'].data_size, 0) mutagen-1.31/tests/test_tools_mid3cp.py0000644000175000017500000001123612574061222020502 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2014 Ben Ockmore # 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. """Tests for mid3cp tool. Since the tool is quite simple, most of the functionality is covered by the mutagen package tests - these simply test usage. """ import os from tempfile import mkstemp import shutil from mutagen.id3 import ID3, ParseID3v1 from mutagen._toolsutil import fsnative as fsn from tests.test_tools import _TTools from tests import DATA_DIR class TMid3cp(_TTools): TOOL_NAME = u"mid3cp" def setUp(self): super(TMid3cp, self).setUp() original = os.path.join(DATA_DIR, fsn(u'silence-44-s.mp3')) fd, self.filename = mkstemp(suffix=fsn(u'öäü.mp3')) os.close(fd) shutil.copy(original, self.filename) fd, self.blank_file = mkstemp(suffix=fsn(u'.mp3')) os.close(fd) def tearDown(self): super(TMid3cp, self).tearDown() os.unlink(self.filename) os.unlink(self.blank_file) def test_noop(self): res, out, err = self.call2() self.assertNotEqual(res, 0) self.failUnless("Usage:" in err) def test_src_equal_dst(self): res = self.call2(self.filename, self.filename)[0] self.assertEqual(res, 0) def test_copy(self): res = self.call(self.filename, self.blank_file)[0] self.failIf(res) original_id3 = ID3(self.filename) copied_id3 = ID3(self.blank_file) self.failUnlessEqual(original_id3, copied_id3) for key in original_id3: # Go through every tag in the original file, and check that it's # present and correct in the copy self.failUnless(key in copied_id3) self.failUnlessEqual(copied_id3[key], original_id3[key]) def test_include_id3v1(self): self.call(fsn(u'--write-v1'), self.filename, self.blank_file) with open(self.blank_file, 'rb') as fileobj: fileobj.seek(-128, 2) frames = ParseID3v1(fileobj.read(128)) # If ID3v1 frames are present, assume they've been written correctly by # mutagen, so no need to check them self.failUnless(frames) def test_exclude_tag_unicode(self): self.call(fsn(u'-x'), fsn(u''), self.filename, self.blank_file) def test_exclude_single_tag(self): self.call(fsn(u'-x'), fsn(u'TLEN'), self.filename, self.blank_file) original_id3 = ID3(self.filename) copied_id3 = ID3(self.blank_file) self.failUnless('TLEN' in original_id3) self.failIf('TLEN' in copied_id3) def test_exclude_multiple_tag(self): self.call(fsn(u'-x'), fsn(u'TLEN'), fsn(u'-x'), fsn(u'TCON'), fsn(u'-x'), fsn(u'TALB'), self.filename, self.blank_file) original_id3 = ID3(self.filename) copied_id3 = ID3(self.blank_file) self.failUnless('TLEN' in original_id3) self.failUnless('TCON' in original_id3) self.failUnless('TALB' in original_id3) self.failIf('TLEN' in copied_id3) self.failIf('TCON' in copied_id3) self.failIf('TALB' in copied_id3) def test_no_src_header(self): fd, blank_file2 = mkstemp(suffix=fsn(u'.mp3')) os.close(fd) try: err = self.call2(self.blank_file, blank_file2)[2] self.failUnless("No ID3 header found" in err) finally: os.unlink(blank_file2) def test_verbose(self): err = self.call2(self.filename, fsn(u"--verbose"), self.blank_file)[2] self.failUnless('mp3 contains:' in err) self.failUnless('Successfully saved' in err) def test_quiet(self): out = self.call(self.filename, self.blank_file)[1] self.failIf(out) def test_exit_status(self): status, out, err = self.call2(self.filename) self.assertTrue(status) status, out, err = self.call2(self.filename, self.filename) self.assertFalse(status) status, out, err = self.call2(self.blank_file, self.filename) self.assertTrue(status) status, out, err = self.call2(fsn(u""), self.filename) self.assertTrue(status) status, out, err = self.call2(self.filename, self.blank_file) self.assertFalse(status) def test_v23_v24(self): self.assertEqual(ID3(self.filename).version, (2, 3, 0)) self.call(self.filename, self.blank_file) self.assertEqual(ID3(self.blank_file).version, (2, 3, 0)) ID3(self.filename).save() self.call(self.filename, self.blank_file) self.assertEqual(ID3(self.blank_file).version, (2, 4, 0)) mutagen-1.31/tests/test__util.py0000644000175000017500000005526712574061222017233 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- from mutagen._util import DictMixin, cdata, insert_bytes, delete_bytes from mutagen._util import decode_terminated, dict_match, enum, get_size from mutagen._util import BitReader, BitReaderError, resize_bytes from mutagen._compat import text_type, itervalues, iterkeys, iteritems, PY2, \ cBytesIO, xrange from tests import TestCase import random import mmap try: import fcntl except ImportError: fcntl = None 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 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) if PY2: 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(iterkeys(self.fdict)), list(iterkeys(self.rdict))) def test_values(self): self.failUnlessEqual( list(self.fdict.values()), list(self.rdict.values())) self.failUnlessEqual( list(itervalues(self.fdict)), list(itervalues(self.rdict))) def test_items(self): self.failUnlessEqual( list(self.fdict.items()), list(self.rdict.items())) self.failUnlessEqual( list(iteritems(self.fdict)), list(iteritems(self.rdict))) 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) class Tcdata(TestCase): ZERO = staticmethod(lambda s: b"\x00" * s) LEONE = staticmethod(lambda s: b"\x01" + b"\x00" * (s - 1)) BEONE = staticmethod(lambda s: b"\x00" * (s - 1) + b"\x01") NEGONE = staticmethod(lambda s: b"\xff" * s) def test_char(self): self.failUnlessEqual(cdata.char(self.ZERO(1)), 0) self.failUnlessEqual(cdata.char(self.LEONE(1)), 1) self.failUnlessEqual(cdata.char(self.BEONE(1)), 1) self.failUnlessEqual(cdata.char(self.NEGONE(1)), -1) self.assertTrue(cdata.char is cdata.int8) self.assertTrue(cdata.to_char is cdata.to_int8) self.assertTrue(cdata.char_from is cdata.int8_from) def test_char_from_to(self): self.assertEqual(cdata.to_char(-2), b"\xfe") self.assertEqual(cdata.char_from(b"\xfe"), (-2, 1)) self.assertEqual(cdata.char_from(b"\x00\xfe", 1), (-2, 2)) self.assertRaises(cdata.error, cdata.char_from, b"\x00\xfe", 3) def test_uchar(self): self.failUnlessEqual(cdata.uchar(self.ZERO(1)), 0) self.failUnlessEqual(cdata.uchar(self.LEONE(1)), 1) self.failUnlessEqual(cdata.uchar(self.BEONE(1)), 1) self.failUnlessEqual(cdata.uchar(self.NEGONE(1)), 255) self.assertTrue(cdata.uchar is cdata.uint8) self.assertTrue(cdata.to_uchar is cdata.to_uint8) self.assertTrue(cdata.uchar_from is cdata.uint8_from) def test_short(self): self.failUnlessEqual(cdata.short_le(self.ZERO(2)), 0) self.failUnlessEqual(cdata.short_le(self.LEONE(2)), 1) self.failUnlessEqual(cdata.short_le(self.BEONE(2)), 256) self.failUnlessEqual(cdata.short_le(self.NEGONE(2)), -1) self.assertTrue(cdata.short_le is cdata.int16_le) self.failUnlessEqual(cdata.short_be(self.ZERO(2)), 0) self.failUnlessEqual(cdata.short_be(self.LEONE(2)), 256) self.failUnlessEqual(cdata.short_be(self.BEONE(2)), 1) self.failUnlessEqual(cdata.short_be(self.NEGONE(2)), -1) self.assertTrue(cdata.short_be is cdata.int16_be) def test_ushort(self): self.failUnlessEqual(cdata.ushort_le(self.ZERO(2)), 0) self.failUnlessEqual(cdata.ushort_le(self.LEONE(2)), 1) self.failUnlessEqual(cdata.ushort_le(self.BEONE(2)), 2 ** 16 >> 8) self.failUnlessEqual(cdata.ushort_le(self.NEGONE(2)), 65535) self.assertTrue(cdata.ushort_le is cdata.uint16_le) self.failUnlessEqual(cdata.ushort_be(self.ZERO(2)), 0) self.failUnlessEqual(cdata.ushort_be(self.LEONE(2)), 2 ** 16 >> 8) self.failUnlessEqual(cdata.ushort_be(self.BEONE(2)), 1) self.failUnlessEqual(cdata.ushort_be(self.NEGONE(2)), 65535) self.assertTrue(cdata.ushort_be is cdata.uint16_be) def test_int(self): self.failUnlessEqual(cdata.int_le(self.ZERO(4)), 0) self.failUnlessEqual(cdata.int_le(self.LEONE(4)), 1) self.failUnlessEqual(cdata.int_le(self.BEONE(4)), 2 ** 32 >> 8) self.failUnlessEqual(cdata.int_le(self.NEGONE(4)), -1) self.assertTrue(cdata.int_le is cdata.int32_le) self.failUnlessEqual(cdata.int_be(self.ZERO(4)), 0) self.failUnlessEqual(cdata.int_be(self.LEONE(4)), 2 ** 32 >> 8) self.failUnlessEqual(cdata.int_be(self.BEONE(4)), 1) self.failUnlessEqual(cdata.int_be(self.NEGONE(4)), -1) self.assertTrue(cdata.int_be is cdata.int32_be) def test_uint(self): self.failUnlessEqual(cdata.uint_le(self.ZERO(4)), 0) self.failUnlessEqual(cdata.uint_le(self.LEONE(4)), 1) self.failUnlessEqual(cdata.uint_le(self.BEONE(4)), 2 ** 32 >> 8) self.failUnlessEqual(cdata.uint_le(self.NEGONE(4)), 2 ** 32 - 1) self.assertTrue(cdata.uint_le is cdata.uint32_le) self.failUnlessEqual(cdata.uint_be(self.ZERO(4)), 0) self.failUnlessEqual(cdata.uint_be(self.LEONE(4)), 2 ** 32 >> 8) self.failUnlessEqual(cdata.uint_be(self.BEONE(4)), 1) self.failUnlessEqual(cdata.uint_be(self.NEGONE(4)), 2 ** 32 - 1) self.assertTrue(cdata.uint_be is cdata.uint32_be) def test_longlong(self): self.failUnlessEqual(cdata.longlong_le(self.ZERO(8)), 0) self.failUnlessEqual(cdata.longlong_le(self.LEONE(8)), 1) self.failUnlessEqual(cdata.longlong_le(self.BEONE(8)), 2 ** 64 >> 8) self.failUnlessEqual(cdata.longlong_le(self.NEGONE(8)), -1) self.assertTrue(cdata.longlong_le is cdata.int64_le) self.failUnlessEqual(cdata.longlong_be(self.ZERO(8)), 0) self.failUnlessEqual(cdata.longlong_be(self.LEONE(8)), 2 ** 64 >> 8) self.failUnlessEqual(cdata.longlong_be(self.BEONE(8)), 1) self.failUnlessEqual(cdata.longlong_be(self.NEGONE(8)), -1) self.assertTrue(cdata.longlong_be is cdata.int64_be) def test_ulonglong(self): self.failUnlessEqual(cdata.ulonglong_le(self.ZERO(8)), 0) self.failUnlessEqual(cdata.ulonglong_le(self.LEONE(8)), 1) self.failUnlessEqual(cdata.longlong_le(self.BEONE(8)), 2 ** 64 >> 8) self.failUnlessEqual(cdata.ulonglong_le(self.NEGONE(8)), 2 ** 64 - 1) self.assertTrue(cdata.ulonglong_le is cdata.uint64_le) self.failUnlessEqual(cdata.ulonglong_be(self.ZERO(8)), 0) self.failUnlessEqual(cdata.ulonglong_be(self.LEONE(8)), 2 ** 64 >> 8) self.failUnlessEqual(cdata.longlong_be(self.BEONE(8)), 1) self.failUnlessEqual(cdata.ulonglong_be(self.NEGONE(8)), 2 ** 64 - 1) self.assertTrue(cdata.ulonglong_be is cdata.uint64_be) def test_invalid_lengths(self): self.failUnlessRaises(cdata.error, cdata.char, b"") self.failUnlessRaises(cdata.error, cdata.uchar, b"") self.failUnlessRaises(cdata.error, cdata.int_le, b"") self.failUnlessRaises(cdata.error, cdata.longlong_le, b"") self.failUnlessRaises(cdata.error, cdata.uint_le, b"") self.failUnlessRaises(cdata.error, cdata.ulonglong_le, b"") self.failUnlessRaises(cdata.error, cdata.int_be, b"") self.failUnlessRaises(cdata.error, cdata.longlong_be, b"") self.failUnlessRaises(cdata.error, cdata.uint_be, b"") self.failUnlessRaises(cdata.error, cdata.ulonglong_be, b"") 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)) 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_resize_decrease(self): o = self.file(b'abcd') resize_bytes(o, 2, 1, 1) self.assertEqual(self.read(o), b"abd") def test_resize_increase(self): o = self.file(b'abcd') resize_bytes(o, 2, 4, 1) self.assertEqual(self.read(o), b"abcd\x00d") def test_resize_nothing(self): o = self.file(b'abcd') resize_bytes(o, 2, 2, 1) self.assertEqual(self.read(o), b"abcd") def test_insert_into_empty(self): o = self.file(b'') insert_bytes(o, 8, 0) self.assertEquals(b'\x00' * 8, self.read(o)) def test_insert_before_one(self): o = self.file(b'a') insert_bytes(o, 8, 0) self.assertEquals(b'a' + b'\x00' * 7 + b'a', self.read(o)) def test_insert_after_one(self): o = self.file(b'a') insert_bytes(o, 8, 1) self.assertEquals(b'a' + b'\x00' * 8, self.read(o)) def test_smaller_than_file_middle(self): o = self.file(b'abcdefghij') insert_bytes(o, 4, 4) self.assertEquals(b'abcdefghefghij', self.read(o)) def test_smaller_than_file_to_end(self): o = self.file(b'abcdefghij') insert_bytes(o, 4, 6) self.assertEquals(b'abcdefghijghij', self.read(o)) def test_smaller_than_file_across_end(self): o = self.file(b'abcdefghij') insert_bytes(o, 4, 8) self.assertEquals(b'abcdefghij\x00\x00ij', self.read(o)) def test_smaller_than_file_at_end(self): o = self.file(b'abcdefghij') insert_bytes(o, 3, 10) self.assertEquals(b'abcdefghij\x00\x00\x00', self.read(o)) def test_smaller_than_file_at_beginning(self): o = self.file(b'abcdefghij') insert_bytes(o, 3, 0) self.assertEquals(b'abcabcdefghij', self.read(o)) def test_zero(self): o = self.file(b'abcdefghij') self.assertRaises((AssertionError, ValueError), insert_bytes, o, 0, 1) def test_negative(self): o = self.file(b'abcdefghij') self.assertRaises((AssertionError, ValueError), insert_bytes, o, 8, -1) def test_delete_one(self): o = self.file(b'a') delete_bytes(o, 1, 0) self.assertEquals(b'', self.read(o)) def test_delete_first_of_two(self): o = self.file(b'ab') delete_bytes(o, 1, 0) self.assertEquals(b'b', self.read(o)) def test_delete_second_of_two(self): o = self.file(b'ab') delete_bytes(o, 1, 1) self.assertEquals(b'a', self.read(o)) def test_delete_third_of_two(self): o = self.file(b'ab') self.assertRaises(AssertionError, delete_bytes, o, 1, 2) def test_delete_middle(self): o = self.file(b'abcdefg') delete_bytes(o, 3, 2) self.assertEquals(b'abfg', self.read(o)) def test_delete_across_end(self): o = self.file(b'abcdefg') self.assertRaises(AssertionError, delete_bytes, o, 4, 8) def test_delete_zero(self): o = self.file(b'abcdefg') self.assertRaises(AssertionError, delete_bytes, o, 0, 3) def test_delete_negative(self): o = self.file(b'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 = u''.join(map(text_type, xrange(12574))) # 51760 bytes data = data.encode("ascii") 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 = u''.join(map(text_type, xrange(12574))) # 51760 bytes data = data.encode("ascii") 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 xrange(num_runs): data = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ" * 1024 fobj = self.file(data) filesize = len(data) # Generate the list of changes to apply changes = [] for i in xrange(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) class FileHandlingMockedMMapMove(FileHandling): def setUp(self): class MockMMap(object): def __init__(self, *args, **kwargs): pass def move(self, dest, src, count): raise ValueError def close(self): pass self._orig_mmap = mmap.mmap mmap.mmap = MockMMap def tearDown(self): mmap.mmap = self._orig_mmap class FileHandlingMockedMMap(FileHandling): def setUp(self): def MockMMap2(*args, **kwargs): raise EnvironmentError self._orig_mmap = mmap.mmap mmap.mmap = MockMMap2 def tearDown(self): mmap.mmap = self._orig_mmap class Tdict_match(TestCase): def test_match(self): self.assertEqual(dict_match({"*": 1}, "a"), 1) self.assertEqual(dict_match({"*": 1}, "*"), 1) self.assertEqual(dict_match({"*a": 1}, "ba"), 1) self.assertEqual(dict_match({"?": 1}, "b"), 1) self.assertEqual(dict_match({"[ab]": 1}, "b"), 1) def test_nomatch(self): self.assertEqual(dict_match({"*a": 1}, "ab"), None) self.assertEqual(dict_match({"??": 1}, "a"), None) self.assertEqual(dict_match({"[ab]": 1}, "c"), None) self.assertEqual(dict_match({"[ab]": 1}, "[ab]"), None) class Tenum(TestCase): def test_enum(self): @enum class Foo(object): FOO = 1 BAR = 3 self.assertEqual(Foo.FOO, 1) self.assertTrue(isinstance(Foo.FOO, Foo)) self.assertEqual(repr(Foo.FOO), "") self.assertEqual(repr(Foo(3)), "") self.assertEqual(repr(Foo(42)), "42") self.assertEqual(str(Foo(42)), "42") self.assertEqual(int(Foo(42)), 42) self.assertEqual(str(Foo(1)), "Foo.FOO") self.assertEqual(int(Foo(1)), 1) self.assertTrue(isinstance(str(Foo.FOO), str)) self.assertTrue(isinstance(repr(Foo.FOO), str)) class Tget_size(TestCase): def test_get_size(self): f = cBytesIO(b"foo") f.seek(1, 0) self.assertEqual(f.tell(), 1) self.assertEqual(get_size(f), 3) self.assertEqual(f.tell(), 1) class Tdecode_terminated(TestCase): def test_all(self): values = [u"", u"", u"\xe4", u"abc", u"", u""] for codec in ["utf8", "utf-8", "utf-16", "latin-1", "utf-16be"]: # NULL without the BOM term = u"\x00".encode(codec)[-2:] data = b"".join(v.encode(codec) + term for v in values) for v in values: dec, data = decode_terminated(data, codec) self.assertEqual(dec, v) self.assertEqual(data, b"") def test_invalid(self): # invalid self.assertRaises( UnicodeDecodeError, decode_terminated, b"\xff", "utf-8") # truncated self.assertRaises( UnicodeDecodeError, decode_terminated, b"\xff\xfe\x00", "utf-16") # not null terminated self.assertRaises(ValueError, decode_terminated, b"abc", "utf-8") # invalid encoding self.assertRaises(LookupError, decode_terminated, b"abc", "foobar") def test_lax(self): # missing termination self.assertEqual( decode_terminated(b"abc", "utf-8", strict=False), (u"abc", b"")) # missing termination and truncated data truncated = u"\xe4\xe4".encode("utf-8")[:-1] self.assertRaises( UnicodeDecodeError, decode_terminated, truncated, "utf-8", strict=False) class TBitReader(TestCase): def test_bits(self): data = b"\x12\x34\x56\x78\x89\xAB\xCD\xEF" ref = cdata.uint64_be(data) for i in xrange(64): fo = cBytesIO(data) r = BitReader(fo) v = r.bits(i) << (64 - i) | r.bits(64 - i) self.assertEqual(v, ref) def test_read_too_much(self): r = BitReader(cBytesIO(b"")) self.assertEqual(r.bits(0), 0) self.assertRaises(BitReaderError, r.bits, 1) def test_skip(self): r = BitReader(cBytesIO(b"\xEF")) r.skip(4) self.assertEqual(r.bits(4), 0xf) def test_skip_more(self): r = BitReader(cBytesIO(b"\xAB\xCD")) self.assertEqual(r.bits(4), 0xa) r.skip(8) self.assertEqual(r.bits(4), 0xd) self.assertRaises(BitReaderError, r.bits, 1) def test_skip_too_much(self): r = BitReader(cBytesIO(b"\xAB\xCD")) # aligned skips don't fail, but the following read will r.skip(32 + 8) self.assertRaises(BitReaderError, r.bits, 1) self.assertRaises(BitReaderError, r.skip, 1) def test_bytes(self): r = BitReader(cBytesIO(b"\xAB\xCD\xEF")) self.assertEqual(r.bytes(2), b"\xAB\xCD") self.assertEqual(r.bytes(0), b"") def test_bytes_unaligned(self): r = BitReader(cBytesIO(b"\xAB\xCD\xEF")) r.skip(4) self.assertEqual(r.bytes(2), b"\xBC\xDE") def test_get_position(self): r = BitReader(cBytesIO(b"\xAB\xCD")) self.assertEqual(r.get_position(), 0) r.bits(3) self.assertEqual(r.get_position(), 3) r.skip(9) self.assertEqual(r.get_position(), 3 + 9) r.align() self.assertEqual(r.get_position(), 16) def test_align(self): r = BitReader(cBytesIO(b"\xAB\xCD\xEF")) r.skip(3) self.assertEqual(r.align(), 5) self.assertEqual(r.get_position(), 8) def test_is_aligned(self): r = BitReader(cBytesIO(b"\xAB\xCD\xEF")) self.assertTrue(r.is_aligned()) r.skip(1) self.assertFalse(r.is_aligned()) r.skip(7) self.assertTrue(r.is_aligned()) r.bits(7) self.assertFalse(r.is_aligned()) r.bits(1) self.assertTrue(r.is_aligned()) mutagen-1.31/tests/test_aac.py0000644000175000017500000000612712574061222016632 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os from tempfile import mkstemp import shutil from mutagen.id3 import ID3, TIT1 from mutagen.aac import AAC, AACError from tests import TestCase, DATA_DIR class TADTS(TestCase): def setUp(self): original = os.path.join(DATA_DIR, "empty.aac") fd, self.filename = mkstemp(suffix='.aac') os.close(fd) shutil.copy(original, self.filename) tag = ID3() tag.add(TIT1(text=[u"a" * 5000], encoding=3)) tag.save(self.filename) self.aac = AAC(original) self.aac_id3 = AAC(self.filename) def tearDown(self): os.remove(self.filename) def test_channels(self): self.failUnlessEqual(self.aac.info.channels, 2) self.failUnlessEqual(self.aac_id3.info.channels, 2) def test_bitrate(self): self.failUnlessEqual(self.aac.info.bitrate, 3159) self.failUnlessEqual(self.aac_id3.info.bitrate, 3159) def test_sample_rate(self): self.failUnlessEqual(self.aac.info.sample_rate, 44100) self.failUnlessEqual(self.aac_id3.info.sample_rate, 44100) def test_length(self): self.failUnlessAlmostEqual(self.aac.info.length, 3.70, 2) self.failUnlessAlmostEqual(self.aac_id3.info.length, 3.70, 2) def test_not_my_file(self): self.failUnlessRaises( AACError, AAC, os.path.join(DATA_DIR, "empty.ogg")) self.failUnlessRaises( AACError, AAC, os.path.join(DATA_DIR, "silence-44-s.mp3")) def test_pprint(self): self.assertEqual(self.aac.pprint(), self.aac_id3.pprint()) self.assertTrue("ADTS" in self.aac.pprint()) class TADIF(TestCase): def setUp(self): original = os.path.join(DATA_DIR, "adif.aac") fd, self.filename = mkstemp(suffix='.aac') os.close(fd) shutil.copy(original, self.filename) tag = ID3() tag.add(TIT1(text=[u"a" * 5000], encoding=3)) tag.save(self.filename) self.aac = AAC(original) self.aac_id3 = AAC(self.filename) def tearDown(self): os.remove(self.filename) def test_channels(self): self.failUnlessEqual(self.aac.info.channels, 2) self.failUnlessEqual(self.aac_id3.info.channels, 2) def test_bitrate(self): self.failUnlessEqual(self.aac.info.bitrate, 128000) self.failUnlessEqual(self.aac_id3.info.bitrate, 128000) def test_sample_rate(self): self.failUnlessEqual(self.aac.info.sample_rate, 48000) self.failUnlessEqual(self.aac_id3.info.sample_rate, 48000) def test_length(self): self.failUnlessAlmostEqual(self.aac.info.length, 0.25, 2) self.failUnlessAlmostEqual(self.aac_id3.info.length, 0.25, 2) def test_not_my_file(self): self.failUnlessRaises( AACError, AAC, os.path.join(DATA_DIR, "empty.ogg")) self.failUnlessRaises( AACError, AAC, os.path.join(DATA_DIR, "silence-44-s.mp3")) def test_pprint(self): self.assertEqual(self.aac.pprint(), self.aac_id3.pprint()) self.assertTrue("ADIF" in self.aac.pprint()) mutagen-1.31/tests/quality/0000755000175000017500000000000012574256060016165 5ustar lazkalazka00000000000000mutagen-1.31/tests/quality/test_pyflakes.py0000644000175000017500000000420212574061222021404 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2013,2014 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 import os import re import sys from tests import TestCase from mutagen import _compat os.environ["PYFLAKES_NODOCTEST"] = "1" try: from pyflakes.scripts import pyflakes except ImportError: pyflakes = None class Error(object): IMPORT_UNUSED = "imported but unused" REDEF_FUNCTION = "redefinition of function" UNABLE_DETECT_UNDEF = "unable to detect undefined names" UNDEFINED_PY2_NAME = \ "undefined name '(unicode|long|basestring|xrange|cmp)'" class FakeStream(object): # skip these by default BL = [Error.UNABLE_DETECT_UNDEF] if _compat.PY3: BL.append(Error.UNDEFINED_PY2_NAME) def __init__(self, blacklist=None): self.lines = [] if blacklist is None: blacklist = [] self.bl = self.BL[:] + blacklist def write(self, text): for p in self.bl: if re.search(p, text): return text = text.strip() if not text: return self.lines.append(text) def check(self): if self.lines: raise Exception("\n" + "\n".join(self.lines)) class TPyFlakes(TestCase): def _run(self, path, **kwargs): old_stdout = sys.stdout stream = FakeStream(**kwargs) try: sys.stdout = stream for dirpath, dirnames, filenames in os.walk(path): for filename in filenames: if filename.endswith('.py'): pyflakes.checkPath(os.path.join(dirpath, filename)) finally: sys.stdout = old_stdout stream.check() def _run_package(self, mod, *args, **kwargs): path = mod.__path__[0] self._run(path, *args, **kwargs) def test_main(self): import mutagen self._run_package(mutagen) def test_tests(self): import tests self._run_package(tests) if not pyflakes: del TPyFlakes mutagen-1.31/tests/quality/__init__.py0000644000175000017500000000036312574061222020272 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2014 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 mutagen-1.31/tests/quality/test_pep8.py0000644000175000017500000000351312574061222020446 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2013,2014 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 import os import glob import subprocess from tests import TestCase PEP8_NAME = "pep8" has_pep8 = True try: subprocess.check_output([PEP8_NAME, "--version"], stderr=subprocess.STDOUT) except OSError: has_pep8 = False class TPEP8(TestCase): IGNORE = ["E12", "W601", "E402", "E731"] def _run(self, path, ignore=None): if ignore is None: ignore = [] ignore += self.IGNORE p = subprocess.Popen( [PEP8_NAME, "--ignore=" + ",".join(ignore), path], stderr=subprocess.PIPE, stdout=subprocess.PIPE) class Future(object): def __init__(self, p): self.p = p def result(self): if self.p.wait() != 0: return self.p.communicate() return Future(p) def _run_package(self, mod, ignore=None): path = mod.__path__[0] files = glob.glob(os.path.join(path, "*.py")) assert files futures = [] for file_ in files: futures.append(self._run(file_, ignore)) errors = [] for future in futures: status = future.result() if status is not None: errors.append(status[0].decode("utf-8")) if errors: raise Exception("\n".join(errors)) def test_main_package(self): import mutagen self._run_package(mutagen) def test_id3_package(self): import mutagen.id3 self._run_package(mutagen.id3) def test_tests(self): import tests self._run_package(tests) if not has_pep8: del TPEP8 mutagen-1.31/tests/test_oggflac.py0000644000175000017500000000655712574061222017517 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os import shutil from tempfile import mkstemp from mutagen._compat import cBytesIO from mutagen.oggflac import OggFLAC, OggFLACStreamInfo, delete from mutagen.ogg import OggPage, error as OggError from tests import TestCase, DATA_DIR from tests.test_ogg import TOggFileTypeMixin from tests.test_flac import have_flac, call_flac class TOggFLAC(TestCase, TOggFileTypeMixin): Kind = OggFLAC PADDING_SUPPORT = False def setUp(self): original = os.path.join(DATA_DIR, "empty.oggflac") fd, self.filename = mkstemp(suffix='.ogg') os.close(fd) shutil.copy(original, self.filename) self.audio = OggFLAC(self.filename) def tearDown(self): os.unlink(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(b"fLaC", b"!fLa", 1) self.failUnlessRaises(IOError, OggFLACStreamInfo, cBytesIO(page)) def test_streaminfo_too_short(self): page = OggPage(open(self.filename, "rb")).write() self.failUnlessRaises(OggError, OggFLACStreamInfo, cBytesIO(page[:10])) def test_streaminfo_bad_version(self): page = OggPage(open(self.filename, "rb")).write() page = page.replace(b"\x01\x00", b"\x02\x00", 1) self.failUnlessRaises(IOError, OggFLACStreamInfo, cBytesIO(page)) def test_flac_reference_simple_save(self): if not have_flac: return self.audio.save() self.scan_file() self.assertEqual(call_flac("--ogg", "-t", self.filename), 0) def test_flac_reference_really_big(self): if not have_flac: return self.test_really_big() self.audio.save() self.scan_file() self.assertEqual(call_flac("--ogg", "-t", self.filename), 0) 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() self.assertEqual(call_flac("--ogg", "-t", self.filename), 0) def test_flac_reference_medium_sized(self): if not have_flac: return self.audio["foobar"] = "foobar" * 1000 self.audio.save() self.scan_file() self.assertEqual(call_flac("--ogg", "-t", self.filename), 0) 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() self.assertEqual(call_flac("--ogg", "-t", self.filename), 0) def test_not_my_ogg(self): fn = os.path.join(DATA_DIR, '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) def test_info_pprint(self): self.assertTrue(self.audio.info.pprint().startswith(u"Ogg FLAC")) mutagen-1.31/tests/test_tools_moggsplit.py0000644000175000017500000000261412574061222021330 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os from tempfile import mkstemp import shutil from mutagen._compat import text_type from mutagen._toolsutil import fsnative as fsn from tests.test_tools import _TTools from tests import DATA_DIR class TMOggSPlit(_TTools): TOOL_NAME = u"moggsplit" def setUp(self): super(TMOggSPlit, self).setUp() original = os.path.join( DATA_DIR, fsn(u'multipagecomment.ogg')) fd, self.filename = mkstemp(suffix=fsn(u'.ogg')) os.close(fd) shutil.copy(original, self.filename) # append the second file first = open(self.filename, "ab") to_append = os.path.join( DATA_DIR, fsn(u'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, fsn(u"%(stream)d.%(ext)s")) res, out = self.call(fsn(u"--pattern"), p, self.filename) self.failIf(res) self.failIf(out) for stream in [1002429366, 1806412655]: stream_path = os.path.join( d, fsn(text_type(stream)) + fsn(u".ogg")) self.failUnless(os.path.exists(stream_path)) os.unlink(stream_path) mutagen-1.31/tests/test_oggvorbis.py0000644000175000017500000001456112574061222020110 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os import shutil from mutagen._compat import cBytesIO from mutagen.ogg import OggPage from mutagen.oggvorbis import OggVorbis, OggVorbisInfo, delete from tests import TestCase, DATA_DIR from tests.test_ogg import TOggFileTypeMixin from tempfile import mkstemp class TOggVorbis(TestCase, TOggFileTypeMixin): Kind = OggVorbis def setUp(self): original = os.path.join(DATA_DIR, "empty.ogg") fd, self.filename = mkstemp(suffix='.ogg') os.close(fd) shutil.copy(original, self.filename) self.audio = self.Kind(self.filename) def tearDown(self): os.unlink(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, cBytesIO(page.write())) def test_avg_bitrate(self): page = OggPage(open(self.filename, "rb")) packet = page.packets[0] packet = (packet[:16] + b"\x00\x00\x01\x00" + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00" + packet[28:]) page.packets[0] = packet info = OggVorbisInfo(cBytesIO(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] + b"\x00\x00\x01\x00" + b"\x00\x00\x00\x01" + b"\x00\x00\x00\x00" + packet[28:]) page.packets[0] = packet info = OggVorbisInfo(cBytesIO(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] + b"\x00\x00\x01\x00" + b"\x01\x00\x00\x00" + b"\x00\x00\x01\x00" + packet[28:]) page.packets[0] = packet info = OggVorbisInfo(cBytesIO(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] + b"\xff\xff\xff\xff" + b"\xff\xff\xff\xff" + b"\xff\xff\xff\xff" + packet[28:]) page.packets[0] = packet info = OggVorbisInfo(cBytesIO(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(DATA_DIR, "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(DATA_DIR, '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(DATA_DIR, "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(DATA_DIR, "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) def test_init_padding(self): self.assertEqual(self.audio.tags._padding, 0) try: import ogg.vorbis except ImportError: print("WARNING: Skipping Ogg Vorbis reference tests.") ogg = None mutagen-1.31/tests/test_monkeysaudio.py0000644000175000017500000000342312574061222020611 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os from mutagen.monkeysaudio import MonkeysAudio, MonkeysAudioHeaderError from tests import TestCase, DATA_DIR class TMonkeysAudio(TestCase): def setUp(self): self.mac399 = MonkeysAudio(os.path.join(DATA_DIR, "mac-399.ape")) self.mac396 = MonkeysAudio(os.path.join(DATA_DIR, "mac-396.ape")) self.mac390 = MonkeysAudio(os.path.join(DATA_DIR, "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(DATA_DIR, "empty.ogg")) self.failUnlessRaises( MonkeysAudioHeaderError, MonkeysAudio, os.path.join(DATA_DIR, "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()) mutagen-1.31/tests/test__vorbis.py0000644000175000017500000002212612574061222017546 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- from tests import TestCase from mutagen._vorbis import VComment, VCommentDict, istag from mutagen._compat import text_type, PY3 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("!{}[]-_()*&")) def test_unicode(self): self.failUnless(istag(u"ti tle")) if PY3: def test_py3(self): self.failUnlessRaises(TypeError, istag, b"abc") 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, b"foo") def test_unset_framing_bit(self): self.failUnlessRaises( IOError, VComment, b"\x00\x00\x00\x00" * 2 + b"\x00") def test_empty_valid(self): self.failIf(VComment(b"\x00\x00\x00\x00" * 2 + b"\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((u"valid", 1)) self.failUnlessRaises(ValueError, self.c.validate) self.failUnlessRaises(ValueError, self.c.write) def test_validate_nonunicode_value(self): self.c.append((u"valid", b"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(u"Mutagen")) def test_vendor_set(self): self.c.vendor = u"Not Mutagen" self.failUnless(self.c.write()[4:].startswith(b"Not Mutagen")) def test_vendor_invalid(self): self.c.vendor = b"\xffNot Mutagen" self.failUnlessRaises(ValueError, self.c.validate) self.failUnlessRaises(ValueError, self.c.write) def test_validate_utf8_value(self): self.c.append((u"valid", b"\xc3\xbc\xc3\xb6\xc3\xa4")) if PY3: self.failUnlessRaises(ValueError, self.c.validate) else: self.c.validate() def test_invalid_format_strict(self): data = (b'\x07\x00\x00\x00Mutagen\x01\x00\x00\x00\x03\x00\x00' b'\x00abc\x01') self.failUnlessRaises(IOError, VComment, data, errors='strict') def test_invalid_format_replace(self): data = (b'\x07\x00\x00\x00Mutagen\x01\x00\x00\x00\x03\x00\x00' b'\x00abc\x01') comment = VComment(data) self.failUnlessEqual(u"abc", comment[0][1]) def test_python_key_value_type(self): data = (b'\x07\x00\x00\x00Mutagen\x01\x00\x00\x00\x03\x00\x00' b'\x00abc\x01') comment = VComment(data) self.assertTrue(isinstance(comment[0][0], type(''))) self.assertTrue(isinstance(comment[0][1], text_type)) if PY3: def test_python3_strict_str(self): comment = VComment() comment.append((u"abc", u"test")) comment.validate() comment[0] = (u"abc", b"test") self.failUnlessRaises(ValueError, comment.validate) comment[0] = (b"abc", u"test") self.failUnlessRaises(ValueError, comment.validate) def test_invalid_format_ignore(self): data = (b'\x07\x00\x00\x00Mutagen\x01\x00\x00\x00\x03\x00\x00' b'\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 = (b'\x07\x00\x00\x00Mutagen\x01\x00\x00\x00\x04\x00\x00' b'\x00\xc2\xaa=c\x01') self.failUnlessRaises(IOError, VComment, data, errors='strict') def test_invalid_tag_replace(self): data = (b'\x07\x00\x00\x00Mutagen\x01\x00\x00\x00\x04\x00\x00' b'\x00\xc2\xaa=c\x01') comment = VComment(data) self.failUnlessEqual(u"?=c", comment.pprint()) def test_invalid_tag_ignore(self): data = (b'\x07\x00\x00\x00Mutagen\x01\x00\x00\x00\x04\x00\x00' b'\x00\xc2\xaa=c\x01') comment = VComment(data, errors='ignore') self.failIf(len(comment)) def test_roundtrip(self): self.assertReallyEqual(self.c, VComment(self.c.write())) 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_slice(self): l = [("foo", "bar"), ("foo", "bar2")] self.c[:] = l self.assertEqual(self.c[:], l) self.failUnlessEqual(self.c["foo"], ["bar", "bar2"]) del self.c[:] self.assertEqual(self.c[:], []) def test_iter(self): self.assertEqual(next(iter(self.c)), ("artist", "mu")) self.assertEqual(list(self.c)[0], ("artist", "mu")) 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(list(self.c.keys())) self.failIf(list(self.c.values())) self.failIf(list(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(ValueError, self.c.get, u"\u1234") self.failUnlessRaises( ValueError, self.c.__setitem__, u"\u1234", "foo") self.failUnlessRaises( ValueError, self.c.__delitem__, u"\u1234") if PY3: def test_py3_bad_key(self): self.failUnlessRaises(TypeError, self.c.get, b"a") self.failUnlessRaises( TypeError, self.c.__setitem__, b"a", "foo") self.failUnlessRaises( TypeError, self.c.__delitem__, b"a") 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) mutagen-1.31/tests/test_flac.py0000644000175000017500000005643112574061222017016 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import shutil import os import subprocess from tempfile import mkstemp from tests import TestCase, DATA_DIR 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 mutagen._compat import PY3 from tests.test__vorbis import TVCommentDict, VComment def call_flac(*args): with open(os.devnull, 'wb') as null: return subprocess.call( ["flac"] + list(args), stdout=null, stderr=subprocess.STDOUT) class Tto_int_be(TestCase): def test_empty(self): self.failUnlessEqual(to_int_be(b""), 0) def test_0(self): self.failUnlessEqual(to_int_be(b"\x00"), 0) def test_1(self): self.failUnlessEqual(to_int_be(b"\x01"), 1) def test_256(self): self.failUnlessEqual(to_int_be(b"\x01\x00"), 256) def test_long(self): self.failUnlessEqual(to_int_be(b"\x01\x00\x00\x00\x00"), 2 ** 32) class TVCFLACDict(TVCommentDict): Kind = VCFLACDict def test_roundtrip_vc(self): self.failUnlessEqual(self.c, VComment(self.c.write() + b"\x01")) class TMetadataBlock(TestCase): def test_empty(self): self.failUnlessEqual(MetadataBlock(b"").write(), b"") def test_not_empty(self): self.failUnlessEqual(MetadataBlock(b"foobar").write(), b"foobar") def test_change(self): b = MetadataBlock(b"foobar") b.data = b"quux" self.failUnlessEqual(b.write(), b"quux") def test_write_read_max_size(self): class SomeBlock(MetadataBlock): code = 255 max_data_size = 2 ** 24 - 1 block = SomeBlock(b"\x00" * max_data_size) data = MetadataBlock._writeblock(block) self.assertEqual(data[:4], b"\xff\xff\xff\xff") header_size = 4 self.assertEqual(len(data), max_data_size + header_size) block = SomeBlock(b"\x00" * (max_data_size + 1)) self.assertRaises(error, MetadataBlock._writeblock, block) def test_ctr_garbage(self): self.failUnlessRaises(TypeError, StreamInfo, 12) def test_too_large(self): block = Picture() block.data = b"\x00" * 0x1FFFFFF self.assertRaises( error, MetadataBlock._writeblocks, [block], 0, 0, None) def test_too_large_padding(self): block = Padding() self.assertEqual( len(MetadataBlock._writeblocks([block], 0, 0, lambda x: 2 ** 24)), 2**24 - 1 + 4) class TStreamInfo(TestCase): data = (b'\x12\x00\x12\x00\x00\x00\x0e\x005\xea\n\xc4H\xf0\x00\xca0' b'\x14(\x90\xf9\xe1)2\x13\x01\xd4\xa7\xa9\x11!8\xab\x91') data_invalid = len(data) * b'\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) class TSeekTable(TestCase): SAMPLE = os.path.join(DATA_DIR, "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) class TCueSheet(TestCase): SAMPLE = os.path.join(DATA_DIR, "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, b"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, b'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, 44100) self.failUnlessEqual(self.cs.tracks[1].isrc, b'') 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, b'') 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) class TPicture(TestCase): SAMPLE = os.path.join(DATA_DIR, "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) class TPadding(TestCase): def setUp(self): self.b = Padding(b"\x00" * 100) def test_padding(self): self.failUnlessEqual(self.b.write(), b"\x00" * 100) def test_blank(self): self.failIf(Padding().write()) def test_empty(self): self.failIf(Padding(b"").write()) def test_repr(self): repr(Padding()) def test_change(self): self.b.length = 20 self.failUnlessEqual(self.b.write(), b"\x00" * 20) class TFLAC(TestCase): SAMPLE = os.path.join(DATA_DIR, "silence-44-s.flac") def setUp(self): fd, self.NEW = mkstemp(".flac") os.close(fd) shutil.copy(self.SAMPLE, self.NEW) self.flac = FLAC(self.NEW) def tearDown(self): os.unlink(self.NEW) def test_padding(self): for pad in [0, 42, 2**24 - 1, 2 ** 24]: self.flac.save(padding=lambda x: pad) new = FLAC(self.flac.filename) expected = min(2**24 - 1, pad) self.assertEqual(new.metadata_blocks[-1].length, expected) def test_save_multiple_padding(self): # we don't touch existing padding blocks on save, but will # replace them in the file with one at the end def num_padding(f): blocks = f.metadata_blocks return len([b for b in blocks if isinstance(b, Padding)]) num_blocks = num_padding(self.flac) self.assertEqual(num_blocks, 1) block = Padding() block.length = 42 self.flac.metadata_blocks.append(block) block = Padding() block.length = 24 self.flac.metadata_blocks.append(block) self.flac.save() self.assertEqual(num_padding(self.flac), num_blocks + 2) new = FLAC(self.flac.filename) self.assertEqual(num_padding(new), 1) self.assertTrue(isinstance(new.metadata_blocks[-1], Padding)) def test_increase_size_new_padding(self): self.assertEqual(self.flac.metadata_blocks[-1].length, 3060) value = u"foo" * 100 self.flac[u"foo"] = [value] self.flac.save() new = FLAC(self.NEW) self.assertEqual(new.metadata_blocks[-1].length, 2752) self.assertEqual(new[u"foo"], [value]) def test_delete(self): self.failUnless(self.flac.tags) self.flac.delete() self.assertTrue(self.flac.tags is not None) self.assertFalse(self.flac.tags) flac = FLAC(self.NEW) self.assertTrue(flac.tags is None) 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( list(self.flac.keys()), list(self.flac.tags.keys())) def test_values(self): self.failUnlessEqual( list(self.flac.values()), list(self.flac.tags.values())) def test_items(self): self.failUnlessEqual( list(self.flac.items()), list(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, "rb").read(), open(self.NEW, "rb").read()) def test_write_changetitle(self): f = FLAC(self.NEW) if PY3: self.assertRaises( TypeError, f.__setitem__, b'title', b"A New Title") else: f[b"title"] = b"A New Title" f.save() f = FLAC(self.NEW) self.failUnlessEqual(f[b"title"][0], b"A New Title") def test_write_changetitle_unicode_value(self): f = FLAC(self.NEW) if PY3: self.assertRaises( TypeError, f.__setitem__, b'title', u"A Unicode Title \u2022") else: f[b"title"] = u"A Unicode Title \u2022" f.save() f = FLAC(self.NEW) self.failUnlessEqual(f[b"title"][0], u"A Unicode Title \u2022") def test_write_changetitle_unicode_key(self): f = FLAC(self.NEW) f[u"title"] = b"A New Title" if PY3: self.assertRaises(ValueError, f.save) else: f.save() f = FLAC(self.NEW) self.failUnlessEqual(f[u"title"][0], b"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(DATA_DIR, "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(DATA_DIR, "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(DATA_DIR, 'ooming-header.flac')) def test_with_real_flac(self): if not have_flac: return self.flac["faketag"] = "foobar" * 1000 self.flac.save() self.failIf(call_flac("-t", self.flac.filename) != 0) def test_save_unknown_block(self): block = MetadataBlock(b"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, b"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(DATA_DIR, "xing.mp3")) def test_save_invalid_flac(self): self.failUnlessRaises( IOError, self.flac.save, os.path.join(DATA_DIR, "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_save_on_mp3(self): path = os.path.join(DATA_DIR, "silence-44-s.mp3") self.assertRaises(error, self.flac.save, path) def test_mime(self): self.failUnless("audio/x-flac" in self.flac.mime) def test_variable_block_size(self): FLAC(os.path.join(DATA_DIR, "variable-block.flac")) def test_load_flac_with_application_block(self): FLAC(os.path.join(DATA_DIR, "flac_application.flac")) 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(DATA_DIR, "doesntexist.flac") self.assertRaises(IOError, FLAC, filename) class TFLACBadBlockSize(TestCase): TOO_SHORT = os.path.join(DATA_DIR, "52-too-short-block-size.flac") TOO_SHORT_2 = os.path.join(DATA_DIR, "106-short-picture-block-size.flac") OVERWRITTEN = os.path.join(DATA_DIR, "52-overwritten-metadata.flac") INVAL_INFO = os.path.join(DATA_DIR, "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) class TFLACBadBlockSizeWrite(TestCase): TOO_SHORT = os.path.join(DATA_DIR, "52-too-short-block-size.flac") def setUp(self): fd, self.NEW = mkstemp(".flac") os.close(fd) shutil.copy(self.TOO_SHORT, self.NEW) def tearDown(self): os.unlink(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(b"Tunng" in data) class TFLACBadBlockSizeOverflow(TestCase): def setUp(self): fd, self.filename = mkstemp(".flac") os.close(fd) shutil.copy(os.path.join(DATA_DIR, "silence-44-s.flac"), self.filename) def tearDown(self): os.unlink(self.filename) def test_largest_valid(self): f = FLAC(self.filename) pic = Picture() pic.data = b"\x00" * (2 ** 24 - 1 - 32) self.assertEqual(len(pic.write()), 2 ** 24 - 1) f.add_picture(pic) f.save() def test_smallest_invalid(self): f = FLAC(self.filename) pic = Picture() pic.data = b"\x00" * (2 ** 24 - 32) f.add_picture(pic) self.assertRaises(error, f.save) def test_invalid_overflow_recover_and_save_back(self): # save a picture which is too large for flac, but still write it # with a wrong block size f = FLAC(self.filename) f.clear_pictures() pic = Picture() pic.data = b"\x00" * (2 ** 24 - 32) pic._invalid_overflow_size = 42 f.add_picture(pic) f.save() # make sure we can read it and save it again f = FLAC(self.filename) self.assertTrue(f.pictures) self.assertEqual(len(f.pictures[0].data), 2 ** 24 - 32) f.save() 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(DATA_DIR, "CVE-2007-4619-1.flac") self.failUnlessRaises(IOError, FLAC, filename) # work around https://bitbucket.org/pypy/pypy/issue/1988 import gc gc.collect() 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(DATA_DIR, "CVE-2007-4619-2.flac") self.failUnlessRaises(IOError, FLAC, filename) # work around https://bitbucket.org/pypy/pypy/issue/1988 import gc gc.collect() # "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(DATA_DIR, "CVE-2007-4619-12.flac") self.failUnlessRaises(IOError, FLAC, filename) def test_12_write_too_big(self): filename = os.path.join(DATA_DIR, "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) # Vulnerability 13 and 14 are specific to libFLAC and C/C++ memory # management schemes. have_flac = True try: call_flac() except OSError: have_flac = False print("WARNING: Skipping FLAC reference tests.") mutagen-1.31/tests/test_trueaudio.py0000644000175000017500000000301012574061222020073 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os import shutil from mutagen.trueaudio import TrueAudio, delete from mutagen.id3 import TIT1 from tests import TestCase, DATA_DIR from tempfile import mkstemp class TTrueAudio(TestCase): def setUp(self): self.audio = TrueAudio(os.path.join(DATA_DIR, "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(DATA_DIR, "empty.ogg") self.failUnlessRaises(IOError, TrueAudio, filename) def test_module_delete(self): delete(os.path.join(DATA_DIR, "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) mutagen-1.31/tests/test___init__.py0000644000175000017500000003011512574253351017645 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os from tempfile import mkstemp import shutil from tests import TestCase, DATA_DIR from mutagen._compat import cBytesIO, text_type from mutagen import File, Metadata, FileType, MutagenError, PaddingInfo 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 from mutagen.aiff import AIFF from mutagen.aac import AAC from os import devnull 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") class TPaddingInfo(TestCase): def test_props(self): info = PaddingInfo(10, 100) self.assertEqual(info.size, 100) self.assertEqual(info.padding, 10) info = PaddingInfo(-10, 100) self.assertEqual(info.size, 100) self.assertEqual(info.padding, -10) def test_default_strategy(self): s = 100000 self.assertEqual(PaddingInfo(10, s).get_default_padding(), 10) self.assertEqual(PaddingInfo(-10, s).get_default_padding(), 1124) self.assertEqual(PaddingInfo(0, s).get_default_padding(), 0) self.assertEqual(PaddingInfo(20000, s).get_default_padding(), 1124) self.assertEqual(PaddingInfo(10, 0).get_default_padding(), 10) self.assertEqual(PaddingInfo(-10, 0).get_default_padding(), 1024) self.assertEqual(PaddingInfo(1050, 0).get_default_padding(), 1050) self.assertEqual(PaddingInfo(20000, 0).get_default_padding(), 1024) def test_repr(self): info = PaddingInfo(10, 100) self.assertEqual(repr(info), "") class TFileType(TestCase): def setUp(self): self.vorbis = File(os.path.join(DATA_DIR, "empty.ogg")) fd, filename = mkstemp(".mp3") os.close(fd) shutil.copy(os.path.join(DATA_DIR, "xing.mp3"), filename) self.mp3_notags = File(filename) self.mp3_filename = filename def tearDown(self): os.remove(self.mp3_filename) 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) def test_save_no_tags(self): self.assertTrue(self.mp3_notags.tags is None) self.mp3_notags.save() self.assertTrue(self.mp3_notags.tags is None) class TAbstractFileType(object): PATH = None KIND = None def setUp(self): fd, self.filename = mkstemp("." + self.PATH.rsplit(".", 1)[-1]) os.close(fd) shutil.copy(self.PATH, self.filename) self.audio = self.KIND(self.filename) def tearDown(self): os.remove(self.filename) def test_filename(self): self.assertEqual(self.audio.filename, self.filename) def test_file(self): self.assertTrue(isinstance(File(self.PATH), self.KIND)) def test_pprint(self): res = self.audio.pprint() self.assertTrue(res) self.assertTrue(isinstance(res, text_type)) def test_info(self): self.assertTrue(self.audio.info) def test_info_pprint(self): res = self.audio.info.pprint() self.assertTrue(res) self.assertTrue(isinstance(res, text_type)) def test_mime(self): self.assertTrue(self.audio.mime) self.assertTrue(isinstance(self.audio.mime, list)) def test_load(self): x = self.KIND() x.load(self.filename) x.save() def test_delete(self): self.audio.delete(self.filename) self.audio.delete() def test_save(self): self.audio.save(self.filename) self.audio.save() def test_add_tags(self): had_tags = self.audio.tags is not None try: self.audio.add_tags() except MutagenError: pass else: self.assertFalse(had_tags) self.assertTrue(self.audio.tags is not None) self.assertRaises(MutagenError, self.audio.add_tags) def test_score(self): with open(self.filename, "rb") as fileobj: header = fileobj.read(128) self.KIND.score(self.filename, fileobj, header) def test_dict(self): self.audio.keys() self.assertRaises(KeyError, self.audio.__delitem__, "nopenopenopenope") for key, value in self.audio.items(): del self.audio[key] self.audio[key] = value _FILETYPES = { OggVorbis: [os.path.join(DATA_DIR, "empty.ogg")], OggFLAC: [os.path.join(DATA_DIR, "empty.oggflac")], OggSpeex: [os.path.join(DATA_DIR, "empty.spx")], OggTheora: [os.path.join(DATA_DIR, "sample.oggtheora")], OggOpus: [os.path.join(DATA_DIR, "example.opus")], FLAC: [os.path.join(DATA_DIR, "silence-44-s.flac")], TrueAudio: [os.path.join(DATA_DIR, "empty.tta")], WavPack: [os.path.join(DATA_DIR, "silence-44-s.wv")], MP3: [ os.path.join(DATA_DIR, "bad-xing.mp3"), os.path.join(DATA_DIR, "xing.mp3"), os.path.join(DATA_DIR, "silence-44-s.mp3"), ], Musepack: [ os.path.join(DATA_DIR, "click.mpc"), os.path.join(DATA_DIR, "sv4_header.mpc"), os.path.join(DATA_DIR, "sv5_header.mpc"), os.path.join(DATA_DIR, "sv8_header.mpc"), ], OptimFROG: [ os.path.join(DATA_DIR, "empty.ofr"), os.path.join(DATA_DIR, "empty.ofs"), ], AAC: [ os.path.join(DATA_DIR, "empty.aac"), os.path.join(DATA_DIR, "adif.aac"), ], ASF: [ os.path.join(DATA_DIR, "silence-1.wma"), os.path.join(DATA_DIR, "silence-2.wma"), os.path.join(DATA_DIR, "silence-3.wma"), ], AIFF: [ os.path.join(DATA_DIR, "with-id3.aif"), os.path.join(DATA_DIR, "11k-1ch-2s-silence.aif"), os.path.join(DATA_DIR, "48k-2ch-s16-silence.aif"), os.path.join(DATA_DIR, "8k-1ch-1s-silence.aif"), os.path.join(DATA_DIR, "8k-1ch-3.5s-silence.aif"), os.path.join(DATA_DIR, "8k-4ch-1s-silence.aif") ], MonkeysAudio: [ os.path.join(DATA_DIR, "mac-399.ape"), os.path.join(DATA_DIR, "mac-396.ape"), ], MP4: [ os.path.join(DATA_DIR, "has-tags.m4a"), os.path.join(DATA_DIR, "no-tags.m4a"), os.path.join(DATA_DIR, "no-tags.3g2"), os.path.join(DATA_DIR, "truncated-64bit.mp4"), ], } def create_filetype_tests(): for kind, paths in _FILETYPES.items(): for i, path in enumerate(paths): suffix = "_" + str(i + 1) if i else "" new_type = type("TFileType" + kind.__name__ + suffix, (TAbstractFileType, TestCase), {"PATH": path, "KIND": kind}) globals()[new_type.__name__] = new_type create_filetype_tests() 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(DATA_DIR, "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(DATA_DIR, filename) self.failIf(File(filename, options=[])) def test_easy_mp3(self): self.failUnless(isinstance( File(os.path.join(DATA_DIR, "silence-44-s.mp3"), easy=True), EasyMP3)) def test_apev2(self): self.failUnless(isinstance( File(os.path.join(DATA_DIR, "oldtag.apev2")), APEv2File)) def test_easy_tta(self): self.failUnless(isinstance( File(os.path.join(DATA_DIR, "empty.tta"), easy=True), EasyTrueAudio)) def test_id3_indicates_mp3_not_tta(self): header = b"ID3 the rest of this is garbage" fileobj = cBytesIO(header) filename = "not-identifiable.ext" self.failUnless(TrueAudio.score(filename, fileobj, header) < MP3.score(filename, fileobj, header)) def test_prefer_theora_over_vorbis(self): header = ( b"OggS\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\xe1x\x06\x0f" b"\x00\x00\x00\x00)S'\xf4\x01*\x80theora\x03\x02\x01\x006\x00\x1e" b"\x00\x03V\x00\x01\xe0\x00\x00\x00\x00\x00\x18\x00\x00\x00\x01" b"\x00\x00\x00\x00\x00\x00\x00&%\xa0\x00\xc0OggS\x00\x02\x00\x00" b"\x00\x00\x00\x00\x00\x00d#\xa8\x1f\x00\x00\x00\x00]Y\xc0\xc0" b"\x01\x1e\x01vorbis\x00\x00\x00\x00\x02\x80\xbb\x00\x00\x00\x00" b"\x00\x00\x00\xee\x02\x00\x00\x00\x00\x00\xb8\x01") fileobj = cBytesIO(header) filename = "not-identifiable.ext" self.failUnless(OggVorbis.score(filename, fileobj, header) < OggTheora.score(filename, fileobj, header)) class TFileUpperExt(TestCase): FILES = [ (os.path.join(DATA_DIR, "empty.ofr"), OptimFROG), (os.path.join(DATA_DIR, "sv5_header.mpc"), Musepack), (os.path.join(DATA_DIR, "silence-3.wma"), ASF), (os.path.join(DATA_DIR, "truncated-64bit.mp4"), MP4), (os.path.join(DATA_DIR, "silence-44-s.flac"), FLAC), ] def setUp(self): checks = [] for (original, instance) in self.FILES: ext = os.path.splitext(original)[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: if isinstance(path, bytes): path = path.decode("ascii") self.failUnless( isinstance(File(path, options=[instance]), instance)) path = path.encode("ascii") self.failUnless( isinstance(File(path, options=[instance]), instance)) def tearDown(self): for (path, instance) in self.checks: os.unlink(path) class TModuleImportAll(TestCase): def setUp(self): import mutagen files = os.listdir(mutagen.__path__[0]) modules = set(os.path.splitext(f)[0] for f in files) modules = [f for f in modules if not f.startswith("_")] self.modules = [] for module in modules: mod = getattr(__import__("mutagen." + module), module) self.modules.append(mod) def tearDown(self): del self.modules[:] def test_all(self): for mod in self.modules: for attr in getattr(mod, "__all__", []): getattr(mod, attr) def test_errors(self): for mod in self.modules: self.assertTrue(issubclass(mod.error, MutagenError), msg=mod.error) mutagen-1.31/tests/test_id3.py0000644000175000017500000020317712574061222016571 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os from os.path import join import shutil from tests import TestCase, DATA_DIR from mutagen import id3 from mutagen.apev2 import APEv2 from mutagen.id3 import ID3, COMR, Frames, Frames_2_2, ID3Warning, \ ID3JunkFrameError, ID3Header, ID3UnsupportedVersionError, _fullread from mutagen.id3._util import BitPaddedInt, error as ID3Error from mutagen._compat import cBytesIO, PY2, iteritems, integer_types, izip import warnings from tempfile import mkstemp warnings.simplefilter('error', ID3Warning) _22 = ID3Header() _22.version = (2, 2, 0) _23 = ID3Header() _23.version = (2, 3, 0) _24 = ID3Header() _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(DATA_DIR, 'emptyfile.mp3') silence = join(DATA_DIR, 'silence-44-s.mp3') unsynch = join(DATA_DIR, 'id3v23_unsynch.id3') def test_empty_file(self): name = self.empty self.assertRaises(ID3Error, ID3, filename=name) def test_nonexistent_file(self): name = join(DATA_DIR, 'does', 'not', 'exist') self.assertRaises(EnvironmentError, ID3, name) def test_read_padding(self): self.assertEqual(ID3(self.silence)._padding, 1142) self.assertEqual(ID3(self.unsynch)._padding, 0) def test_header_empty(self): fileobj = open(self.empty, 'rb') self.assertRaises(ID3Error, ID3Header, fileobj) def test_header_silence(self): fileobj = open(self.silence, 'rb') header = ID3Header(fileobj) self.assertEquals(header.version, (2, 3, 0)) self.assertEquals(header.size, 1314) def test_header_2_4_invalid_flags(self): fileobj = cBytesIO(b'ID3\x04\x00\x1f\x00\x00\x00\x00') self.assertRaises(ID3Error, ID3Header, fileobj) def test_header_2_4_unsynch_size(self): fileobj = cBytesIO(b'ID3\x04\x00\x10\x00\x00\x00\xFF') self.assertRaises(ID3Error, ID3Header, fileobj) def test_header_2_4_allow_footer(self): fileobj = cBytesIO(b'ID3\x04\x00\x10\x00\x00\x00\x00') self.assertTrue(ID3Header(fileobj).f_footer) def test_header_2_3_invalid_flags(self): fileobj = cBytesIO(b'ID3\x03\x00\x1f\x00\x00\x00\x00') self.assertRaises(ID3Error, ID3Header, fileobj) fileobj = cBytesIO(b'ID3\x03\x00\x0f\x00\x00\x00\x00') self.assertRaises(ID3Error, ID3Header, fileobj) def test_header_2_2(self): fileobj = cBytesIO(b'ID3\x02\x00\x00\x00\x00\x00\x00') header = ID3Header(fileobj) self.assertEquals(header.version, (2, 2, 0)) def test_header_2_1(self): fileobj = cBytesIO(b'ID3\x01\x00\x00\x00\x00\x00\x00') self.assertRaises(ID3UnsupportedVersionError, ID3Header, fileobj) def test_header_too_small(self): fileobj = cBytesIO(b'ID3\x01\x00\x00\x00\x00\x00') self.assertRaises(ID3Error, ID3Header, fileobj) def test_header_2_4_extended(self): fileobj = cBytesIO( b'ID3\x04\x00\x40\x00\x00\x00\x00\x00\x00\x00\x05\x5a') header = ID3Header(fileobj) self.assertEquals(header._extdata, b'\x5a') def test_header_2_4_extended_unsynch_size(self): fileobj = cBytesIO( b'ID3\x04\x00\x40\x00\x00\x00\x00\x00\x00\x00\xFF\x5a') self.assertRaises(ID3Error, ID3Header, fileobj) def test_header_2_4_extended_but_not(self): fileobj = cBytesIO( b'ID3\x04\x00\x40\x00\x00\x00\x00TIT1\x00\x00\x00\x01a') header = ID3Header(fileobj) self.assertEquals(header._extdata, b'') def test_header_2_4_extended_but_not_but_not_tag(self): fileobj = cBytesIO(b'ID3\x04\x00\x40\x00\x00\x00\x00TIT9') self.failUnlessRaises(ID3Error, ID3Header, fileobj) def test_header_2_3_extended(self): fileobj = cBytesIO( b'ID3\x03\x00\x40\x00\x00\x00\x00\x00\x00\x00\x06' b'\x00\x00\x56\x78\x9a\xbc') header = ID3Header(fileobj) self.assertEquals(header._extdata, b'\x00\x00\x56\x78\x9a\xbc') def test_unsynch(self): header = ID3Header() header.version = (2, 4, 0) header._flags = 0x80 badsync = b'\x00\xff\x00ab\x00' self.assertEquals( Frames["TPE2"]._fromData(header, 0, badsync), [u"\xffab"]) header._flags = 0x00 self.assertEquals( Frames["TPE2"]._fromData(header, 0x02, badsync), [u"\xffab"]) tag = Frames["TPE2"]._fromData(header, 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): fileobj = cBytesIO() self.assertRaises(ValueError, _fullread, fileobj, -3) self.assertRaises(EOFError, _fullread, fileobj, 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(DATA_DIR, '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"]) class ID3Tags(TestCase): def setUp(self): self.silence = join(DATA_DIR, '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_unknown_reset(self): id3 = ID3(self.silence, known_frames={}) self.assertEquals(9, len(id3.unknown_frames)) id3.load(self.silence, known_frames={}) self.assertEquals(9, len(id3.unknown_frames)) def test_has_docs(self): for Kind in (list(Frames.values()) + list(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( ID3JunkFrameError, Frames["TPE1"]._fromData, _24, 0, b"\x09ab") self.assertRaises(ValueError, Frames["TPE1"], encoding=9, text="ab") def test_badsync(self): frame = Frames["TPE1"]._fromData(_24, 0x02, b"\x00\xff\xfe") self.assertEqual(frame.text, [u'\xff\xfe']) def test_noencrypt(self): self.assertRaises( NotImplementedError, Frames["TPE1"]._fromData, _24, 0x04, b"\x00") self.assertRaises( NotImplementedError, Frames["TPE1"]._fromData, _23, 0x40, b"\x00") def test_badcompress(self): self.assertRaises( ValueError, Frames["TPE1"]._fromData, _24, 0x08, b"\x00\x00\x00\x00#") self.assertRaises( ValueError, Frames["TPE1"]._fromData, _23, 0x80, b"\x00\x00\x00\x00#") def test_junkframe(self): self.assertRaises(ValueError, Frames["TPE1"]._fromData, _24, 0, b"") def test_bad_sylt(self): self.assertRaises( ID3JunkFrameError, Frames["SYLT"]._fromData, _24, 0x0, b"\x00eng\x01description\x00foobar") self.assertRaises( ID3JunkFrameError, Frames["SYLT"]._fromData, _24, 0x0, b"\x00eng\x01description\x00foobar\x00\xFF\xFF\xFF") def test_extradata(self): from mutagen.id3 import RVRB, RBUF self.assertEqual(RVRB()._readData(b'L1R1BBFFFFPP#xyz'), b'#xyz') self.assertEqual( RBUF()._readData(b'\x00\x01\x00\x01\x00\x00\x00\x00#xyz'), b'#xyz') class ID3v1Tags(TestCase): def setUp(self): self.silence = join(DATA_DIR, '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] + b' ' + tag[-1:] self.failIf("TRCK" in ParseID3v1(tag)) def test_nulls(self): from mutagen.id3 import ParseID3v1 s = u'TAG%(title)30s%(artist)30s%(album)30s%(year)4s%(cmt)29s\x03\x01' s = s % dict(artist=u'abcd\00fg', title=u'hijklmn\x00p', album=u'qrst\x00v', cmt=u'wxyz', year=u'1224') tags = ParseID3v1(s.encode("ascii")) self.assertEquals(b'abcd'.decode('latin1'), tags['TPE1']) self.assertEquals(b'hijklmn'.decode('latin1'), tags['TIT2']) self.assertEquals(b'qrst'.decode('latin1'), tags['TALB']) def test_nonascii(self): from mutagen.id3 import ParseID3v1 s = u'TAG%(title)30s%(artist)30s%(album)30s%(year)4s%(cmt)29s\x03\x01' s = s % dict(artist=u'abcd\xe9fg', title=u'hijklmn\xf3p', album=u'qrst\xfcv', cmt=u'wxyz', year=u'1234') tags = ParseID3v1(s.encode("latin-1")) self.assertEquals(b'abcd\xe9fg'.decode('latin1'), tags['TPE1']) self.assertEquals(b'hijklmn\xf3p'.decode('latin1'), tags['TIT2']) self.assertEquals(b'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 = b'TAG' + b'\x00' * 124 + b'\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(b"") 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(DATA_DIR, "silence-44-s.mp3") def setUp(self): 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) == b"TAG") def failUnlessV1(self): fileobj = open(self.filename, "rb") fileobj.seek(-128, 2) self.failUnless(fileobj.read(3) == b"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) class TestV22Tags(TestCase): def setUp(self): filename = os.path.join(DATA_DIR, "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"]) def create_read_tag_tests(): tests = [ ['TALB', b'\x00a/b', 'a/b', '', dict(encoding=0)], ['TBPM', b'\x00120', '120', 120, dict(encoding=0)], ['TCMP', b'\x001', '1', 1, dict(encoding=0)], ['TCMP', b'\x000', '0', 0, dict(encoding=0)], ['TCOM', b'\x00a/b', 'a/b', '', dict(encoding=0)], ['TCON', b'\x00(21)Disco', '(21)Disco', '', dict(encoding=0)], ['TCOP', b'\x001900 c', '1900 c', '', dict(encoding=0)], ['TDAT', b'\x00a/b', 'a/b', '', dict(encoding=0)], ['TDEN', b'\x001987', '1987', '', dict(encoding=0, year=[1987])], [ 'TDOR', b'\x001987-12', '1987-12', '', dict(encoding=0, year=[1987], month=[12]) ], ['TDRC', b'\x001987\x00', '1987', '', dict(encoding=0, year=[1987])], [ 'TDRL', b'\x001987\x001988', '1987,1988', '', dict(encoding=0, year=[1987, 1988]) ], ['TDTG', b'\x001987', '1987', '', dict(encoding=0, year=[1987])], ['TDLY', b'\x001205', '1205', 1205, dict(encoding=0)], ['TENC', b'\x00a b/c d', 'a b/c d', '', dict(encoding=0)], ['TEXT', b'\x00a b\x00c d', ['a b', 'c d'], '', dict(encoding=0)], ['TFLT', b'\x00MPG/3', 'MPG/3', '', dict(encoding=0)], ['TIME', b'\x001205', '1205', '', dict(encoding=0)], [ 'TIPL', b'\x02\x00a\x00\x00\x00b', [["a", "b"]], '', dict(encoding=2) ], ['TIT1', b'\x00a/b', 'a/b', '', dict(encoding=0)], # TIT2 checks misaligned terminator '\x00\x00' across crosses utf16 # chars [ 'TIT2', b'\x01\xff\xfe\x38\x00\x00\x38', u'8\u3800', '', dict(encoding=1) ], ['TIT3', b'\x00a/b', 'a/b', '', dict(encoding=0)], ['TKEY', b'\x00A#m', 'A#m', '', dict(encoding=0)], ['TLAN', b'\x006241', '6241', '', dict(encoding=0)], ['TLEN', b'\x006241', '6241', 6241, dict(encoding=0)], [ 'TMCL', b'\x02\x00a\x00\x00\x00b', [["a", "b"]], '', dict(encoding=2) ], ['TMED', b'\x00med', 'med', '', dict(encoding=0)], ['TMOO', b'\x00moo', 'moo', '', dict(encoding=0)], ['TOAL', b'\x00alb', 'alb', '', dict(encoding=0)], ['TOFN', b'\x0012 : bar', '12 : bar', '', dict(encoding=0)], ['TOLY', b'\x00lyr', 'lyr', '', dict(encoding=0)], ['TOPE', b'\x00own/lic', 'own/lic', '', dict(encoding=0)], ['TORY', b'\x001923', '1923', 1923, dict(encoding=0)], ['TOWN', b'\x00own/lic', 'own/lic', '', dict(encoding=0)], ['TPE1', b'\x00ab', ['ab'], '', dict(encoding=0)], [ 'TPE2', b'\x00ab\x00cd\x00ef', ['ab', 'cd', 'ef'], '', dict(encoding=0) ], ['TPE3', b'\x00ab\x00cd', ['ab', 'cd'], '', dict(encoding=0)], ['TPE4', b'\x00ab\x00', ['ab'], '', dict(encoding=0)], ['TPOS', b'\x0008/32', '08/32', 8, dict(encoding=0)], ['TPRO', b'\x00pro', 'pro', '', dict(encoding=0)], ['TPUB', b'\x00pub', 'pub', '', dict(encoding=0)], ['TRCK', b'\x004/9', '4/9', 4, dict(encoding=0)], ['TRDA', b'\x00Sun Jun 12', 'Sun Jun 12', '', dict(encoding=0)], ['TRSN', b'\x00ab/cd', 'ab/cd', '', dict(encoding=0)], ['TRSO', b'\x00ab', 'ab', '', dict(encoding=0)], ['TSIZ', b'\x0012345', '12345', 12345, dict(encoding=0)], ['TSOA', b'\x00ab', 'ab', '', dict(encoding=0)], ['TSOP', b'\x00ab', 'ab', '', dict(encoding=0)], ['TSOT', b'\x00ab', 'ab', '', dict(encoding=0)], ['TSO2', b'\x00ab', 'ab', '', dict(encoding=0)], ['TSOC', b'\x00ab', 'ab', '', dict(encoding=0)], ['TSRC', b'\x0012345', '12345', '', dict(encoding=0)], ['TSSE', b'\x0012345', '12345', '', dict(encoding=0)], ['TSST', b'\x0012345', '12345', '', dict(encoding=0)], ['TYER', b'\x002004', '2004', 2004, dict(encoding=0)], [ 'TXXX', b'\x00usr\x00a/b\x00c', ['a/b', 'c'], '', dict(encoding=0, desc='usr') ], ['WCOM', b'http://foo', 'http://foo', '', {}], ['WCOP', b'http://bar', 'http://bar', '', {}], ['WOAF', b'http://baz', 'http://baz', '', {}], ['WOAR', b'http://bar', 'http://bar', '', {}], ['WOAS', b'http://bar', 'http://bar', '', {}], ['WORS', b'http://bar', 'http://bar', '', {}], ['WPAY', b'http://bar', 'http://bar', '', {}], ['WPUB', b'http://bar', 'http://bar', '', {}], ['WXXX', b'\x00usr\x00http', 'http', '', dict(encoding=0, desc='usr')], [ 'IPLS', b'\x00a\x00A\x00b\x00B\x00', [['a', 'A'], ['b', 'B']], '', dict(encoding=0) ], ['MCDI', b'\x01\x02\x03\x04', b'\x01\x02\x03\x04', '', {}], [ 'ETCO', b'\x01\x12\x00\x00\x7f\xff', [(18, 32767)], '', dict(format=1) ], [ 'COMM', b'\x00ENUT\x00Com', 'Com', '', dict(desc='T', lang='ENU', encoding=0) ], [ 'APIC', b'\x00-->\x00\x03cover\x00cover.jpg', b'cover.jpg', '', dict(mime='-->', type=3, desc='cover', encoding=0) ], ['USER', b'\x00ENUCom', 'Com', '', dict(lang='ENU', encoding=0)], [ 'RVA2', b'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', b'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', b'testdata2\x00\x01\x04\x01\x00', 'Master volume: +2.0020 dB/0.0000', '', dict(desc='testdata2', channel=1, gain=2.001953125, peak=0) ], ['PCNT', b'\x00\x00\x00\x11', 17, 17, dict(count=17)], [ 'POPM', b'foo@bar.org\x00\xde\x00\x00\x00\x11', 222, 222, dict(email="foo@bar.org", rating=222, count=17) ], [ 'POPM', b'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', b'foo@bar.org\x00\xde', 222, 222, dict(email="foo@bar.org", rating=222) ], ['UFID', b'own\x00data', b'data', '', dict(data=b'data', owner='own')], ['UFID', b'own\x00\xdd', b'\xdd', '', dict(data=b'\xdd', owner='own')], [ 'GEOB', b'\x00mime\x00name\x00desc\x00data', b'data', '', dict(encoding=0, mime='mime', filename='name', desc='desc') ], [ 'USLT', b'\x00engsome lyrics\x00woo\nfun', 'woo\nfun', '', dict(encoding=0, lang='eng', desc='some lyrics', text='woo\nfun') ], [ 'SYLT', (b'\x00eng\x02\x01some lyrics\x00foo\x00\x00\x00\x00\x01' b'bar\x00\x00\x00\x00\x10'), "foobar", '', dict(encoding=0, lang='eng', type=1, format=2, desc='some lyrics') ], ['POSS', b'\x01\x0f', 15, 15, dict(format=1, position=15)], [ 'OWNE', b'\x00USD10.01\x0020041010CDBaby', 'CDBaby', 'CDBaby', dict(encoding=0, price="USD10.01", date='20041010', seller='CDBaby') ], [ 'PRIV', b'a@b.org\x00random data', b'random data', 'random data', dict(owner='a@b.org', data=b'random data') ], [ 'PRIV', b'a@b.org\x00\xdd', b'\xdd', '\xdd', dict(owner='a@b.org', data=b'\xdd') ], ['SIGN', b'\x92huh?', b'huh?', 'huh?', dict(group=0x92, sig=b'huh?')], [ 'ENCR', b'a@b.org\x00\x92Data!', b'Data!', 'Data!', dict(owner='a@b.org', method=0x92, data=b'Data!') ], [ 'SEEK', b'\x00\x12\x00\x56', 0x12 * 256 * 256 + 0x56, 0x12 * 256 * 256 + 0x56, dict(offset=0x12 * 256 * 256 + 0x56) ], [ 'SYTC', b"\x01\x10obar", b'\x10obar', '', dict(format=1, data=b'\x10obar') ], [ 'RBUF', b'\x00\x12\x00', 0x12 * 256, 0x12 * 256, dict(size=0x12 * 256) ], [ 'RBUF', b'\x00\x12\x00\x01', 0x12 * 256, 0x12 * 256, dict(size=0x12 * 256, info=1) ], [ 'RBUF', b'\x00\x12\x00\x01\x00\x00\x00\x23', 0x12 * 256, 0x12 * 256, dict(size=0x12 * 256, info=1, offset=0x23) ], [ 'RVRB', b'\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', b'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', b'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=b'!') ], [ 'GRID', b'a@b.org\x00\x99', 'a@b.org', 0x99, dict(owner='a@b.org', group=0x99) ], [ 'GRID', b'a@b.org\x00\x99data', 'a@b.org', 0x99, dict(owner='a@b.org', group=0x99, data=b'data') ], [ 'COMR', (b'\x00USD10.00\x0020051010ql@sc.net\x00\x09Joe\x00A song\x00' b'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=b'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=b'some data') ], [ 'COMR', b'\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', b'\x00\x01\x00\x00\x02\x00\x00\x03\x04\x08foobar', b'foobar', '', dict(frames=1, bytes=2, milliseconds=3, bits_for_bytes=4, bits_for_milliseconds=8, data=b'foobar') ], [ 'EQU2', b'\x00Foobar\x00\x01\x01\x04\x00', [(128.5, 2.0)], '', dict(method=0, desc="Foobar") ], [ 'ASPI', b'\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', b'\x00\x00\x00\x00\x00\x00\x00\x10\x00\x03\x10' b'\x00\x01\x00\x02\x00\x03', [1, 2, 3], '', dict(S=0, L=16, N=3, b=16) ], [ 'LINK', b'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', b'COMMhttp://www.example.org/COMM.txt\x00engfoo', ("COMM", 'http://www.example.org/COMM.txt', b'engfoo'), '', dict(frameid='COMM', url='http://www.example.org/COMM.txt', data=b'engfoo') ], # iTunes podcast frames ['TGID', b'\x00i', u'i', '', dict(encoding=0)], ['TDES', b'\x00ii', u'ii', '', dict(encoding=0)], ['WFED', b'http://zzz', 'http://zzz', '', {}], # 2.2 tags ['UFI', b'own\x00data', b'data', '', dict(data=b'data', owner='own')], [ 'SLT', (b'\x00eng\x02\x01some lyrics\x00foo\x00\x00\x00\x00\x01bar' b'\x00\x00\x00\x00\x10'), "foobar", '', dict(encoding=0, lang='eng', type=1, format=2, desc='some lyrics') ], ['TT1', b'\x00ab\x00', 'ab', '', dict(encoding=0)], ['TT2', b'\x00ab', 'ab', '', dict(encoding=0)], ['TT3', b'\x00ab', 'ab', '', dict(encoding=0)], ['TP1', b'\x00ab\x00', 'ab', '', dict(encoding=0)], ['TP2', b'\x00ab', 'ab', '', dict(encoding=0)], ['TP3', b'\x00ab', 'ab', '', dict(encoding=0)], ['TP4', b'\x00ab', 'ab', '', dict(encoding=0)], ['TCM', b'\x00ab/cd', 'ab/cd', '', dict(encoding=0)], ['TXT', b'\x00lyr', 'lyr', '', dict(encoding=0)], ['TLA', b'\x00ENU', 'ENU', '', dict(encoding=0)], ['TCO', b'\x00gen', 'gen', '', dict(encoding=0)], ['TAL', b'\x00alb', 'alb', '', dict(encoding=0)], ['TPA', b'\x001/9', '1/9', 1, dict(encoding=0)], ['TRK', b'\x002/8', '2/8', 2, dict(encoding=0)], ['TRC', b'\x00isrc', 'isrc', '', dict(encoding=0)], ['TYE', b'\x001900', '1900', 1900, dict(encoding=0)], ['TDA', b'\x002512', '2512', '', dict(encoding=0)], ['TIM', b'\x001225', '1225', '', dict(encoding=0)], ['TRD', b'\x00Jul 17', 'Jul 17', '', dict(encoding=0)], ['TMT', b'\x00DIG/A', 'DIG/A', '', dict(encoding=0)], ['TFT', b'\x00MPG/3', 'MPG/3', '', dict(encoding=0)], ['TBP', b'\x00133', '133', 133, dict(encoding=0)], ['TCP', b'\x001', '1', 1, dict(encoding=0)], ['TCP', b'\x000', '0', 0, dict(encoding=0)], ['TCR', b'\x00Me', 'Me', '', dict(encoding=0)], ['TPB', b'\x00Him', 'Him', '', dict(encoding=0)], ['TEN', b'\x00Lamer', 'Lamer', '', dict(encoding=0)], ['TSS', b'\x00ab', 'ab', '', dict(encoding=0)], ['TOF', b'\x00ab:cd', 'ab:cd', '', dict(encoding=0)], ['TLE', b'\x0012', '12', 12, dict(encoding=0)], ['TSI', b'\x0012', '12', 12, dict(encoding=0)], ['TDY', b'\x0012', '12', 12, dict(encoding=0)], ['TKE', b'\x00A#m', 'A#m', '', dict(encoding=0)], ['TOT', b'\x00org', 'org', '', dict(encoding=0)], ['TOA', b'\x00org', 'org', '', dict(encoding=0)], ['TOL', b'\x00org', 'org', '', dict(encoding=0)], ['TOR', b'\x001877', '1877', 1877, dict(encoding=0)], ['TXX', b'\x00desc\x00val', 'val', '', dict(encoding=0, desc='desc')], ['WAF', b'http://zzz', 'http://zzz', '', {}], ['WAR', b'http://zzz', 'http://zzz', '', {}], ['WAS', b'http://zzz', 'http://zzz', '', {}], ['WCM', b'http://zzz', 'http://zzz', '', {}], ['WCP', b'http://zzz', 'http://zzz', '', {}], ['WPB', b'http://zzz', 'http://zzz', '', {}], [ 'WXX', b'\x00desc\x00http', 'http', '', dict(encoding=0, desc='desc') ], [ 'IPL', b'\x00a\x00A\x00b\x00B\x00', [['a', 'A'], ['b', 'B']], '', dict(encoding=0) ], ['MCI', b'\x01\x02\x03\x04', b'\x01\x02\x03\x04', '', {}], [ 'ETC', b'\x01\x12\x00\x00\x7f\xff', [(18, 32767)], '', dict(format=1) ], [ 'COM', b'\x00ENUT\x00Com', 'Com', '', dict(desc='T', lang='ENU', encoding=0) ], [ 'PIC', b'\x00-->\x03cover\x00cover.jpg', b'cover.jpg', '', dict(mime='-->', type=3, desc='cover', encoding=0) ], [ 'POP', b'foo@bar.org\x00\xde\x00\x00\x00\x11', 222, 222, dict(email="foo@bar.org", rating=222, count=17) ], ['CNT', b'\x00\x00\x00\x11', 17, 17, dict(count=17)], [ 'GEO', b'\x00mime\x00name\x00desc\x00data', b'data', '', dict(encoding=0, mime='mime', filename='name', desc='desc') ], [ 'ULT', b'\x00engsome lyrics\x00woo\nfun', 'woo\nfun', '', dict(encoding=0, lang='eng', desc='some lyrics', text='woo\nfun')], [ 'BUF', b'\x00\x12\x00', 0x12 * 256, 0x12 * 256, dict(size=0x12 * 256) ], [ 'CRA', b'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', b'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=b'!') ], [ 'REV', b'\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', b"\x01\x10obar", b'\x10obar', '', dict(format=1, data=b'\x10obar') ], [ 'MLL', b'\x00\x01\x00\x00\x02\x00\x00\x03\x04\x08foobar', b'foobar', '', dict(frames=1, bytes=2, milliseconds=3, bits_for_bytes=4, bits_for_milliseconds=8, data=b'foobar') ], [ 'LNK', b'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', b'foo@example.org\x00test\x00woo', b'woo', '', dict(owner='foo@example.org', desc='test', data=b'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 iteritems(info): t = tag if not isinstance(value, list): value = [value] t = [t] for value, t in izip(value, iter(t)): if isinstance(value, float): self.failUnlessAlmostEqual(value, getattr(t, attr), 5) else: self.assertEquals(value, getattr(t, attr)) if isinstance(intval, integer_types): 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)) self.assertTrue(isinstance(tag.__str__(), str)) if PY2: if hasattr(tag, "__unicode__"): self.assertTrue(isinstance(tag.__unicode__(), unicode)) else: if hasattr(tag, "__bytes__"): self.assertTrue(isinstance(tag.__bytes__(), bytes)) 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) assert testcase.__name__ not in globals() globals()[testcase.__name__] = testcase testcase = type('TestReadReprTags', (TestCase,), repr_tests) assert testcase.__name__ not in globals() globals()[testcase.__name__] = testcase testcase = type('TestReadWriteTags', (TestCase,), write_tests) assert testcase.__name__ not in globals() globals()[testcase.__name__] = testcase from mutagen.id3 import Frames, Frames_2_2 check = dict.fromkeys(list(Frames.keys()) + list(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) assert testcase.__name__ not in globals() globals()[testcase.__name__] = testcase create_read_tag_tests() 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=b"")) id3.update_to_v24() self.failUnlessEqual(id3["APIC:cover"].mime, "image/png") def test_lnk(self): from mutagen.id3 import LNK id3 = ID3() id3.version = (2, 2) id3.add(LNK(frameid="PIC", url="http://foo.bar")) id3.update_to_v24() self.assertFalse(id3.getall("LINK")) 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")) class Issue97_UpgradeUnknown23(TestCase): SILENCE = os.path.join(DATA_DIR, "97-unknown-23-update.mp3") def setUp(self): 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], b"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 = [b"foobar"] unknown.update_to_v24() self.failUnless(unknown.unknown_frames) def test_unknown_invalid(self): f = ID3(self.filename, translate=False) f.unknown_frames = [b"foobar", b"\xff" * 50] # throw away invalid frames f.update_to_v24() self.failIf(f.unknown_frames) def tearDown(self): os.unlink(self.filename) class BrokenDiscarded(TestCase): def test_empty(self): from mutagen.id3 import TPE1, ID3JunkFrameError self.assertRaises(ID3JunkFrameError, TPE1._fromData, _24, 0x00, b'') def test_wacky_truncated_RVA2(self): from mutagen.id3 import RVA2, ID3JunkFrameError data = b'\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 = b'\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() id3._header = ID3Header() id3._header.version = (2, 4, 0) tail = b'\x00\x00\x00\x03\x00\x00' b'\x01\x02\x03' for head in b'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 = b'\x00\x00\x00\x03\x00\x00' b'\x01\x02\x03' for head in [b'\x06\xaf\xfe\x20', b'ABC\x00', b'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 = b'\x01\x00\x00\x00\xff\xfe\x00\xff\xfeh\x00' self.assertRaises(ID3JunkFrameError, COMM._fromData, _24, 0x00, data) class BrokenButParsed(TestCase): def test_zerolength_framedata(self): from mutagen.id3 import Frames id3 = ID3() tail = b'\x00' * 6 for head in b'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, b'\x01\x00') self.assertEquals(u'', tpe1) tpe1 = TPE1._fromData(_24, 0, b'\x01\x00\x00\x00\x00') self.assertEquals([u'', u''], tpe1) def test_utf16_wrongnullterm(self): # issue 169 from mutagen.id3 import TPE1 tpe1 = TPE1._fromData( _24, 0, b'\x01\xff\xfeH\x00e\x00l\x00l\x00o\x00\x00') self.assertEquals(tpe1, [u'Hello']) def test_fake_zlib(self): from mutagen.id3 import TPE1, Frame header = ID3Header() header.version = (2, 4, 0) self.assertRaises(ID3JunkFrameError, TPE1._fromData, header, Frame.FLAG24_COMPRESS, b'\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) >= b'\x80'[0], "data is not syncsafe: %r" % data) def test_ql_0_12_missing_uncompressed_size(self): from mutagen.id3 import TPE1 tag = TPE1._fromData( _24, 0x08, b'x\x9cc\xfc\xff\xaf\x84!\x83!\x93' b'\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, b'\x00\x00\x00\x0f' b'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 = b'TIT1\x00\x00\x01\x00\x00\x00\x00' tail = b'TPE1\x00\x00\x00\x05\x00\x00\x00Yay!' id3 = ID3() id3._header = ID3Header() id3._header.version = (2, 4, 0) tagsgood = list( id3._ID3__read_frames(head + b'a' * 127 + tail, Frames)) tagsbad = list(id3._ID3__read_frames(head + b'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(id3._ID3__read_frames(head + b'a' * 127, Frames)) tagsbad = list(id3._ID3__read_frames(head + b'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(DATA_DIR, 'silence-44-s.mp3') newsilence = join(DATA_DIR, '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(b"!") f.close() ID3(self.silence).save(self.newsilence) self.assert_(os.path.getsize(self.newsilence) > 1) self.assertEquals(open(self.newsilence, "rb").read()[-1], b"!"[0]) def tearDown(self): try: os.unlink(self.newsilence) except OSError: pass class WriteRoundtrip(TestCase): silence = join(DATA_DIR, 'silence-44-s.mp3') newsilence = join(DATA_DIR, '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_silencetag(self): id3 = ID3(self.newsilence) os.unlink(self.newsilence) id3.save(self.newsilence) self.assertEquals(b'ID3', open(self.newsilence, 'rb').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(b'ID3', open(self.newsilence, 'rb').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, 'rb').read(10), b'') def test_delete_invalid_zero(self): f = open(self.newsilence, 'wb') f.write(b'ID3\x04\x00\x00\x00\x00\x00\x00abc') f.close() ID3(self.newsilence).delete() self.assertEquals(open(self.newsilence, 'rb').read(10), b'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=b"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(b"TIT2") < data.find(b"APIC")) self.assert_(data.find(b"TIT2") < data.find(b"COMM")) self.assert_(data.find(b"TALB") < data.find(b"APIC")) self.assert_(data.find(b"TALB") < data.find(b"COMM")) self.assert_(data.find(b"TIT2") < data.find(b"TALB")) def tearDown(self): try: os.unlink(self.newsilence) except EnvironmentError: pass class WriteForEyeD3(TestCase): silence = join(DATA_DIR, 'silence-44-s.mp3') newsilence = join(DATA_DIR, '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(DATA_DIR, '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(DATA_DIR, 'bad-POPM-frame.mp3') newfilename = join(DATA_DIR, '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( b'ABCTAGhello world\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\xff' ) self.failUnlessEqual(tag["TIT2"], "hello world") def test_short_year(self): from mutagen.id3 import ParseID3v1, _find_id3v1 data = ( b'XTAGhello world\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\xff' ) tag = ParseID3v1(data) self.failUnlessEqual(tag["TIT2"], "hello world") self.failUnlessEqual(tag["TDRC"], "0001") frames, offset = _find_id3v1(cBytesIO(data)) self.assertEqual(offset, -125) self.assertEqual(frames, tag) 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(DATA_DIR, "silence-44-s.mp3") def setUp(self): 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) class Read22FrameNamesin23(TestCase): def test_PIC_in_23(self): fd, filename = mkstemp(suffix='.mp3') os.close(fd) try: with open(filename, "wb") as h: # contains a bad upgraded frame, 2.3 structure with 2.2 name. # PIC was upgraded to APIC, but mime was not h.write(b"ID3\x03\x00\x00\x00\x00\x08\x00PIC\x00\x00\x00" b"\x00\x0b\x00\x00\x00JPG\x00\x03foo\x00\x42" b"\x00" * 100) id3 = ID3(filename) self.assertEqual(id3.version, (2, 3, 0)) self.assertTrue(id3.getall("APIC")) frame = id3.getall("APIC")[0] self.assertEqual(frame.mime, "image/jpeg") self.assertEqual(frame.data, b"\x42") self.assertEqual(frame.type, 3) self.assertEqual(frame.desc, "foo") finally: os.remove(filename) class ID3V1_vs_APEv2(TestCase): SILENCE = os.path.join(DATA_DIR, "silence-44-s.mp3") def setUp(self): fd, self.filename = mkstemp(suffix='.mp3') os.close(fd) shutil.copy(self.SILENCE, self.filename) def test_save_id3_over_ape(self): id3.delete(self.filename, delete_v2=False) ape_tag = APEv2() ape_tag["oh"] = ["no"] ape_tag.save(self.filename) ID3(self.filename).save() self.assertEqual(APEv2(self.filename)["oh"], "no") def test_delete_id3_with_ape(self): ID3(self.filename).save(v1=2) ape_tag = APEv2() ape_tag["oh"] = ["no"] ape_tag.save(self.filename) id3.delete(self.filename, delete_v2=False) self.assertEqual(APEv2(self.filename)["oh"], "no") def test_ape_id3_lookalike(self): # mp3 with apev2 tag that parses as id3v1 (at least with ParseID3v1) id3.delete(self.filename, delete_v2=False) ape_tag = APEv2() ape_tag["oh"] = [ "noooooooooo0000000000000000000000000000000000ooooooooooo"] ape_tag.save(self.filename) ID3(self.filename).save() self.assertTrue(APEv2(self.filename)) def tearDown(self): os.remove(self.filename) class TID3Misc(TestCase): def test_main(self): self.assertEqual(id3.Encoding.UTF8, 3) self.assertEqual(id3.ID3v1SaveOptions.UPDATE, 1) self.assertEqual(id3.PictureType.COVER_FRONT, 3) def test_determine_bpi(self): determine_bpi = id3._determine_bpi # default to BitPaddedInt self.assertTrue(determine_bpi("", {}) is BitPaddedInt) def get_frame_data(name, size, bpi=True): data = name if bpi: data += BitPaddedInt.to_str(size) else: data += BitPaddedInt.to_str(size, bits=8) data += b"\x00\x00" + b"\x01" * size return data data = get_frame_data(b"TPE2", 1000, True) self.assertTrue(determine_bpi(data, Frames) is BitPaddedInt) self.assertTrue( determine_bpi(data + b"\x00" * 1000, Frames) is BitPaddedInt) data = get_frame_data(b"TPE2", 1000, False) self.assertTrue(determine_bpi(data, Frames) is int) self.assertTrue(determine_bpi(data + b"\x00" * 1000, Frames) is int) # in this case it helps that we know the frame name d = get_frame_data(b"TPE2", 1000) + get_frame_data(b"TPE2", 10) + \ b"\x01" * 875 self.assertTrue(determine_bpi(d, Frames) is BitPaddedInt) class TID3Padding(TestCase): def setUp(self): fd, self.filename = mkstemp(suffix='.mp3') os.close(fd) orig = os.path.join(DATA_DIR, "silence-44-s.mp3") shutil.copy(orig, self.filename) def tearDown(self): os.remove(self.filename) def test_fill_all(self): tag = ID3(self.filename) self.assertEqual(tag._padding, 1142) tag.delall("TPE1") # saving should increase the padding not decrease the tag size tag.save() tag = ID3(self.filename) self.assertEqual(tag._padding, 1166) def test_remove_add_padding(self): ID3(self.filename).save() tag = ID3(self.filename) old_padding = tag._padding old_size = os.path.getsize(self.filename) tag.save(padding=lambda x: 0) self.assertEqual(os.path.getsize(self.filename), old_size - old_padding) old_size = old_size - old_padding tag.save(padding=lambda x: 137) self.assertEqual(os.path.getsize(self.filename), old_size + 137) class TID3Corrupt(TestCase): def setUp(self): fd, self.filename = mkstemp(suffix='.mp3') os.close(fd) orig = os.path.join(DATA_DIR, "silence-44-s.mp3") shutil.copy(orig, self.filename) def tearDown(self): os.remove(self.filename) def test_header_too_small(self): with open(self.filename, "r+b") as h: h.truncate(5) self.assertRaises(id3.error, ID3, self.filename) def test_tag_too_small(self): with open(self.filename, "r+b") as h: h.truncate(50) self.assertRaises(id3.error, ID3, self.filename) def test_save(self): with open(self.filename, "r+b") as h: h.seek(5, 0) h.write(b"nope") self.assertRaises(id3.error, ID3().save, self.filename) try: import eyeD3 except ImportError: del WriteForEyeD3 mutagen-1.31/tests/test_asf.py0000644000175000017500000005702512574061222016662 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os import shutil from tempfile import mkstemp from tests import TestCase, DATA_DIR from mutagen._compat import PY3, text_type, PY2, izip from mutagen.asf import ASF, ASFHeaderError, ASFValue, UNICODE, DWORD, QWORD from mutagen.asf import BOOL, WORD, BYTEARRAY, GUID from mutagen.asf._util import guid2bytes, bytes2guid from mutagen.asf._objects import ContentDescriptionObject, \ ExtendedContentDescriptionObject, HeaderExtensionObject, \ MetadataObject, MetadataLibraryObject, CodecListObject, PaddingObject from mutagen.asf import ASFUnicodeAttribute, ASFError, ASFByteArrayAttribute, \ ASFBoolAttribute, ASFDWordAttribute, ASFQWordAttribute, ASFWordAttribute, \ ASFGUIDAttribute class TASFFile(TestCase): def test_not_my_file(self): self.failUnlessRaises( ASFHeaderError, ASF, os.path.join(DATA_DIR, "empty.ogg")) self.failUnlessRaises( ASFHeaderError, ASF, os.path.join(DATA_DIR, "click.mpc")) class TASFMisc(TestCase): def test_guid(self): ex = "75B22633-668E-11CF-A6D9-00AA0062CE6C" b = guid2bytes(ex) self.assertEqual(len(b), 16) self.assertTrue(isinstance(b, bytes)) self.assertEqual(bytes2guid(b), ex) class TASFInfo(TestCase): def setUp(self): # WMA 9.1 64kbps CBR 48khz self.wma1 = ASF(os.path.join(DATA_DIR, "silence-1.wma")) # WMA 9.1 Professional 192kbps VBR 44khz self.wma2 = ASF(os.path.join(DATA_DIR, "silence-2.wma")) # WMA 9.1 Lossless 44khz self.wma3 = ASF(os.path.join(DATA_DIR, "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) def test_codec_type(self): self.assertEqual(self.wma1.info.codec_type, "Windows Media Audio 9 Standard") self.assertEqual(self.wma2.info.codec_type, "Windows Media Audio 9 Professional") self.assertEqual(self.wma3.info.codec_type, "Windows Media Audio 9 Lossless") def test_codec_name(self): self.assertEqual(self.wma1.info.codec_name, "Windows Media Audio 9.1") self.assertEqual(self.wma2.info.codec_name, "Windows Media Audio 9.1 Professional") self.assertEqual(self.wma3.info.codec_name, "Windows Media Audio 9.1 Lossless") def test_codec_description(self): self.assertEqual(self.wma1.info.codec_description, "64 kbps, 48 kHz, stereo 2-pass CBR") self.assertEqual(self.wma2.info.codec_description, "192 kbps, 44 kHz, 2 channel 24 bit 2-pass VBR") self.assertEqual(self.wma3.info.codec_description, "VBR Quality 100, 44 kHz, 2 channel 16 bit 1-pass VBR") def test_pprint(self): self.assertTrue(self.wma1.info.pprint()) self.assertTrue(isinstance(self.wma1.info.pprint(), text_type)) 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) class TASFMixin(object): def test_header_object_misc(self): header = self.audio._header header.pprint() repr(header) def test_delete(self): self.audio["QL/Bla"] = u"Foooooooooooooooooo" self.audio.save(padding=lambda x: 0) filesize = os.path.getsize(self.audio.filename) self.audio.delete() self.assertTrue(os.path.getsize(self.audio.filename) < filesize) 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 izip(sorted(newvalue), sorted(result or value)): self.failUnlessEqual(a, b) else: self.failUnlessEqual(self.audio[key], result or value) def test_slice(self): tags = self.audio.tags tags.clear() tags["Author"] = [u"Foo", u"Bar"] self.assertEqual(tags[:], [("Author", "Foo"), ("Author", "Bar")]) del tags[:] self.assertEqual(tags[:], []) tags[:] = [("Author", "Baz")] self.assertEqual(tags.items(), [("Author", ["Baz"])]) def test_iter(self): self.assertEqual(next(iter(self.audio.tags)), ("Title", "test")) self.assertEqual(list(self.audio.tags)[0], ("Title", "test")) 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(b'\x9eZl}\x89\xa2\xb5D\xb8\xa30\xfe', GUID) self.set_key(u"WM/WMCollectionGroupID", value, [value]) def test_py3_bytes(self): if PY3: value = ASFValue(b'\xff\x00', BYTEARRAY) self.set_key(u"QL/Something", [b'\xff\x00'], [value]) def test_set_invalid(self): setitem = self.audio.__setitem__ if PY2: self.assertRaises(ValueError, setitem, u"QL/Something", [b"\xff"]) self.assertRaises(TypeError, setitem, u"QL/Something", [object()]) # don't delete on error setitem(u"QL/Foobar", [u"ok"]) self.assertRaises(TypeError, setitem, u"QL/Foobar", [object()]) self.assertEqual(self.audio[u"QL/Foobar"], [u"ok"]) 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", 12, [ASFValue(12, QWORD)]) def test_auto_qword_list(self): self.set_key(u"WM/Track", [12, 13], [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"][1].stream, 2) self.failUnlessEqual(self.audio["QL/OneHasStream"][0].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"][1].language, 2) self.failUnlessEqual(self.audio["QL/OneHasLang"][0].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) # order not preserved here because they end up in different objects. self.failUnlessEqual(self.audio["QL/Mix"][1].language, None) self.failUnlessEqual(self.audio["QL/Mix"][1].stream, 1) self.failUnlessEqual(self.audio["QL/Mix"][2].language, 2) self.failUnlessEqual(self.audio["QL/Mix"][2].stream, 0) self.failUnlessEqual(self.audio["QL/Mix"][3].language, 4) self.failUnlessEqual(self.audio["QL/Mix"][3].stream, 3) self.failUnlessEqual(self.audio["QL/Mix"][0].language, None) self.failUnlessEqual(self.audio["QL/Mix"][0].stream, None) def test_data_size(self): v = ASFValue("", UNICODE, data=b'4\xd8\x1e\xdd\x00\x00') self.failUnlessEqual(v.data_size(), len(v._render())) class TASFAttributes(TestCase): def test_ASFUnicodeAttribute(self): if PY3: self.assertRaises(TypeError, ASFUnicodeAttribute, b"\xff") else: self.assertRaises(ValueError, ASFUnicodeAttribute, b"\xff") val = u'\xf6\xe4\xfc' self.assertEqual(ASFUnicodeAttribute(val.encode("utf-8")), val) self.assertRaises(ASFError, ASFUnicodeAttribute, data=b"\x00") self.assertEqual(ASFUnicodeAttribute(u"foo").value, u"foo") def test_ASFUnicodeAttribute_dunder(self): attr = ASFUnicodeAttribute(u"foo") self.assertEqual(bytes(attr), b"f\x00o\x00o\x00") self.assertEqual(text_type(attr), u"foo") if PY3: self.assertEqual(repr(attr), "ASFUnicodeAttribute('foo')") else: self.assertEqual(repr(attr), "ASFUnicodeAttribute(u'foo')") self.assertRaises(TypeError, int, attr) def test_ASFByteArrayAttribute(self): self.assertRaises(TypeError, ASFByteArrayAttribute, u"foo") self.assertEqual(ASFByteArrayAttribute(data=b"\xff").value, b"\xff") def test_ASFByteArrayAttribute_dunder(self): attr = ASFByteArrayAttribute(data=b"\xff") self.assertEqual(bytes(attr), b"\xff") self.assertEqual(text_type(attr), u"[binary data (1 bytes)]") if PY3: self.assertEqual(repr(attr), r"ASFByteArrayAttribute(b'\xff')") else: self.assertEqual(repr(attr), r"ASFByteArrayAttribute('\xff')") self.assertRaises(TypeError, int, attr) def test_ASFByteArrayAttribute_compat(self): ba = ASFByteArrayAttribute() ba.value = b"\xff" self.assertEqual(ba._render(), b"\xff") def test_ASFGUIDAttribute(self): self.assertEqual(ASFGUIDAttribute(data=b"\xff").value, b"\xff") self.assertRaises(TypeError, ASFGUIDAttribute, u"foo") def test_ASFGUIDAttribute_dunder(self): attr = ASFGUIDAttribute(data=b"\xff") self.assertEqual(bytes(attr), b"\xff") if PY3: self.assertEqual(text_type(attr), u"b'\\xff'") self.assertEqual(repr(attr), "ASFGUIDAttribute(b'\\xff')") else: self.assertEqual(text_type(attr), u"'\\xff'") self.assertEqual(repr(attr), "ASFGUIDAttribute('\\xff')") self.assertRaises(TypeError, int, attr) def test_ASFBoolAttribute(self): self.assertEqual( ASFBoolAttribute(data=b"\x01\x00\x00\x00").value, True) self.assertEqual( ASFBoolAttribute(data=b"\x00\x00\x00\x00").value, False) self.assertEqual(ASFBoolAttribute(False).value, False) def test_ASFBoolAttribute_dunder(self): attr = ASFBoolAttribute(False) self.assertEqual(bytes(attr), b"False") self.assertEqual(text_type(attr), u"False") self.assertEqual(repr(attr), "ASFBoolAttribute(False)") self.assertRaises(TypeError, int, attr) def test_ASFWordAttribute(self): self.assertEqual( ASFWordAttribute(data=b"\x00" * 2).value, 0) self.assertEqual( ASFWordAttribute(data=b"\xff" * 2).value, 2 ** 16 - 1) self.assertRaises(ValueError, ASFWordAttribute, -1) self.assertRaises(ValueError, ASFWordAttribute, 2 ** 16) def test_ASFWordAttribute_dunder(self): attr = ASFWordAttribute(data=b"\x00" * 2) self.assertEqual(bytes(attr), b"0") self.assertEqual(text_type(attr), u"0") self.assertEqual(repr(attr), "ASFWordAttribute(0)") self.assertEqual(int(attr), 0) def test_ASFDWordAttribute(self): self.assertEqual( ASFDWordAttribute(data=b"\x00" * 4).value, 0) self.assertEqual( ASFDWordAttribute(data=b"\xff" * 4).value, 2 ** 32 - 1) self.assertRaises(ValueError, ASFDWordAttribute, -1) self.assertRaises(ValueError, ASFDWordAttribute, 2 ** 32) def test_ASFDWordAttribute_dunder(self): attr = ASFDWordAttribute(data=b"\x00" * 4) self.assertEqual(bytes(attr), b"0") self.assertEqual(text_type(attr), u"0") self.assertEqual(repr(attr).replace("0L", "0"), "ASFDWordAttribute(0)") self.assertEqual(int(attr), 0) def test_ASFQWordAttribute(self): self.assertEqual( ASFQWordAttribute(data=b"\x00" * 8).value, 0) self.assertEqual( ASFQWordAttribute(data=b"\xff" * 8).value, 2 ** 64 - 1) self.assertRaises(ValueError, ASFQWordAttribute, -1) self.assertRaises(ValueError, ASFQWordAttribute, 2 ** 64) def test_ASFQWordAttribute_dunder(self): attr = ASFQWordAttribute(data=b"\x00" * 8) self.assertEqual(bytes(attr), b"0") self.assertEqual(text_type(attr), u"0") self.assertEqual(repr(attr).replace("0L", "0"), "ASFQWordAttribute(0)") self.assertEqual(int(attr), 0) class TASFTags1(TASF, TASFMixin): original = os.path.join(DATA_DIR, "silence-1.wma") class TASFTags2(TASF, TASFMixin): original = os.path.join(DATA_DIR, "silence-2.wma") class TASFTags3(TASF, TASFMixin): original = os.path.join(DATA_DIR, "silence-3.wma") class TASFIssue29(TestCase): original = os.path.join(DATA_DIR, "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_pprint(self): self.audio.pprint() 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) class TASFAttrDest(TestCase): original = os.path.join(DATA_DIR, "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.clear() audio.save() def tearDown(self): os.unlink(self.filename) def test_author(self): audio = ASF(self.filename) values = [u"Foo", u"Bar", u"Baz"] audio["Author"] = values audio.save() self.assertEqual( list(audio.to_content_description.items()), [(u"Author", u"Foo")]) self.assertEqual( audio.to_metadata_library, [(u"Author", u"Bar"), (u"Author", u"Baz")]) new = ASF(self.filename) self.assertEqual(new["Author"], values) def test_author_long(self): audio = ASF(self.filename) # 2 ** 16 - 2 bytes encoded text + 2 bytes termination just_small_enough = u"a" * (((2 ** 16) // 2) - 2) audio["Author"] = [just_small_enough] audio.save() self.assertTrue(audio.to_content_description) self.assertFalse(audio.to_metadata_library) audio["Author"] = [just_small_enough + u"a"] audio.save() self.assertFalse(audio.to_content_description) self.assertTrue(audio.to_metadata_library) def test_multi_order(self): audio = ASF(self.filename) audio["Author"] = [u"a", u"b", u"c"] audio.save() audio = ASF(self.filename) self.assertEqual(audio["Author"], [u"a", u"b", u"c"]) def test_multi_order_extended(self): audio = ASF(self.filename) audio["WM/Composer"] = [u"a", u"b", u"c"] audio.save() audio = ASF(self.filename) self.assertEqual(audio["WM/Composer"], [u"a", u"b", u"c"]) def test_non_text_type(self): audio = ASF(self.filename) audio["Author"] = [42] audio.save() self.assertFalse(audio.to_content_description) new = ASF(self.filename) self.assertEqual(new["Author"], [42]) def test_empty(self): audio = ASF(self.filename) audio["Author"] = [u"", u""] audio["Title"] = [u""] audio["Copyright"] = [] audio.save() new = ASF(self.filename) self.assertEqual(new["Author"], [u"", u""]) self.assertEqual(new["Title"], [u""]) self.assertFalse("Copyright" in new) class TASFLargeValue(TestCase): original = os.path.join(DATA_DIR, "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(b"." * 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(b"." * (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(b" " * 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)) class TASFSave(TestCase): # http://code.google.com/p/mutagen/issues/detail?id=81#c4 original = os.path.join(DATA_DIR, "silence-1.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_save_filename(self): self.audio.save(self.audio.filename) def test_multiple_delete(self): self.audio["large_value1"] = "#" * 50000 self.audio.save() audio = ASF(self.filename) for tag in audio.keys(): del(audio[tag]) audio.save() def test_readd_objects(self): header = self.audio._header del header.objects[:] self.audio.save() self.assertTrue(header.get_child(ContentDescriptionObject.GUID)) self.assertTrue( header.get_child(ExtendedContentDescriptionObject.GUID)) self.assertTrue(header.get_child(HeaderExtensionObject.GUID)) ext = header.get_child(HeaderExtensionObject.GUID) self.assertTrue(ext.get_child(MetadataObject.GUID)) self.assertTrue(ext.get_child(MetadataLibraryObject.GUID)) def test_keep_others(self): self.audio.save() new = ASF(self.filename) self.assertTrue(new._header.get_child(CodecListObject.GUID)) def test_padding(self): old_tags = sorted(self.audio.items()) def get_padding(fn): header = ASF(fn)._header return len(header.get_child(PaddingObject.GUID).data) for i in [0, 1, 2, 3, 42, 100, 5000, 30432, 1]: def padding_cb(info): self.assertEqual(info.size, 30432) return i self.audio.save(padding=padding_cb) self.assertEqual(get_padding(self.filename), i) last = ASF(self.filename) self.assertEqual(sorted(last.items()), old_tags) mutagen-1.31/tests/test__toolsutil.py0000644000175000017500000000356512574061222020306 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os from mutagen._toolsutil import get_win32_unicode_argv, split_escape from mutagen._compat import text_type from tests import TestCase class Tget_win32_unicode_argv(TestCase): def test_main(self): argv = get_win32_unicode_argv() if os.name == "nt" and argv: self.assertTrue(isinstance(argv[0], text_type)) class Tsplit_escape(TestCase): def test_split_escape(self): 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_types(self): parts = split_escape(b"\xff:\xff", b":") self.assertEqual(parts, [b"\xff", b"\xff"]) self.assertTrue(isinstance(parts[0], bytes)) parts = split_escape(b"", b":") self.assertEqual(parts, [b""]) self.assertTrue(isinstance(parts[0], bytes)) parts = split_escape(u"a:b", u":") self.assertEqual(parts, [u"a", u"b"]) self.assertTrue(all(isinstance(p, text_type) for p in parts)) parts = split_escape(u"", u":") self.assertEqual(parts, [u""]) self.assertTrue(all(isinstance(p, text_type) for p in parts)) parts = split_escape(u":", u":") self.assertEqual(parts, [u"", u""]) self.assertTrue(all(isinstance(p, text_type) for p in parts)) mutagen-1.31/tests/data/0000755000175000017500000000000012574256060015406 5ustar lazkalazka00000000000000mutagen-1.31/tests/data/48k-2ch-s16-silence.aif0000644000175000017500000004546612574061222021207 0ustar lazkalazka00000000000000FORMK.AIFFCOMM@SSNDKmutagen-1.31/tests/data/silence-44-s-mpeg2.mp30000644000175000017500000002057012574061222021144 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.31/tests/data/empty.aac0000644000175000017500000000502112574061222017202 0ustar lazkalazka00000000000000P?libfaac 1.28B 2GP!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#P!I#mutagen-1.31/tests/data/truncated-64bit.mp40000644000175000017500000000372012574061222020743 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.31/tests/data/empty.ofs0000644000175000017500000000073012574061222017247 0ustar lazkalazka00000000000000OFR D@ HEAD,RIFF$ WAVEfmt Ddata COMP}w @5.<TAILmutagen-1.31/tests/data/empty.ogg0000644000175000017500000001035012574061222017233 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.31/tests/data/52-overwritten-metadata.flac0000644000175000017500000000100012574061222022610 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.31/tests/data/sv8_header.mpc0000644000175000017500000000016212574061222020130 0ustar lazkalazka00000000000000MPCKSH*)^RG E}ZE}ZEIQSODAPAPAPAPAPAPAPAPAPAPAPAPAPAPAPSTSEmutagen-1.31/tests/data/sample_bitrate.oggtheora0000644000175000017500000000600012574061222022270 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.31/tests/data/106-short-picture-block-size.flac0000644000175000017500000001112412574061222023402 0ustar lazkalazka00000000000000fLaC" BzAf鄚0 ! d! d! d! d! d8! d7p! d7p! d7p! d7p! d7p! d7p! d7p! d7p! d7p! d7p! d7p! dmutagen-1.31/tests/data/too-short.mp30000644000175000017500000000515512574061222017765 0ustar lazkalazka00000000000000ID3YTT2 Track 10TP1 HieroglyphTAL HieroglyphTRK10/10TENiTunes v4.9.0.17COMhengiTunNORM 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000COMcengiTunes_CDDB_16A09F20A+191100+10+150+16638+36822+53548+67697+86438+108987+128071+147058+168229COMengiTunes_CDDB_TrackNumber10@7mutagen-1.31/tests/data/no_length.wv0000644000175000017500000000102412574061222017730 0ustar lazkalazka00000000000000wvpkh"V!Bw!RIFF$ WAVEfmt Ddata eXwvpk4@~"V"V!BwXwvpk4@~D"V!BwXwvpk4@~f"V!BwXwvpk4@~X"V!BwXwvpk4@~"V!BwXwvpk4@~<!܉twvpk4@~A<!܉tmutagen-1.31/tests/data/silence-2.wma0000644000175000017500000005510612574061222017676 0ustar lazkalazka000000000000000&ufbl3&ufbl4 testܫG Seh݀cBl?[FZ0ܤ@#w<+""~_. Se:ӫ Se FC|K)9>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.31/tests/data/52-too-short-block-size.flac0000644000175000017500000014152012574061222022454 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.31/tests/data/64bit.mp40000644000175000017500000000012512574061222016750 0ustar lazkalazka00000000000000moovMudta=meta-!ilstcpildatamutagen-1.31/tests/data/variable-block.flac0000644000175000017500000002400012574061222021100 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.31/tests/data/145-invalid-item-count.apev20000644000175000017500000000135612574061222022363 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.31/tests/data/8k-1ch-1s-silence.aif0000644000175000017500000003726612574061222021033 0ustar lazkalazka00000000000000FORM>AIFFCOMM@@ SSND>mutagen-1.31/tests/data/id3v23_unsynch.id30000644000175000017500000000050012574061222020555 0ustar lazkalazka00000000000000ID30TIT25My babe just cares for meTPE1Nina SimoneTALB100% JazzTRCK03TLEN@216000 HI ,^% >}G˿50}KR"J\ĻC圂gmutagen-1.31/tests/data/empty.ofr0000644000175000017500000000020012574061222017236 0ustar lazkalazka00000000000000OFR D@ HEAD,RIFF$ WAVEfmt Ddata COMP%Q_A@5TAILmutagen-1.31/tests/data/empty.spx0000644000175000017500000005735512574061222017311 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.31/tests/data/sample_length.oggtheora0000644000175000017500000004000012574061222022115 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.31/tests/data/alac.m4a0000644000175000017500000002240412574061222016705 0ustar lazkalazka00000000000000 ftypM4A M4A mp42isom moovlmvhd]]Dz@jtrak\tkhd]]z@mdia mdhd]]DzU"hdlrsounminfsmhd$dinfdref url stblXstsdHalacD$alac( $ D stts' stscstsz( $0stco !@!"# #$` 1udta )meta"hdlrmdirapplilstnamdataemptycpildatapgapdatatmpodata#toodataiTunes 11.1g----meancom.apple.iTunesnameEncoding Params(dataversacbfvbrq----meancom.apple.iTunesnameiTunNORMjdata 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000^freefree mdat                                                                                 _Wmutagen-1.31/tests/data/xing.mp30000644000175000017500000002002012574061222016760 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.31/tests/data/CVE-2007-4619-12.flac0000644000175000017500000014333012574061222020057 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.31/tests/data/sample.oggtheora0000644000175000017500000004740512574061222020574 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.31/tests/data/vbri.mp30000644000175000017500000002000012574061222016753 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%h|(呤@ a鏂`$22019tS2 2R1E"200?0`.1$"(tP8+V GYWG; ھ "j1*-t 9U΋OaCϬu˝Vj+.EpiѦI)DQ_yوfQAXfYV[S,9j勷2ݜk;9~??>/wy?zo=s_ܿ{>)@0#ZXǏE$M@F,Hl F={d)W2ja -i7+OTL_o\C|EǾnYig88X|2cpU@Ybj[ eP; >f@2b3QvC+p$ }Z NgSϰ؀6YϥWG ,P\$x;B aSRSNAEPlW+$Y[(ѝM#LHoA5baN`Tip]IHB" @9A0#'^rύQb mRv]Z`0΁D:W rKg])!!Fw8gCRI ϗu%GD.ʹG-;c3'<|KSxkӔr"eD󙓕 Q8 MV Hdy5m U?`k4MD y3IGꄡYLX"څ>6[Y2WPD[A.Sp/Ehi g%Xf2/&rۊM9l#婫.AV RCJv[#( Jr@ A@ E'iL GVa2N>: KC40l3]WXԘ֬5&֬5&IC֫ƪaRj2IbRgV&5FX(*(S P wLAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUpdgP/3Z %E$q4UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU0dKk   4UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUmutagen-1.31/tests/data/empty.tta0000644000175000017500000023326212574061222017260 0ustar lazkalazka00000000000000TTA1Dz)RLZRZZ'` _ @ @  P@@(@   P@ @ (( P P(A@!  (HB%%B D@APPP`(`P@IPPP(@TC@!PX @a @A& d(00 L @&" E(EaPtP`LLT EP[2`0`ee$@Y ˀ d0lr1I&a2`* eeTILR6 e0&P  eQ[0 `*&`eT`e0 ` e2*0 P6 0 ee@ eP6LB52PL `R\! &@``( &P2jL`ʘee &Ap0 P 0PeL $e0 0WRI(1a(I dP6L C eS IIL0 Pm2lj T@6LA  @5 0 @1&(I&P0PF5&6$ (T0`0!L` @Y5ʀ2jd0$P)ee!L2j`V @0P$0LLe3(l@T&eLʀ2`(cB(eP L L2 &@5 PV@5՘@P( 2p'!M ` I` ʨjE e ( $P! a`P6\6'` * e@eTC@T P@A0`k*ajP6 C2 (`!@ 0 CH`@( &0Y 2PL`)j@Ym #C1($ ( <& (S`rɐ00 `00 f ePF2j%L5&P2a$@x!R2TP`” '0 &af0LAPV6 C`d2ef&$S!L! L2l&IT d& 0$0A a3 C( aC (ʀ0A abʨ`@! (AJ *V-  x&a`Ն0@0T 00 . a( OQca! A`L`j  @\m$&aA(0!yõ2@9aTl\0! Pa&aL@6SaIՆ0eI0$ aeCLj#a0  6\m&eCjL2dAePa(=̜ - y( @50 a d$ a A23P$Px2&'I R6L2@! 0WIxP)0P)0՘dL.d!L0$3Ax6u{0Pa` Pa` CH$ Cr$Lep9 ddM2 d0pfA L`j6'S[ɐ &$3x8<$O5&gAIPF5D02<'3  @6!Leh 0d g&a0 CA2C0I1WIfH0&a۬$2Hf0eaA2C2HfB$3\MƩ@sj.r3a'dj*aN<CPL jC yS! @fN23(Y mI"0 @$3d\$p2(0jF L&0 rmd&@$@ٓ! f0 %0a$ aL&a83&a83d3 0uIf!y<3'pm&C9 s5!L2 AP69@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.31/tests/data/8k-1ch-3.5s-silence.aif0000644000175000017500000015536612574061222021202 0ustar lazkalazka00000000000000FORMAIFFCOMMm`@ SSNDmutagen-1.31/tests/data/mac-390-hdr.ape0000644000175000017500000000020012574061222017703 0ustar lazkalazka00000000000000MAC <D, nd' RIFF*WAVEfmt Ddata*|L~c-d= YkRJnwmutagen-1.31/tests/data/has-tags.m4a0000644000175000017500000001176412574061222017523 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.31/tests/data/silence-3.wma0000644000175000017500000007644412574061222017707 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.31/tests/data/no-tags.m4a0000644000175000017500000000552212574061222017357 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.31/tests/data/no-tags.flac0000644000175000017500000001112412574061222017576 0ustar lazkalazka00000000000000fLaC" BzAf鄚0  >>P0ݵr%BXàfT=FBMAsBV @釆^QI>JUl7'ar=E jS/(6,Vj֫*K)4b8_ZNaybW-^۩ngsrۙNn\ε}Nɚi2\M*Hi0 0b2l;1l2D00420? B0Ҏa@̄\*"h3d 5/v a*.˩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.31/tests/data/106-invalid-streaminfo.flac0000644000175000017500000001112412574061222022325 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.31/tests/data/bad-POPM-frame.mp30000644000175000017500000000372012574061222020412 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.31/tests/data/id3v22-test.mp30000644000175000017500000001200012574061222020000 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.31/tests/data/no-tags.3g20000644000175000017500000020535712574061222017301 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.31/tests/data/sv5_header.mpc0000644000175000017500000000020012574061222020116 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.31/tests/data/CVE-2007-4619-1.flac0000644000175000017500000014333012574061222017775 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.31/tests/data/example.opus0000644000175000017500000017602012574061222017751 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.31/tests/data/silence-44-s.mp30000644000175000017500000004000012574061222020122 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.31/tests/data/silence-44-s-v1.mp30000644000175000017500000003533612574061222020466 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.31/tests/data/issue_29.wma0000644000175000017500000007640012574061222017557 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.31/tests/data/ooming-header.flac0000644000175000017500000000012612574061222020744 0ustar lazkalazka00000000000000fLaC"B B]{&VC1( reference libFLAC 1.2.0 20070715%nmutagen-1.31/tests/data/brokentag.apev20000644000175000017500000004013412574061222020315 0ustar lazkalazka00000000000000APETAGEX0000000000000000APETAGEXxTrack07ArtistAnArtist TitleSome Music AlbumA test caseAPETAGEXxmutagen-1.31/tests/data/mac-396.ape0000644000175000017500000000015012574061222017142 0ustar lazkalazka00000000000000MAC xD,zRIFF$ WAVEfmt Ddata Xmutagen-1.31/tests/data/apev2-lyricsv2.mp30000644000175000017500000014135212574061222020617 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.31/tests/data/almostempty.mpc0000644000175000017500000000002112574061222020450 0ustar lazkalazka00000000000000ID3 dsfdsfds MP+ mutagen-1.31/tests/data/sv4_header.mpc0000644000175000017500000000020012574061222020115 0ustar lazkalazka00000000000000' .'RK߷S1fYLbeYe1B ,hn ^ٕh3隩*\˳K|9vqZr+dՏ؉ZG,܋LvkJÜmutagen-1.31/tests/data/mac-399.ape0000644000175000017500000000025412574061222017152 0ustar lazkalazka00000000000000MAC 4 ,(fu3 qi :DRIFF$ WAVEfmt Ddata bbmutagen-1.31/tests/data/bad-TYER-frame.mp30000644000175000017500000011400012574061222020414 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.31/tests/data/oldtag.apev20000644000175000017500000004010412574061222017610 0ustar lazkalazka00000000000000APETAGEXxTrack07ArtistAnArtist TitleSome Music AlbumA test caseAPETAGEXxmutagen-1.31/tests/data/multipage-setup.ogg0000644000175000017500000022626712574061222021242 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.31/tests/data/silence-44-s-mpeg25.mp30000644000175000017500000001056012574061222021227 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.31/tests/data/with-id3.aif0000644000175000017500000004131012574061222017510 0ustar lazkalazka00000000000000FORMBAIFFCOMM@@ SSND>ID3  ID3TIT2 AIFF titlemutagen-1.31/tests/data/silence-44-s.flac0000644000175000017500000014333012574061222020342 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.31/tests/test_mp4.py0000644000175000017500000012043112574113424016603 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os import shutil import struct import subprocess from mutagen._compat import cBytesIO, PY3, text_type, PY2, izip from tempfile import mkstemp from tests import TestCase, DATA_DIR from mutagen.mp4 import (MP4, Atom, Atoms, MP4Tags, MP4Info, delete, MP4Cover, MP4MetadataError, MP4FreeForm, error, AtomDataType, MP4MetadataValueError, AtomError) from mutagen.mp4._util import parse_full_atom from mutagen.mp4._as_entry import AudioSampleEntry, ASEntryError from mutagen._util import cdata class TAtom(TestCase): def test_no_children(self): fileobj = cBytesIO(b"\x00\x00\x00\x08atom") atom = Atom(fileobj) self.failUnlessRaises(KeyError, atom.__getitem__, "test") def test_length_1(self): fileobj = cBytesIO(b"\x00\x00\x00\x01atom" b"\x00\x00\x00\x00\x00\x00\x00\x10" + b"\x00" * 16) atom = Atom(fileobj) self.failUnlessEqual(atom.length, 16) self.failUnlessEqual(atom.datalength, 0) def test_length_64bit_less_than_16(self): fileobj = cBytesIO(b"\x00\x00\x00\x01atom" b"\x00\x00\x00\x00\x00\x00\x00\x08" + b"\x00" * 8) self.assertRaises(AtomError, Atom, fileobj) def test_length_less_than_8(self): fileobj = cBytesIO(b"\x00\x00\x00\x02atom") self.assertRaises(AtomError, Atom, fileobj) def test_truncated(self): self.assertRaises(AtomError, Atom, cBytesIO(b"\x00")) self.assertRaises(AtomError, Atom, cBytesIO(b"\x00\x00\x00\x01atom")) def test_render_too_big(self): class TooBig(bytes): def __len__(self): return 1 << 32 data = TooBig(b"test") try: len(data) except OverflowError: # Py_ssize_t is still only 32 bits on this system. self.failUnlessRaises(OverflowError, Atom.render, b"data", data) else: data = Atom.render(b"data", data) self.failUnlessEqual(len(data), 4 + 4 + 8 + 4) def test_non_top_level_length_0_is_invalid(self): data = cBytesIO(struct.pack(">I4s", 0, b"whee")) self.assertRaises(AtomError, Atom, data, level=1) def test_length_0(self): fileobj = cBytesIO(b"\x00\x00\x00\x00atom" + 40 * b"\x00") atom = Atom(fileobj) self.failUnlessEqual(fileobj.tell(), 48) self.failUnlessEqual(atom.length, 48) self.failUnlessEqual(atom.datalength, 40) def test_length_0_container(self): data = cBytesIO(struct.pack(">I4s", 0, b"moov") + Atom.render(b"data", b"whee")) atom = Atom(data) self.failUnlessEqual(len(atom.children), 1) self.failUnlessEqual(atom.length, 20) self.failUnlessEqual(atom.children[-1].length, 12) def test_read(self): payload = 8 * b"\xff" fileobj = cBytesIO(b"\x00\x00\x00\x10atom" + payload) atom = Atom(fileobj) ok, data = atom.read(fileobj) self.assertTrue(ok) self.assertEqual(data, payload) payload = 7 * b"\xff" fileobj = cBytesIO(b"\x00\x00\x00\x10atom" + payload) atom = Atom(fileobj) ok, data = atom.read(fileobj) self.assertFalse(ok) self.assertEqual(data, payload) class TAtoms(TestCase): filename = os.path.join(DATA_DIR, "has-tags.m4a") def setUp(self): self.atoms = Atoms(open(self.filename, "rb")) def test_getitem(self): self.failUnless(self.atoms[b"moov"]) self.failUnless(self.atoms[b"moov.udta"]) self.failUnlessRaises(KeyError, self.atoms.__getitem__, b"whee") def test_contains(self): self.failUnless(b"moov" in self.atoms) self.failUnless(b"moov.udta" in self.atoms) self.failUnless(b"whee" not in self.atoms) def test_name(self): self.failUnlessEqual(self.atoms.atoms[0].name, b"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 = cBytesIO(Atom.render(b"data", b"whee") + b"\x00\x00") self.failUnless(Atoms(data)) def test_repr(self): repr(self.atoms) class TMP4Info(TestCase): def test_no_soun(self): self.failUnlessRaises( IOError, self.test_mdhd_version_1, b"vide") def test_mdhd_version_1(self, soun=b"soun"): mdhd = Atom.render(b"mdhd", (b"\x01\x00\x00\x00" + b"\x00" * 16 + b"\x00\x00\x00\x02" + # 2 Hz b"\x00\x00\x00\x00\x00\x00\x00\x10")) hdlr = Atom.render(b"hdlr", b"\x00" * 8 + soun) mdia = Atom.render(b"mdia", mdhd + hdlr) trak = Atom.render(b"trak", mdia) moov = Atom.render(b"moov", trak) fileobj = cBytesIO(moov) atoms = Atoms(fileobj) info = MP4Info(atoms, fileobj) self.failUnlessEqual(info.length, 8) def test_multiple_tracks(self): hdlr = Atom.render(b"hdlr", b"\x00" * 8 + b"whee") mdia = Atom.render(b"mdia", hdlr) trak1 = Atom.render(b"trak", mdia) mdhd = Atom.render(b"mdhd", (b"\x01\x00\x00\x00" + b"\x00" * 16 + b"\x00\x00\x00\x02" + # 2 Hz b"\x00\x00\x00\x00\x00\x00\x00\x10")) hdlr = Atom.render(b"hdlr", b"\x00" * 8 + b"soun") mdia = Atom.render(b"mdia", mdhd + hdlr) trak2 = Atom.render(b"trak", mdia) moov = Atom.render(b"moov", trak1 + trak2) fileobj = cBytesIO(moov) atoms = Atoms(fileobj) info = MP4Info(atoms, fileobj) self.failUnlessEqual(info.length, 8) class TMP4Tags(TestCase): def wrap_ilst(self, data): ilst = Atom.render(b"ilst", data) meta = Atom.render(b"meta", b"\x00" * 4 + ilst) data = Atom.render(b"moov", Atom.render(b"udta", meta)) fileobj = cBytesIO(data) return MP4Tags(Atoms(fileobj), fileobj) def test_parse_multiple_atoms(self): # while we don't write multiple values as multiple atoms # still read them # https://bitbucket.org/lazka/mutagen/issue/165 data = Atom.render(b"data", b"\x00\x00\x00\x01" + b"\x00" * 4 + b"foo") grp1 = Atom.render(b"\xa9grp", data) data = Atom.render(b"data", b"\x00\x00\x00\x01" + b"\x00" * 4 + b"bar") grp2 = Atom.render(b"\xa9grp", data) tags = self.wrap_ilst(grp1 + grp2) self.assertEqual(tags["\xa9grp"], [u"foo", u"bar"]) def test_purl(self): # purl can have 0 or 1 flags (implicit or utf8) data = Atom.render(b"data", b"\x00\x00\x00\x01" + b"\x00" * 4 + b"foo") purl = Atom.render(b"purl", data) tags = self.wrap_ilst(purl) self.failUnlessEqual(tags["purl"], ["foo"]) data = Atom.render(b"data", b"\x00\x00\x00\x00" + b"\x00" * 4 + b"foo") purl = Atom.render(b"purl", data) tags = self.wrap_ilst(purl) self.failUnlessEqual(tags["purl"], ["foo"]) # invalid flag data = Atom.render(b"data", b"\x00\x00\x00\x03" + b"\x00" * 4 + b"foo") purl = Atom.render(b"purl", data) tags = self.wrap_ilst(purl) self.assertFalse("purl" in tags) self.assertTrue("purl" in tags._failed_atoms) # invalid utf8 data = Atom.render( b"data", b"\x00\x00\x00\x01" + b"\x00" * 4 + b"\xff") purl = Atom.render(b"purl", data) tags = self.wrap_ilst(purl) self.assertFalse("purl" in tags) def test_genre(self): data = Atom.render(b"data", b"\x00" * 8 + b"\x00\x01") genre = Atom.render(b"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(b"cpil", Atom.render(b"data", b"\x00" * 8)) tags = self.wrap_ilst(cpil) self.assertFalse("cpil" in tags) def test_genre_too_big(self): data = Atom.render(b"data", b"\x00" * 8 + b"\x01\x00") genre = Atom.render(b"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(b"data", b"\x00" * 8 + b"whee") foob = Atom.render(b"foob", data) tags = self.wrap_ilst(foob) self.failIf(tags) def test_strips_bad_unknown_types(self): data = Atom.render(b"datA", b"\x00" * 8 + b"whee") foob = Atom.render(b"foob", data) tags = self.wrap_ilst(foob) self.failIf(tags) def test_bad_covr(self): data = Atom.render( b"foob", b"\x00\x00\x00\x0E" + b"\x00" * 4 + b"whee") covr = Atom.render(b"covr", data) tags = self.wrap_ilst(covr) self.assertFalse(tags) def test_covr_blank_format(self): data = Atom.render( b"data", b"\x00\x00\x00\x00" + b"\x00" * 4 + b"whee") covr = Atom.render(b"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), b"\x00\x00\x00\x19pgap\x00\x00\x00\x11data" b"\x00\x00\x00\x15\x00\x00\x00\x00\x01" ) self.failUnlessEqual( MP4Tags()._MP4Tags__render_bool('pgap', False), b"\x00\x00\x00\x19pgap\x00\x00\x00\x11data" b"\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), b"\x00\x00\x00*purl\x00\x00\x00\"data\x00\x00\x00\x00\x00\x00" b"\x00\x00http://foo/bar.xml" ) self.failUnlessEqual( MP4Tags()._MP4Tags__render_text( 'aART', [u'\u0041lbum Artist']), b"\x00\x00\x00$aART\x00\x00\x00\x1cdata\x00\x00\x00\x01\x00\x00" b"\x00\x00\x41lbum Artist" ) self.failUnlessEqual( MP4Tags()._MP4Tags__render_text( 'aART', [u'Album Artist', u'Whee']), b"\x00\x00\x008aART\x00\x00\x00\x1cdata\x00\x00\x00\x01\x00\x00" b"\x00\x00Album Artist\x00\x00\x00\x14data\x00\x00\x00\x01\x00" b"\x00\x00\x00Whee" ) def test_render_data(self): self.failUnlessEqual( MP4Tags()._MP4Tags__render_data('aART', 0, 1, [b'whee']), b"\x00\x00\x00\x1caART" b"\x00\x00\x00\x14data\x00\x00\x00\x01\x00\x00\x00\x00whee" ) self.failUnlessEqual( MP4Tags()._MP4Tags__render_data('aART', 0, 2, [b'whee', b'wee']), b"\x00\x00\x00/aART" b"\x00\x00\x00\x14data\x00\x00\x00\x02\x00\x00\x00\x00whee" b"\x00\x00\x00\x13data\x00\x00\x00\x02\x00\x00\x00\x00wee" ) def test_bad_text_data(self): data = Atom.render(b"datA", b"\x00\x00\x00\x01\x00\x00\x00\x00whee") data = Atom.render(b"aART", data) tags = self.wrap_ilst(data) self.assertFalse(tags) def test_bad_cprt(self): data = Atom.render(b"cprt", b"\x00\x00\x00#data\x00") tags = self.wrap_ilst(data) self.assertFalse(tags) def test_write_back_bad_atoms(self): # write a broken atom and try to load it data = Atom.render(b"datA", b"\x00\x00\x00\x01\x00\x00\x00\x00wheeee") data = Atom.render(b"aART", data) tags = self.wrap_ilst(data) self.assertFalse(tags) # save it into an existing mp4 original = os.path.join(DATA_DIR, "has-tags.m4a") fd, filename = mkstemp(suffix='.mp4') os.close(fd) shutil.copy(original, filename) try: delete(filename) # it should still end up in the file tags.save(filename) with open(filename, "rb") as h: self.assertTrue(b"wheeee" in h.read()) # if we define our own aART throw away the broken one tags["aART"] = ["new"] tags.save(filename) with open(filename, "rb") as h: self.assertFalse(b"wheeee" in h.read()) # add the broken one back and delete all tags including # the broken one del tags["aART"] tags.save(filename) with open(filename, "rb") as h: self.assertTrue(b"wheeee" in h.read()) delete(filename) with open(filename, "rb") as h: self.assertFalse(b"wheeee" in h.read()) finally: os.unlink(filename) def test_render_freeform(self): data = ( b"\x00\x00\x00a----" b"\x00\x00\x00\"mean\x00\x00\x00\x00net.sacredchao.Mutagen" b"\x00\x00\x00\x10name\x00\x00\x00\x00test" b"\x00\x00\x00\x14data\x00\x00\x00\x01\x00\x00\x00\x00whee" b"\x00\x00\x00\x13data\x00\x00\x00\x01\x00\x00\x00\x00wee" ) key = '----:net.sacredchao.Mutagen:test' self.failUnlessEqual( MP4Tags()._MP4Tags__render_freeform(key, [b'whee', b'wee']), data) def test_parse_freeform(self): double_data = ( b"\x00\x00\x00a----" b"\x00\x00\x00\"mean\x00\x00\x00\x00net.sacredchao.Mutagen" b"\x00\x00\x00\x10name\x00\x00\x00\x00test" b"\x00\x00\x00\x14data\x00\x00\x00\x01\x00\x00\x00\x00whee" b"\x00\x00\x00\x13data\x00\x00\x00\x01\x00\x00\x00\x00wee" ) key = '----:net.sacredchao.Mutagen:test' double_atom = \ MP4Tags()._MP4Tags__render_freeform(key, [b'whee', b'wee']) tags = self.wrap_ilst(double_data) self.assertTrue(key in tags) self.assertEqual(tags[key], [b'whee', b'wee']) tags2 = self.wrap_ilst(double_atom) self.assertEqual(tags, tags2) def test_multi_freeform(self): # merge multiple freeform tags with the same key mean = Atom.render(b"mean", b"\x00" * 4 + b"net.sacredchao.Mutagen") name = Atom.render(b"name", b"\x00" * 4 + b"foo") data = Atom.render(b"data", b"\x00\x00\x00\x01" + b"\x00" * 4 + b"bar") result = Atom.render(b"----", mean + name + data) data = Atom.render( b"data", b"\x00\x00\x00\x01" + b"\x00" * 4 + b"quux") result += Atom.render(b"----", mean + name + data) tags = self.wrap_ilst(result) values = tags["----:net.sacredchao.Mutagen:foo"] self.assertEqual(values[0], b"bar") self.assertEqual(values[1], b"quux") def test_bad_freeform(self): mean = Atom.render(b"mean", b"net.sacredchao.Mutagen") name = Atom.render(b"name", b"empty test key") bad_freeform = Atom.render(b"----", b"\x00" * 4 + mean + name) tags = self.wrap_ilst(bad_freeform) self.assertFalse(tags) 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 = (b"vers\x00\x00\x00\x01acbf\x00\x00\x00\x01brat\x00\x01\xf4" b"\x00cdcv\x00\x01\x05\x04") data = (b"\x00\x00\x00\x1cmean\x00\x00\x00\x00com.apple.iTunes\x00\x00" b"\x00\x1bname\x00\x00\x00\x00Encoding Params\x00\x00\x000data" b"\x00\x00\x00\x00\x00\x00\x00\x00vers\x00\x00\x00\x01acbf\x00" b"\x00\x00\x01brat\x00\x01\xf4\x00cdcv\x00\x01\x05\x04") tags = self.wrap_ilst(Atom.render(b"----", data)) v = tags[key][0] self.failUnlessEqual(v, value) self.failUnlessEqual(v.dataformat, AtomDataType.IMPLICIT) data = MP4Tags()._MP4Tags__render_freeform(key, v) v = self.wrap_ilst(data)[key][0] self.failUnlessEqual(v.dataformat, AtomDataType.IMPLICIT) data = MP4Tags()._MP4Tags__render_freeform(key, value) v = self.wrap_ilst(data)[key][0] self.failUnlessEqual(v.dataformat, AtomDataType.UTF8) 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 tearDown(self): os.unlink(self.filename) class TMP4Mixin(object): def faad(self): if not have_faad: return self.assertEqual(call_faad("-w", self.filename), 0) def test_score(self): fileobj = open(self.filename, "rb") header = fileobj.read(128) self.failUnless(MP4.score(self.filename, fileobj, header)) fileobj.close() 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_kind(self): self.assertEqual(self.audio.info.codec, u'mp4a.40.2') 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 with open(self.audio.filename, "rb+") as fileobj: atoms = Atoms(fileobj) meta = atoms[b"moov", b"udta", b"meta"] meta_length1 = meta.length ilst = meta[b"ilst", ] free = meta[b"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) with open(self.audio.filename, "rb+") as fileobj: atoms = Atoms(fileobj) meta = atoms[b"moov", b"udta", b"meta"] ilst = meta[b"ilst", ] free = meta[b"free", ] self.failUnlessEqual(free.offset + free.length, ilst.offset) # Save the file self.audio["\xa9nam"] = u"wheeee" * 11 self.audio.save() # Check the order of "free" and "ilst" atoms with open(self.audio.filename, "rb+") as fileobj: atoms = Atoms(fileobj) meta = atoms[b"moov", b"udta", b"meta"] ilst = meta[b"ilst", ] free = meta[b"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): try: self.set_key('\xa9nam', [b'\xe3\x82\x8a\xe3\x81\x8b'], result=[u'\u308a\u304b']) except MP4MetadataValueError: if not PY3: raise def test_preserve_freeform(self): self.set_key('----:net.sacredchao.Mutagen:test key', [MP4FreeForm(b'woooo', 142, 42)]) def test_invalid_text(self): self.assertRaises( MP4MetadataValueError, self.set_key, '\xa9nam', [b'\xff']) 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', [b"whee"]) def test_freeform_2(self): self.set_key( '----:net.sacredchao.Mutagen:test key', b"whee", [b"whee"]) def test_freeforms(self): self.set_key( '----:net.sacredchao.Mutagen:test key', [b"whee", b"uhh"]) def test_freeform_bin(self): self.set_key('----:net.sacredchao.Mutagen:test key', [ MP4FreeForm(b'woooo', AtomDataType.UTF8), MP4FreeForm(b'hoooo', AtomDataType.IMPLICIT), MP4FreeForm(b'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', [b'woooo']) def test_cover_png(self): self.set_key('covr', [ MP4Cover(b'woooo', MP4Cover.FORMAT_PNG), MP4Cover(b'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()) self.assertTrue(isinstance(self.audio.pprint(), text_type)) def test_pprint_binary(self): self.audio["covr"] = [b"\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[b'moov'] samples = [] for atom in moov.findall(b'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(b'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[b"moof"].findall(b'tfhd', True): data = fileobj.read(atom.length - 9) flags = cdata.uint_be(b"\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 izip(aa, bb): self.failUnlessEqual(a, b) def test_mime(self): self.failUnless("audio/mp4" in self.audio.mime) def test_set_init_padding_zero(self): if self.audio.tags is None: self.audio.add_tags() self.audio.save(padding=lambda x: 0) self.assertEqual(MP4(self.audio.filename)._padding, 0) def test_set_init_padding_large(self): if self.audio.tags is None: self.audio.add_tags() self.audio.save(padding=lambda x: 5000) self.assertEqual(MP4(self.audio.filename)._padding, 5000) def test_set_various_padding(self): if self.audio.tags is None: self.audio.add_tags() for i in [0, 1, 2, 3, 1024, 983, 5000, 0, 1]: self.audio.save(padding=lambda x: i) self.assertEqual(MP4(self.audio.filename)._padding, i) self.faad() class TMP4HasTagsMixin(TMP4Mixin): def test_save_simple(self): self.audio.save() self.faad() def test_shrink(self): self.audio.clear() 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[b"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(DATA_DIR, "empty.ogg")) def test_delete_remove_padding(self): self.audio.clear() self.audio.tags['foob'] = u"foo" self.audio.save(padding=lambda x: 0) filesize = os.path.getsize(self.audio.filename) self.audio.delete() self.assertTrue(os.path.getsize(self.audio.filename) < filesize) class TMP4Datatypes(TMP4, TMP4HasTagsMixin): original = os.path.join(DATA_DIR, "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, AtomDataType.UTF8) self.failUnlessEqual(ff[0].version, 0) 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) def test_pprint(self): text = self.audio.tags.pprint().splitlines() self.assertTrue(u"©ART=Test Artist" in text) def test_get_padding(self): self.assertEqual(self.audio._padding, 1634) class TMP4CovrWithName(TMP4, TMP4Mixin): # http://bugs.musicbrainz.org/ticket/5894 original = os.path.join(DATA_DIR, "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) class TMP4HasTags64Bit(TMP4, TMP4HasTagsMixin): original = os.path.join(DATA_DIR, "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 class TMP4NoTagsM4A(TMP4, TMP4Mixin): original = os.path.join(DATA_DIR, "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) class TMP4NoTags3G2(TMP4, TMP4Mixin): original = os.path.join(DATA_DIR, "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) class TMP4UpdateParents64Bit(TestCase): original = os.path.join(DATA_DIR, "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): with open(self.filename, "rb") as fileobj: atoms = Atoms(fileobj) self.assertEqual(77, atoms.atoms[0].length) self.assertEqual(61, atoms.atoms[0].children[0].length) tags = MP4Tags(atoms, fileobj) tags['pgap'] = True tags.save(self.filename, padding=lambda x: 0) with open(self.filename, "rb") as fileobj: atoms = Atoms(fileobj) # original size + 'pgap' size + padding self.assertEqual(77 + 25 + 8, atoms.atoms[0].length) self.assertEqual(61 + 25 + 8, atoms.atoms[0].children[0].length) def tearDown(self): os.unlink(self.filename) class TMP4ALAC(TestCase): original = os.path.join(DATA_DIR, "alac.m4a") def setUp(self): self.audio = MP4(self.original) 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_length(self): self.failUnlessAlmostEqual(3.7, self.audio.info.length, 1) def test_bitrate(self): self.assertEqual(self.audio.info.bitrate, 2764) def test_kind(self): self.assertEqual(self.audio.info.codec, u'alac') class TMP4Misc(TestCase): def test_parse_full_atom(self): p = parse_full_atom(b"\x01\x02\x03\x04\xff") self.assertEqual(p, (1, 131844, b'\xff')) self.assertRaises(ValueError, parse_full_atom, b"\x00\x00\x00") def test_sort_items(self): items = [ ("\xa9nam", ["foo"]), ("gnre", ["fo"]), ("----", ["123"]), ("----", ["1234"]), ] sorted_items = sorted(items, key=MP4Tags._key_sort) self.assertEqual(sorted_items, items) class TMP4Freeform(TestCase): def test_cmp(self): self.assertReallyEqual( MP4FreeForm(b'woooo', 142, 42), MP4FreeForm(b'woooo', 142, 42)) self.assertReallyNotEqual( MP4FreeForm(b'woooo', 142, 43), MP4FreeForm(b'woooo', 142, 42)) self.assertReallyNotEqual( MP4FreeForm(b'woooo', 143, 42), MP4FreeForm(b'woooo', 142, 42)) self.assertReallyNotEqual( MP4FreeForm(b'wooox', 142, 42), MP4FreeForm(b'woooo', 142, 42)) def test_cmp_bytes(self): self.assertReallyEqual(MP4FreeForm(b'woooo'), b"woooo") self.assertReallyNotEqual(MP4FreeForm(b'woooo'), b"foo") if PY2: self.assertReallyEqual(MP4FreeForm(b'woooo'), u"woooo") self.assertReallyNotEqual(MP4FreeForm(b'woooo'), u"foo") class TMP4Cover(TestCase): def test_cmp(self): self.assertReallyEqual( MP4Cover(b'woooo', 142), MP4Cover(b'woooo', 142)) self.assertReallyNotEqual( MP4Cover(b'woooo', 143), MP4Cover(b'woooo', 142)) self.assertReallyNotEqual( MP4Cover(b'woooo', 142), MP4Cover(b'wooox', 142)) def test_cmp_bytes(self): self.assertReallyEqual(MP4Cover(b'woooo'), b"woooo") self.assertReallyNotEqual(MP4Cover(b'woooo'), b"foo") if PY2: self.assertReallyEqual(MP4Cover(b'woooo'), u"woooo") self.assertReallyNotEqual(MP4Cover(b'woooo'), u"foo") class TMP4AudioSampleEntry(TestCase): def test_alac(self): # an exampe where the channel count in the alac cookie is right # but the SampleEntry is wrong atom_data = ( b'\x00\x00\x00Halac\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x02\x00\x10\x00\x00\x00\x00\x1f@\x00' b'\x00\x00\x00\x00$alac\x00\x00\x00\x00\x00\x00\x10\x00\x00\x10' b'(\n\x0e\x01\x00\xff\x00\x00P\x01\x00\x00\x00\x00\x00\x00\x1f@') fileobj = cBytesIO(atom_data) atom = Atom(fileobj) entry = AudioSampleEntry(atom, fileobj) self.assertEqual(entry.bitrate, 0) self.assertEqual(entry.channels, 1) self.assertEqual(entry.codec, "alac") self.assertEqual(entry.codec_description, "ALAC") self.assertEqual(entry.sample_rate, 8000) def test_alac_2(self): # an example where the samplerate is only correct in the cookie, # also contains a bitrate atom_data = ( b'\x00\x00\x00Halac\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x02\x00\x18\x00\x00\x00\x00X\x88\x00' b'\x00\x00\x00\x00$alac\x00\x00\x00\x00\x00\x00\x10\x00\x00\x18' b'(\n\x0e\x02\x00\xff\x00\x00F/\x00%2\xd5\x00\x01X\x88') fileobj = cBytesIO(atom_data) atom = Atom(fileobj) entry = AudioSampleEntry(atom, fileobj) self.assertEqual(entry.bitrate, 2437845) self.assertEqual(entry.channels, 2) self.assertEqual(entry.codec, "alac") self.assertEqual(entry.codec_description, "ALAC") self.assertEqual(entry.sample_rate, 88200) def test_pce(self): atom_data = ( b'\x00\x00\x00dmp4a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x02\x00\x10\x00\x00\x00\x00\xbb\x80' b'\x00\x00\x00\x00\x00@esds\x00\x00\x00\x00\x03\x80\x80\x80/\x00' b'\x00\x00\x04\x80\x80\x80!@\x15\x00\x15\x00\x00\x03\xed\xaa\x00' b'\x03k\x00\x05\x80\x80\x80\x0f+\x01\x88\x02\xc4\x04\x90,\x10\x8c' b'\x80\x00\x00\xed@\x06\x80\x80\x80\x01\x02') fileobj = cBytesIO(atom_data) atom = Atom(fileobj) entry = AudioSampleEntry(atom, fileobj) self.assertEqual(entry.bitrate, 224000) self.assertEqual(entry.channels, 8) self.assertEqual(entry.codec_description, "AAC LC+SBR") self.assertEqual(entry.codec, "mp4a.40.2") self.assertEqual(entry.sample_rate, 48000) self.assertEqual(entry.sample_size, 16) def test_sbr_ps_sig_1(self): atom_data = ( b"\x00\x00\x00\\mp4a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x02\x00\x10\x00\x00\x00\x00\xbb\x80\x00" b"\x00\x00\x00\x008esds\x00\x00\x00\x00\x03\x80\x80\x80'\x00\x00" b"\x00\x04\x80\x80\x80\x19@\x15\x00\x03\x00\x00\x00\xe9j\x00\x00" b"\xda\xc0\x05\x80\x80\x80\x07\x13\x08V\xe5\x9dH\x80\x06\x80\x80" b"\x80\x01\x02") fileobj = cBytesIO(atom_data) atom = Atom(fileobj) entry = AudioSampleEntry(atom, fileobj) self.assertEqual(entry.bitrate, 56000) self.assertEqual(entry.channels, 2) self.assertEqual(entry.codec_description, "AAC LC+SBR+PS") self.assertEqual(entry.codec, "mp4a.40.2") self.assertEqual(entry.sample_rate, 48000) self.assertEqual(entry.sample_size, 16) self.assertTrue(isinstance(entry.codec, text_type)) self.assertTrue(isinstance(entry.codec_description, text_type)) def test_als(self): atom_data = ( b'\x00\x00\x00\x9dmp4a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00' b'\x00\x00\x00\x00\x00\x00\x02\x00\x00\x10\x00\x00\x00\x00\x07' b'\xd0\x00\x00\x00\x00\x00yesds\x00\x00\x00\x00\x03k\x00\x00\x00' b'\x04c@\x15\x10\xe7\xe6\x00W\xcbJ\x00W\xcbJ\x05T\xf8\x9e\x00\x0f' b'\xa0\x00ALS\x00\x00\x00\x07\xd0\x00\x00\x0c\t\x01\xff$O\xff\x00' b'g\xff\xfc\x80\x00\x00\x00,\x00\x00\x00\x00RIFF$$0\x00WAVEfmt ' b'\x10\x00\x00\x00\x01\x00\x00\x02\xd0\x07\x00\x00\x00@\x1f\x00' b'\x00\x04\x10\x00data\x00$0\x00\xf6\xceF+\x06\x01\x02') fileobj = cBytesIO(atom_data) atom = Atom(fileobj) entry = AudioSampleEntry(atom, fileobj) self.assertEqual(entry.bitrate, 5753674) self.assertEqual(entry.channels, 512) self.assertEqual(entry.codec_description, "ALS") self.assertEqual(entry.codec, "mp4a.40.36") self.assertEqual(entry.sample_rate, 2000) self.assertEqual(entry.sample_size, 16) def test_ac3(self): atom_data = ( b'\x00\x00\x00/ac-3\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x02\x00\x10\x00\x00\x00\x00V"\x00\x00' b'\x00\x00\x00\x0bdac3R\t\x00') fileobj = cBytesIO(atom_data) atom = Atom(fileobj) entry = AudioSampleEntry(atom, fileobj) self.assertEqual(entry.bitrate, 128000) self.assertEqual(entry.channels, 1) self.assertEqual(entry.codec_description, "AC-3") self.assertEqual(entry.codec, "ac-3") self.assertEqual(entry.sample_rate, 22050) self.assertEqual(entry.sample_size, 16) self.assertTrue(isinstance(entry.codec, text_type)) self.assertTrue(isinstance(entry.codec_description, text_type)) def test_samr(self): # parsing not implemented, values are wrong but at least it loads. # should be Mono 7.95kbps 8KHz atom_data = ( b'\x00\x00\x005samr\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x02\x00\x10\x00\x00\x00\x00\x1f@\x00' b'\x00\x00\x00\x00\x11damrFFMP\x00\x81\xff\x00\x01') fileobj = cBytesIO(atom_data) atom = Atom(fileobj) entry = AudioSampleEntry(atom, fileobj) self.assertEqual(entry.bitrate, 0) self.assertEqual(entry.channels, 2) self.assertEqual(entry.codec_description, "SAMR") self.assertEqual(entry.codec, "samr") self.assertEqual(entry.sample_rate, 8000) self.assertEqual(entry.sample_size, 16) self.assertTrue(isinstance(entry.codec, text_type)) self.assertTrue(isinstance(entry.codec_description, text_type)) def test_error(self): fileobj = cBytesIO(b"\x00" * 20) atom = Atom(fileobj) self.assertRaises(ASEntryError, AudioSampleEntry, atom, fileobj) def call_faad(*args): with open(os.devnull, 'wb') as null: return subprocess.call( ["faad"] + list(args), stdout=null, stderr=subprocess.STDOUT) have_faad = True try: call_faad() except OSError: have_faad = False print("WARNING: Skipping FAAD reference tests.") mutagen-1.31/tests/test_easyid3.py0000644000175000017500000003076312574061222017452 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os import shutil import pickle from tests import TestCase, DATA_DIR from mutagen.id3 import ID3FileType, ID3 from mutagen.easyid3 import EasyID3, error as ID3Error from mutagen._compat import PY3 from tempfile import mkstemp class TEasyID3(TestCase): def setUp(self): fd, self.filename = mkstemp('.mp3') os.close(fd) empty = os.path.join(DATA_DIR, 'emptyfile.mp3') shutil.copy(empty, self.filename) self.id3 = EasyID3() def test_remember_ctr(self): empty = os.path.join(DATA_DIR, '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_ignore_23(self): self.id3["date"] = "2004" self.id3.save(self.filename, v2_version=3) self.assertEqual(ID3(self.filename).version, (2, 4, 0)) 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_in(self): self.failIf("foo" in self.id3) if not PY3: def test_has_key(self): self.failIf(self.id3.has_key("foo")) def test_empty_file(self): empty = os.path.join(DATA_DIR, 'emptyfile.mp3') self.assertRaises(ID3Error, EasyID3, filename=empty) def test_nonexistent_file(self): empty = os.path.join(DATA_DIR, 'does', 'not', 'exist') self.assertRaises(IOError, EasyID3, filename=empty) def test_write_single(self): for key in EasyID3.valid_keys: if (key == "date") or (key == "originaldate"): 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") or (key == "originaldate"): 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) # some keys end up in multiple frames and ID3.getall returns # them in undefined order self.failUnlessEqual(sorted(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(sorted(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_original_date(self): self.id3["originaldate"] = "2004" self.id3.save(self.filename) id3 = EasyID3(self.filename) self.failUnlessEqual(id3["originaldate"], ["2004"]) self.id3["originaldate"] = "2004" self.id3.save(self.filename) id3 = EasyID3(self.filename) self.failUnlessEqual(id3["originaldate"], ["2004"]) def test_original_date_delete(self): self.id3["originaldate"] = "2004" self.failUnlessEqual(self.id3["originaldate"], ["2004"]) del(self.id3["originaldate"]) self.failIf("originaldate" in self.id3.keys()) def test_write_original_date_double(self): self.id3["originaldate"] = ["2004", "2005"] self.id3.save(self.filename) id3 = EasyID3(self.filename) self.failUnlessEqual(id3["originaldate"], ["2004", "2005"]) self.id3["originaldate"] = ["2004", "2005"] self.id3.save(self.filename) id3 = EasyID3(self.filename) self.failUnlessEqual(id3["originaldate"], ["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) mutagen-1.31/tests/__init__.py0000644000175000017500000001140412574061222016600 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- from __future__ import division, print_function import re import glob import os import sys import unittest from unittest import TestCase as BaseTestCase from mutagen._compat import PY3 from mutagen._toolsutil import fsencoding, is_fsnative DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data") if os.name == "nt" and not PY3: DATA_DIR = DATA_DIR.decode("ascii") assert is_fsnative(DATA_DIR) if os.name != "nt": try: u"öäü".encode(fsencoding()) except ValueError: raise RuntimeError("This test suite needs a unicode locale encoding. " "Try setting LANG=C.UTF-8") class TestCase(BaseTestCase): def failUnlessRaisesRegexp(self, exc, re_, fun, *args, **kwargs): def wrapped(*args, **kwargs): try: fun(*args, **kwargs) except Exception as 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) if not PY3: 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) if not PY3: self.assertNotEqual(0, cmp(a, b)) self.assertNotEqual(0, cmp(b, a)) def import_tests(): tests = [] for name in glob.glob( os.path.join(os.path.dirname(__file__), "test_*.py")): module_name = "tests." + os.path.basename(name) mod = __import__(module_name[:-3], {}, {}, []) mod = getattr(mod, os.path.basename(name)[:-3]) tests.extend(get_tests_from_mod(mod)) return list(set(tests)) def get_tests_from_mod(mod): tests = [] for name in dir(mod): obj = getattr(mod, name) if isinstance(obj, type) and issubclass(obj, BaseTestCase) and \ obj is not TestCase: tests.append(obj) return tests 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)), end="") result = Result() suite(result) result.printErrors() return bool(result.failures + result.errors) def check(): from tests.quality import test_pep8 from tests.quality import test_pyflakes tests = get_tests_from_mod(test_pep8) tests += get_tests_from_mod(test_pyflakes) runner = Runner() failures = 0 for test in sorted(tests, key=lambda c: c.__name__): failures += runner.run(test) return len(tests), failures def unit(run=[], exitfirst=False): tests = import_tests() runner = Runner() failures = 0 filtered = [t for t in tests if not run or t.__name__ in run] for test in sorted(filtered, key=lambda c: c.__name__): if failures and exitfirst: break failures += runner.run(test) return len(filtered), failures mutagen-1.31/tests/test_oggspeex.py0000644000175000017500000000430212574061222017720 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os import shutil from mutagen._compat import cBytesIO from mutagen.ogg import OggPage from mutagen.oggspeex import OggSpeex, OggSpeexInfo, delete from tests import TestCase, DATA_DIR from tests.test_ogg import TOggFileTypeMixin from tempfile import mkstemp class TOggSpeex(TestCase, TOggFileTypeMixin): Kind = OggSpeex def setUp(self): original = os.path.join(DATA_DIR, "empty.spx") fd, self.filename = mkstemp(suffix='.ogg') os.close(fd) shutil.copy(original, self.filename) self.audio = self.Kind(self.filename) def tearDown(self): os.unlink(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, cBytesIO(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(DATA_DIR, '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(DATA_DIR, "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) def test_init_padding(self): self.assertEqual(self.audio.tags._padding, 0) mutagen-1.31/tests/test_tools_mid3v2.py0000644000175000017500000001744112574061222020433 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os from tempfile import mkstemp import shutil import locale import mutagen from mutagen.id3 import ID3 from mutagen._compat import PY2, PY3 from mutagen._toolsutil import fsnative as fsn, is_fsnative as isfsn from tests.test_tools import _TTools from tests import DATA_DIR class TMid3v2(_TTools): TOOL_NAME = u"mid3v2" def setUp(self): super(TMid3v2, self).setUp() original = os.path.join(DATA_DIR, fsn(u'silence-44-s.mp3')) fd, self.filename = mkstemp(suffix=fsn(u'öäü.mp3')) assert isfsn(self.filename) os.close(fd) shutil.copy(original, self.filename) def tearDown(self): super(TMid3v2, self).tearDown() os.unlink(self.filename) def test_no_tags(self): f = ID3(self.filename) f.delete() res, out, err = self.call2(fsn(u"-l"), self.filename) self.assertTrue("No ID3 header found" in out) def test_list_genres(self): for arg in [fsn(u"-L"), fsn(u"--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 [fsn(u"-f"), fsn(u"--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 [fsn(u"-l"), fsn(u"--list")]: res, out = self.call(arg, self.filename) self.assertFalse("b'" in out) self.failUnlessEqual(res, 0) self.failUnless("TALB=" + fsn(album) in out) def test_list_raw(self): f = ID3(self.filename) res, out = self.call(fsn(u"--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 = fsn(u"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(fsn(u"-a"), fsn(u"--artist"), "TPE1") def test_album(self): self._test_text_frame(fsn(u"-A"), fsn(u"--album"), "TALB") def test_title(self): self._test_text_frame(fsn(u"-t"), fsn(u"--song"), "TIT2") def test_genre(self): self._test_text_frame(fsn(u"-g"), fsn(u"--genre"), "TCON") def test_convert(self): res, out = self.call(fsn(u"--convert"), self.filename) self.failUnlessEqual((res, out), (0, "")) def test_artist_escape(self): res, out = self.call( fsn(u"-e"), fsn(u"-a"), fsn(u"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( fsn(u"-e"), fsn(u"--TXXX"), fsn(u"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, u"EscapeTest::albumartist") self.failUnlessEqual(frame.text, [u"Ex:ample"]) def test_txxx(self): res, out = self.call(fsn(u"--TXXX"), fsn(u"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(fsn(u"--COMM"), fsn(u"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(fsn(u"--COMM"), fsn(u"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( fsn(u"-e"), fsn(u"--COMM"), fsn(u"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( fsn(u"--COMM"), fsn(u"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): is_bytes = PY2 and os.name != "nt" text = u'\xe4\xf6\xfc' if is_bytes: enc = locale.getpreferredencoding() # don't fail in case getpreferredencoding doesn't give us a unicode # encoding. text = text.encode(enc, "replace") res, out = self.call(fsn(u"-e"), fsn(u"-a"), text, self.filename) self.failUnlessEqual((res, out), (0, "")) f = ID3(self.filename) if is_bytes: text = text.decode(enc) self.assertEqual(f.getall("TPE1")[0], text) def test_invalid_encoding_escaped(self): res, out, err = self.call2( fsn(u"--TALB"), fsn(u'\\xff\\x81'), fsn(u'-e'), self.filename) self.failIfEqual(res, 0) self.failUnless("TALB" in err) def test_invalid_encoding(self): if os.name == "nt": return value = b"\xff\xff\x81" self.assertRaises(ValueError, value.decode, "utf-8") self.assertRaises(ValueError, value.decode, "cp1252") enc = locale.getpreferredencoding() # we need the decoding to fail for this test to work... try: value.decode(enc) except ValueError: pass else: return if not PY2: value = value.decode(enc, "surrogateescape") res, out, err = self.call2("--TALB", value, self.filename) self.failIfEqual(res, 0) self.failUnless("TALB" in err) def test_invalid_escape(self): res, out, err = self.call2( fsn(u"--TALB"), fsn(u'\\xaz'), fsn(u'-e'), self.filename) self.failIfEqual(res, 0) self.failUnless("TALB" in err) res, out, err = self.call2( fsn(u"--TALB"), fsn(u'\\'), fsn(u'-e'), self.filename) self.failIfEqual(res, 0) self.failUnless("TALB" in err) def test_value_from_fsnative(self): vffs = self.get_var("value_from_fsnative") self.assertEqual(vffs(fsn(u"öäü\\n"), True), u"öäü\n") self.assertEqual(vffs(fsn(u"öäü\\n"), False), u"öäü\\n") if os.name != "nt" and PY3: se = b"\xff".decode("utf-8", "surrogateescape") self.assertRaises(ValueError, vffs, se, False) def test_frame_from_fsnative(self): fffs = self.get_var("frame_from_fsnative") self.assertTrue(isinstance(fffs(fsn(u"abc")), str)) self.assertEqual(fffs(fsn(u"abc")), "abc") self.assertRaises(ValueError, fffs, fsn(u"öäü")) mutagen-1.31/tests/test_tools_mutagen_inspect.py0000644000175000017500000000120712574061222022505 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os import glob from tests.test_tools import _TTools from mutagen._toolsutil import fsnative as fsn class TMutagenInspect(_TTools): TOOL_NAME = u"mutagen-inspect" def test_basic(self): base = os.path.join(fsn(u'tests'), fsn(u'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) mutagen-1.31/tests/test_musepack.py0000644000175000017500000001115112574061222017707 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os import shutil from tempfile import mkstemp from mutagen.id3 import ID3, TIT2 from mutagen.musepack import Musepack, MusepackInfo, MusepackHeaderError from mutagen._compat import cBytesIO from tests import TestCase, DATA_DIR class TMusepack(TestCase): def setUp(self): self.sv8 = Musepack(os.path.join(DATA_DIR, "sv8_header.mpc")) self.sv7 = Musepack(os.path.join(DATA_DIR, "click.mpc")) self.sv5 = Musepack(os.path.join(DATA_DIR, "sv5_header.mpc")) self.sv4 = Musepack(os.path.join(DATA_DIR, "sv4_header.mpc")) def test_bad_header(self): self.failUnlessRaises( MusepackHeaderError, Musepack, os.path.join(DATA_DIR, "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(DATA_DIR, "empty.ogg")) self.failUnlessRaises( MusepackHeaderError, Musepack, os.path.join(DATA_DIR, "emptyfile.mp3")) def test_almost_my_file(self): self.failUnlessRaises( MusepackHeaderError, MusepackInfo, cBytesIO(b"MP+" + b"\x00" * 32)) self.failUnlessRaises( MusepackHeaderError, MusepackInfo, cBytesIO(b"MP+" + b"\x00" * 100)) self.failUnlessRaises( MusepackHeaderError, MusepackInfo, cBytesIO(b"MPCK" + b"\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) def test_zero_padded_sh_packet(self): # https://bitbucket.org/lazka/mutagen/issue/198 data = (b"MPCKSH\x10\x95 Q\xa2\x08\x81\xb8\xc9T\x00\x1e\x1b" b"\x00RG\x0c\x01A\xcdY\x06?\x80Z\x06EI") fileobj = cBytesIO(data) info = MusepackInfo(fileobj) self.assertEqual(info.channels, 2) self.assertEqual(info.samples, 3024084) class TMusepackWithID3(TestCase): SAMPLE = os.path.join(DATA_DIR, "click.mpc") def setUp(self): fd, self.NEW = mkstemp(suffix='mpc') os.close(fd) shutil.copy(self.SAMPLE, self.NEW) with open(self.SAMPLE, "rb") as h1: with open(self.NEW, "rb") as h2: self.failUnlessEqual(h1.read(), h2.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') mutagen-1.31/tests/test__id3specs.py0000644000175000017500000002720012574061222017755 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import sys from tests import TestCase from mutagen._compat import PY2, PY3, text_type from mutagen.id3 import BitPaddedInt, BitPaddedLong, unsynch from mutagen.id3._specs import SpecError class SpecSanityChecks(TestCase): def test_aspiindexspec(self): from mutagen.id3 import ASPIIndexSpec from mutagen.id3 import ASPI frame = ASPI(b=16, N=2) s = ASPIIndexSpec('name') self.assertRaises(SpecError, s.read, frame, b'') self.assertEqual(s.read(frame, b'\x01\x00\x00\x01'), ([256, 1], b"")) frame = ASPI(b=42) self.assertRaises(SpecError, s.read, frame, b'') def test_bytespec(self): from mutagen.id3 import ByteSpec s = ByteSpec('name') self.assertEquals((97, b'bcdefg'), s.read(None, b'abcdefg')) self.assertEquals(b'a', s.write(None, 97)) self.assertRaises(TypeError, s.write, None, b'abc') self.assertRaises(TypeError, s.write, None, None) def test_encodingspec(self): from mutagen.id3 import EncodingSpec s = EncodingSpec('name') self.assertEquals((3, b'abcdefg'), s.read(None, b'\x03abcdefg')) self.assertRaises(SpecError, s.read, None, b'\x04abcdefg') self.assertEquals(b'\x00', s.write(None, 0)) self.assertRaises(TypeError, s.write, None, b'abc') self.assertRaises(TypeError, s.write, None, None) def test_stringspec(self): from mutagen.id3 import StringSpec s = StringSpec('name', 3) self.assertEquals(('abc', b'defg'), s.read(None, b'abcdefg')) self.assertEquals(b'abc', s.write(None, 'abcdefg')) self.assertEquals(b'\x00\x00\x00', s.write(None, None)) self.assertEquals(b'\x00\x00\x00', s.write(None, '\x00')) self.assertEquals(b'a\x00\x00', s.write(None, 'a')) self.assertRaises(SpecError, s.read, None, b'\xff') def test_binarydataspec(self): from mutagen.id3 import BinaryDataSpec s = BinaryDataSpec('name') self.assertEquals((b'abcdefg', b''), s.read(None, b'abcdefg')) self.assertEquals(b'', s.write(None, None)) self.assertEquals(b'43', s.write(None, 43)) self.assertEquals(b'abc', s.write(None, b'abc')) def test_encodedtextspec(self): from mutagen.id3 import EncodedTextSpec, Frame s = EncodedTextSpec('name') f = Frame() f.encoding = 0 self.assertEquals((u'abcd', b'fg'), s.read(f, b'abcd\x00fg')) self.assertEquals(b'abcdefg\x00', s.write(f, u'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'), b'fg'), s.read(f, b'ab\x00fg')) self.assertEquals((ID3TimeStamp('1234'), b''), s.read(f, b'1234\x00')) self.assertEquals(b'1234\x00', s.write(f, ID3TimeStamp('1234'))) self.assertRaises(AttributeError, s.write, f, None) if PY3: self.assertRaises(TypeError, ID3TimeStamp, b"blah") self.assertEquals( text_type(ID3TimeStamp(u"2000-01-01")), u"2000-01-01") self.assertEquals( bytes(ID3TimeStamp(u"2000-01-01")), b"2000-01-01") def test_volumeadjustmentspec(self): from mutagen.id3 import VolumeAdjustmentSpec s = VolumeAdjustmentSpec('gain') self.assertEquals((0.0, b''), s.read(None, b'\x00\x00')) self.assertEquals((2.0, b''), s.read(None, b'\x04\x00')) self.assertEquals((-2.0, b''), s.read(None, b'\xfc\x00')) self.assertEquals(b'\x00\x00', s.write(None, 0.0)) self.assertEquals(b'\x04\x00', s.write(None, 2.0)) self.assertEquals(b'\xfc\x00', s.write(None, -2.0)) def test_synchronizedtextspec(self): from mutagen.id3 import SynchronizedTextSpec, Frame s = SynchronizedTextSpec('name') f = Frame() values = [(u"A", 100), (u"\xe4xy", 0), (u"", 42), (u"", 0)] # utf-16 f.encoding = 1 self.assertEqual(s.read(f, s.write(f, values)), (values, b"")) self.assertEquals( s.write(f, [(u"A", 100)]), b"\xff\xfeA\x00\x00\x00\x00\x00\x00d") # utf-16be f.encoding = 2 self.assertEqual(s.read(f, s.write(f, values)), (values, b"")) self.assertEquals( s.write(f, [(u"A", 100)]), b"\x00A\x00\x00\x00\x00\x00d") # utf-8 f.encoding = 3 self.assertEqual(s.read(f, s.write(f, values)), (values, b"")) self.assertEquals(s.write(f, [(u"A", 100)]), b"A\x00\x00\x00\x00d") 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) def test_stringspec(self): from mutagen.id3 import StringSpec s = StringSpec('byte', 3) self.assertEqual(s.validate(None, None), None) self.assertEqual(s.validate(None, "ABC"), "ABC") self.assertEqual(s.validate(None, u"ABC"), u"ABC") self.assertRaises(ValueError, s.validate, None, "abc2") self.assertRaises(ValueError, s.validate, None, "ab") if PY3: self.assertRaises(TypeError, s.validate, None, b"ABC") self.assertRaises(ValueError, s.validate, None, u"\xf6\xe4\xfc") def test_binarydataspec(self): from mutagen.id3 import BinaryDataSpec s = BinaryDataSpec('name') self.assertEqual(s.validate(None, None), None) self.assertEqual(s.validate(None, b"abc"), b"abc") if PY3: self.assertRaises(TypeError, s.validate, None, "abc") else: self.assertEqual(s.validate(None, u"abc"), b"abc") self.assertRaises(ValueError, s.validate, None, u"\xf6\xe4\xfc") class NoHashSpec(TestCase): def test_spec(self): from mutagen.id3 import Spec self.failUnlessRaises(TypeError, {}.__setitem__, Spec("foo"), None) class BitPaddedIntTest(TestCase): def test_long(self): if PY2: data = BitPaddedInt.to_str(sys.maxint + 1, width=16) val = BitPaddedInt(data) self.assertEqual(val, sys.maxint + 1) self.assertTrue(isinstance(val, BitPaddedLong)) else: self.assertTrue(BitPaddedInt is BitPaddedLong) def test_zero(self): self.assertEquals(BitPaddedInt(b'\x00\x00\x00\x00'), 0) def test_1(self): self.assertEquals(BitPaddedInt(b'\x00\x00\x00\x01'), 1) def test_1l(self): self.assertEquals( BitPaddedInt(b'\x01\x00\x00\x00', bigendian=False), 1) def test_129(self): self.assertEquals(BitPaddedInt(b'\x00\x00\x01\x01'), 0x81) def test_129b(self): self.assertEquals(BitPaddedInt(b'\x00\x00\x01\x81'), 0x81) def test_65(self): self.assertEquals(BitPaddedInt(b'\x00\x00\x01\x81', 6), 0x41) def test_32b(self): self.assertEquals(BitPaddedInt(b'\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(b'\xFF\xFF\xFF\xFF', bits=8).as_str(), b'\xFF\xFF\xFF\xFF') def test_s0(self): self.assertEquals(BitPaddedInt.to_str(0), b'\x00\x00\x00\x00') def test_s1(self): self.assertEquals(BitPaddedInt.to_str(1), b'\x00\x00\x00\x01') def test_s1l(self): self.assertEquals( BitPaddedInt.to_str(1, bigendian=False), b'\x01\x00\x00\x00') def test_s129(self): self.assertEquals(BitPaddedInt.to_str(129), b'\x00\x00\x01\x01') def test_s65(self): self.assertEquals(BitPaddedInt.to_str(0x41, 6), b'\x00\x00\x01\x01') def test_w129(self): self.assertEquals(BitPaddedInt.to_str(129, width=2), b'\x01\x01') def test_w129l(self): self.assertEquals( BitPaddedInt.to_str(129, width=2, bigendian=False), b'\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) if PY2: 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(b"\xff\xff", bits=8)) self.failIf(BitPaddedInt.has_valid_padding(b"\xff")) self.failIf(BitPaddedInt.has_valid_padding(b"\x00\xff")) self.failUnless(BitPaddedInt.has_valid_padding(b"\x7f\x7f")) self.failIf(BitPaddedInt.has_valid_padding(b"\x7f", bits=6)) self.failIf(BitPaddedInt.has_valid_padding(b"\x9f", bits=6)) self.failUnless(BitPaddedInt.has_valid_padding(b"\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)) class TestUnsynch(TestCase): def test_unsync_encode_decode(self): pairs = [ (b'', b''), (b'\x00', b'\x00'), (b'\x44', b'\x44'), (b'\x44\xff', b'\x44\xff\x00'), (b'\xe0', b'\xe0'), (b'\xe0\xe0', b'\xe0\xe0'), (b'\xe0\xff', b'\xe0\xff\x00'), (b'\xff', b'\xff\x00'), (b'\xff\x00', b'\xff\x00\x00'), (b'\xff\x00\x00', b'\xff\x00\x00\x00'), (b'\xff\x01', b'\xff\x01'), (b'\xff\x44', b'\xff\x44'), (b'\xff\xe0', b'\xff\x00\xe0'), (b'\xff\xe0\xff', b'\xff\x00\xe0\xff\x00'), (b'\xff\xf0\x0f\x00', b'\xff\x00\xf0\x0f\x00'), (b'\xff\xff', b'\xff\x00\xff\x00'), (b'\xff\xff\x01', b'\xff\x00\xff\x01'), (b'\xff\xff\xff\xff', b'\xff\x00\xff\x00\xff\x00\xff\x00'), ] for d, e in pairs: self.assertEqual(unsynch.encode(d), e) self.assertEqual(unsynch.decode(e), d) self.assertEqual(unsynch.decode(unsynch.encode(e)), e) self.assertEqual(unsynch.decode(e + e), d + d) def test_unsync_decode_invalid(self): self.assertRaises(ValueError, unsynch.decode, b'\xff\xff\xff\xff') self.assertRaises(ValueError, unsynch.decode, b'\xff\xf0\x0f\x00') self.assertRaises(ValueError, unsynch.decode, b'\xff\xe0') self.assertRaises(ValueError, unsynch.decode, b'\xff') mutagen-1.31/tests/test_tools.py0000644000175000017500000000350412574061222017242 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os import sys import imp import mutagen from mutagen._compat import StringIO, text_type, PY2 from mutagen._toolsutil import fsnative, is_fsnative from tests import TestCase def get_var(tool_name, entry="main"): tool_path = os.path.join( mutagen.__path__[0], "..", "tools", fsnative(tool_name)) dont_write_bytecode = sys.dont_write_bytecode sys.dont_write_bytecode = True try: mod = imp.load_source(tool_name, tool_path) finally: sys.dont_write_bytecode = dont_write_bytecode return getattr(mod, entry) class _TTools(TestCase): TOOL_NAME = None def setUp(self): self.assertTrue(isinstance(self.TOOL_NAME, text_type)) self._main = get_var(self.TOOL_NAME) def get_var(self, name): return get_var(self.TOOL_NAME, name) def call2(self, *args): for arg in args: self.assertTrue(is_fsnative(arg)) old_stdout = sys.stdout old_stderr = sys.stderr try: out = StringIO() err = StringIO() sys.stdout = out sys.stderr = err try: ret = self._main([fsnative(self.TOOL_NAME)] + list(args)) except SystemExit as e: ret = e.code ret = ret or 0 out_val = out.getvalue() err_val = err.getvalue() if os.name == "nt" and PY2: encoding = getattr(sys.stdout, "encoding", None) or "utf-8" out_val = text_type(out_val, encoding) err_val = text_type(err_val, encoding) return (ret, out_val, err_val) finally: sys.stdout = old_stdout sys.stderr = old_stderr def call(self, *args): return self.call2(*args)[:2] def tearDown(self): del self._main mutagen-1.31/tests/test__id3frames.py0000644000175000017500000004320012574061222020113 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- from tests import TestCase from mutagen.id3 import Frames, Frames_2_2, ID3, ID3Header from mutagen._compat import text_type, xrange _22 = ID3() _22._header = ID3Header() _22._header.version = (2, 2, 0) _23 = ID3() _23._header = ID3Header() _23._header.version = (2, 3, 0) _24 = ID3() _24._header = ID3Header() _24._header.version = (2, 4, 0) class FrameSanityChecks(TestCase): def test_CRA_upgrade(self): from mutagen.id3 import CRA, AENC frame = CRA(owner="a", preview_start=1, preview_length=2, data=b"foo") new = AENC(frame) self.assertEqual(new.owner, "a") self.assertEqual(new.preview_start, 1) self.assertEqual(new.preview_length, 2) self.assertEqual(new.data, b"foo") frame = CRA(owner="a", preview_start=1, preview_length=2) new = AENC(frame) self.assertFalse(hasattr(new, "data")) def test_PIC_upgrade(self): from mutagen.id3 import PIC, APIC frame = PIC(encoding=0, mime="PNG", desc="bla", type=3, data=b"\x00") new = APIC(frame) self.assertEqual(new.encoding, 0) self.assertEqual(new.mime, "PNG") self.assertEqual(new.desc, "bla") self.assertEqual(new.data, b"\x00") frame = PIC(encoding=0, mime="foo", desc="bla", type=3, data=b"\x00") self.assertEqual(frame.mime, "foo") new = APIC(frame) self.assertEqual(new.mime, "foo") def test_SIGN(self): from mutagen.id3 import SIGN frame = SIGN(group=1, sig=b"foo") self.assertEqual(frame.HashKey, "SIGN:1:foo") frame._pprint() def test_PRIV(self): from mutagen.id3 import PRIV frame = PRIV(owner="foo", data=b"foo") self.assertEqual(frame.HashKey, "PRIV:foo:foo") frame._pprint() frame = PRIV(owner="foo", data=b"\x00\xff") self.assertEqual(frame.HashKey, u"PRIV:foo:\x00\xff") frame._pprint() def test_GRID(self): from mutagen.id3 import GRID frame = GRID(owner="foo", group=42) self.assertEqual(frame.HashKey, "GRID:42") frame._pprint() def test_ENCR(self): from mutagen.id3 import ENCR frame = ENCR(owner="foo", method=42, data=b"\xff") self.assertEqual(frame.HashKey, "ENCR:foo") frame._pprint() def test_COMR(self): from mutagen.id3 import COMR frame = COMR( encoding=0, price="p", valid_until="v" * 8, contact="c", format=42, seller="s", desc="d", mime="m", logo=b"\xff") self.assertEqual( frame.HashKey, u"COMR:\x00p\x00vvvvvvvvc\x00*s\x00d\x00m\x00\xff") frame._pprint() def test_USER(self): from mutagen.id3 import USER frame = USER(encoding=0, lang="foo", text="bla") self.assertEqual(frame.HashKey, "USER:foo") frame._pprint() def test_UFID(self): from mutagen.id3 import UFID frame = UFID(owner="foo", data=b"\x42") self.assertEqual(frame.HashKey, "UFID:foo") frame._pprint() def test_LINK(self): from mutagen.id3 import LINK frame = LINK(frameid="TPE1", url="http://foo.bar", data=b"\x42") self.assertEqual(frame.HashKey, "LINK:TPE1:http://foo.bar:B") frame._pprint() frame = LINK(frameid="TPE1", url="http://foo.bar") self.assertEqual(frame.HashKey, "LINK:TPE1:http://foo.bar") def test_AENC(self): from mutagen.id3 import AENC frame = AENC( owner="foo", preview_start=1, preview_length=2, data=b"\x42") self.assertEqual(frame.HashKey, "AENC:foo") frame._pprint() def test_GEOB(self): from mutagen.id3 import GEOB frame = GEOB( encoding=0, mtime="m", filename="f", desc="d", data=b"\x42") self.assertEqual(frame.HashKey, "GEOB:d") frame._pprint() def test_POPM(self): from mutagen.id3 import POPM frame = POPM(email="e", rating=42) self.assertEqual(frame.HashKey, "POPM:e") frame._pprint() def test_APIC(self): from mutagen.id3 import APIC frame = APIC(encoding=0, mime="m", type=3, desc="d", data=b"\x42") self.assertEqual(frame.HashKey, "APIC:d") frame._pprint() def test_EQU2(self): from mutagen.id3 import EQU2 frame = EQU2(method=42, desc="d", adjustments=[(0, 0)]) self.assertEqual(frame.HashKey, "EQU2:d") frame._pprint() def test_RVA2(self): from mutagen.id3 import RVA2 frame = RVA2(method=42, desc="d", channel=1, gain=1, peak=1) self.assertEqual(frame.HashKey, "RVA2:d") frame._pprint() def test_COMM(self): from mutagen.id3 import COMM frame = COMM(encoding=0, lang="foo", desc="d") self.assertEqual(frame.HashKey, "COMM:d:foo") frame._pprint() def test_SYLT(self): from mutagen.id3 import SYLT frame = SYLT(encoding=0, lang="foo", format=1, type=2, desc="d", text=[("t", 0)]) self.assertEqual(frame.HashKey, "SYLT:d:foo") frame._pprint() def test_USLT(self): from mutagen.id3 import USLT frame = USLT(encoding=0, lang="foo", desc="d", text="t") self.assertEqual(frame.HashKey, "USLT:d:foo") frame._pprint() def test_WXXX(self): from mutagen.id3 import WXXX self.assert_(isinstance(WXXX(url='durl'), WXXX)) frame = WXXX(encoding=0, desc="d", url="u") self.assertEqual(frame.HashKey, "WXXX:d") frame._pprint() def test_TXXX(self): from mutagen.id3 import TXXX self.assert_(isinstance(TXXX(desc='d', text='text'), TXXX)) frame = TXXX(encoding=0, desc="d", text=[]) self.assertEqual(frame.HashKey, "TXXX:d") frame._pprint() def test_WCOM(self): from mutagen.id3 import WCOM frame = WCOM(url="u") self.assertEqual(frame.HashKey, "WCOM:u") frame._pprint() 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_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_22_uses_direct_ints(self): data = b'TT1\x00\x00\x83\x00' + (b'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(b'012345678', Frames))) self.assertEquals( [], list(_23._ID3__read_frames(b'012345678', Frames))) self.assertEquals( [], list(_22._ID3__read_frames(b'01234', Frames_2_2))) self.assertEquals( [], list(_22._ID3__read_frames(b'TT1' + b'\x00' * 3, Frames_2_2))) def test_unknown_22_frame(self): data = b'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._header, 0x9, b'\x00\x00\x00\x0f' b'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._header, 0x01, b'\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._header, 0x00, b'\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 = (b'\x00\x00\x00\x1fx\x9cc\xfc\xff\xaf\x84!\x83!\x93\xa1\x98A' b'\x01J&2\xe83\x940\xa4\x02\xd9%\x0c\x00\x87\xc6\x07#') tag = TPE1._fromData(_23._header, 0x80, data) self.assertEquals(tag.encoding, 1) self.assertEquals(tag, ['this is a/test']) tag = TPE1._fromData(_24._header, 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 [b'\xc2\xb5', b'\xe6\x97\xa5\xe6\x9c\xac']] artist = TPE1(encoding=3, text=artists) id3 = ID3() id3._header = ID3Header() id3._header.version = (2, 4, 0) 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=b"1").HashKey, APIC(data=b"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=b"1").HashKey, GEOB(data=b"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=b"1").HashKey, UFID(data=b"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) 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 xrange(len(self.GENRES)): self.assertEquals(self._g("%02d" % i), [self.GENRES[i]]) def test_parened_num(self): for i in xrange(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"]) 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) class NoHashFrame(TestCase): def test_frame(self): from mutagen.id3 import TIT1 self.failUnlessRaises( TypeError, {}.__setitem__, TIT1(encoding=0, text="foo"), None) 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")) 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, text_type(frame)) 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"]) class TRVA2(TestCase): def test_basic(self): from mutagen.id3 import RVA2 r = RVA2(gain=1, channel=1, peak=1) self.assertEqual(r, r) self.assertNotEqual(r, 42) mutagen-1.31/tests/test_apev2.py0000644000175000017500000003332212574061222017120 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # 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, DATA_DIR import mutagen.apev2 from mutagen._compat import PY3, text_type, izip, xrange from mutagen.apev2 import APEv2File, APEv2, is_valid_apev2_key, \ APEBadItemError, error as APEv2Error SAMPLE = os.path.join(DATA_DIR, "click.mpc") OLD = os.path.join(DATA_DIR, "oldtag.apev2") BROKEN = os.path.join(DATA_DIR, "brokentag.apev2") LYRICS2 = os.path.join(DATA_DIR, "apev2-lyricsv2.mp3") INVAL_ITEM_COUNT = os.path.join(DATA_DIR, "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)) if PY3: def test_py3(self): self.assertRaises(TypeError, is_valid_apev2_key, b"abc") 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) 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(b"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(DATA_DIR, "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") class TAPEv2ThenID3v1Writer(TAPEWriter): offset = 128 def setUp(self): super(TAPEv2ThenID3v1Writer, self).setUp() f = open(SAMPLE + ".new", "ab+") f.write(b"TAG" + b"\x00" * 125) f.close() f = open(BROKEN + ".new", "ab+") f.write(b"TAG" + b"\x00" * 125) f.close() f = open(SAMPLE + ".justtag", "ab+") f.write(b"TAG" + b"\x00" * 125) f.close() def test_tag_at_start_write(self): pass 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"] = b"\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(DATA_DIR, "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(DATA_DIR, "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(), list(izip(self.audio.keys(), self.audio.values()))) def test_key_type(self): key = self.audio.keys()[0] if PY3: self.assertTrue(isinstance(key, text_type)) else: self.assertTrue(isinstance(key, bytes)) 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) class TAPEv2ThenID3v1(TAPEv2): def setUp(self): super(TAPEv2ThenID3v1, self).setUp() f = open(self.filename, "ab+") f.write(b"TAG" + b"\x00" * 125) f.close() self.audio = APEv2(self.filename) 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") class TAPEBinaryValue(TestCase): from mutagen.apev2 import APEBinaryValue as BV BV = BV def setUp(self): self.sample = b"\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, bytes(self.value)) def test_repr(self): repr(self.value) def test_pprint(self): self.assertEqual(self.value.pprint(), "[3 bytes]") def test_type2(self): self.assertRaises(TypeError, mutagen.apev2.APEValue, u"abc", mutagen.apev2.BINARY) 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_parse(self): self.assertRaises(APEBadItemError, self.TV._new, b"\xff") def test_type(self): self.failUnless(isinstance(self.value, self.TV)) def test_construct(self): self.assertEqual(text_type(self.TV(u"foo")), u"foo") if not PY3: self.assertEqual(text_type(self.TV(b"foo")), u"foo") self.assertRaises(ValueError, self.TV, b"\xff") 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 xrange(len(self.value)): self.failUnlessEqual(self.sample[i], self.value[i]) def test_delitem(self): del self.sample[1] self.assertEqual(list(self.sample), ["foo", "baz"]) del self.sample[1:] self.assertEqual(list(self.sample), ["foo"]) def test_insert(self): self.sample.insert(0, "a") self.assertEqual(len(self.sample), 4) self.assertEqual(self.sample[0], "a") if PY3: self.assertRaises(TypeError, self.value.insert, 2, b"abc") def test_types(self): if PY3: self.assertRaises(TypeError, self.value.__setitem__, 2, b"abc") self.assertRaises( TypeError, mutagen.apev2.APEValue, b"abc", mutagen.apev2.TEXT) def test_repr(self): repr(self.value) def test_str(self): self.assertEqual(text_type(self.value), u"foo\x00bar\x00baz") def test_pprint(self): self.assertEqual(self.value.pprint(), "foo / bar / baz") 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, self.value) def test_repr(self): repr(self.value) if PY3: def test_py3(self): self.assertRaises( TypeError, mutagen.apev2.APEValue, b"abc", mutagen.apev2.EXTERNAL) def test_pprint(self): self.assertEqual(self.value.pprint(), "[External] http://foo") class TAPEv2File(TestCase): def setUp(self): self.audio = APEv2File(os.path.join(DATA_DIR, "click.mpc")) def test_empty(self): f = APEv2File(os.path.join(DATA_DIR, "xing.mp3")) self.assertFalse(f.items()) 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(APEv2Error, self.audio.add_tags) def test_unknown_info(self): info = self.audio.info info.pprint() mutagen-1.31/tests/test_ogg.py0000644000175000017500000006214312574061222016662 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os import random import shutil import subprocess from mutagen._compat import BytesIO, xrange from tests import TestCase, DATA_DIR from mutagen.ogg import OggPage, error as OggError from mutagen._util import cdata from mutagen import _util from tempfile import mkstemp class TOggPage(TestCase): def setUp(self): self.fileobj = open(os.path.join(DATA_DIR, "empty.ogg"), "rb") self.page = OggPage(self.fileobj) pages = [OggPage(), OggPage(), OggPage()] pages[0].packets = [b"foo"] pages[1].packets = [b"bar"] pages[2].packets = [b"baz"] for i in xrange(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(BytesIO(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 = [b"1" * 511, b"2" * 511, b"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 = [b"1" * 511, b"2" * 511, b"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 = [b"1" * 511, b"2" * 511, b"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_replace(self): # create interleaved pages fileobj = BytesIO() pages = [OggPage(), OggPage(), OggPage()] pages[0].serial = 42 pages[0].sequence = 0 pages[0].packets = [b"foo"] pages[1].serial = 24 pages[1].sequence = 0 pages[1].packets = [b"bar"] pages[2].serial = 42 pages[2].sequence = 1 pages[2].packets = [b"baz"] for page in pages: fileobj.write(page.write()) fileobj.seek(0, 0) pages_from_file = [OggPage(fileobj), OggPage(fileobj), OggPage(fileobj)] old_pages = [pages_from_file[0], pages_from_file[2]] packets = OggPage.to_packets(old_pages, strict=True) self.assertEqual(packets, [b"foo", b"baz"]) new_packets = [b"1111", b"2222"] new_pages = OggPage.from_packets(new_packets, sequence=old_pages[0].sequence) self.assertEqual(len(new_pages), 1) OggPage.replace(fileobj, old_pages, new_pages) fileobj.seek(0, 0) first = OggPage(fileobj) self.assertEqual(first.serial, 42) self.assertEqual(OggPage.to_packets([first], strict=True), [b"1111", b"2222"]) second = OggPage(fileobj) self.assertEqual(second.serial, 24) self.assertEqual(OggPage.to_packets([second], strict=True), [b"bar"]) def test_replace_fast_path(self): # create interleaved pages fileobj = BytesIO() pages = [OggPage(), OggPage(), OggPage()] pages[0].serial = 42 pages[0].sequence = 0 pages[0].packets = [b"foo"] pages[1].serial = 24 pages[1].sequence = 0 pages[1].packets = [b"bar"] pages[2].serial = 42 pages[2].sequence = 1 pages[2].packets = [b"baz"] for page in pages: fileobj.write(page.write()) fileobj.seek(0, 0) pages_from_file = [OggPage(fileobj), OggPage(fileobj), OggPage(fileobj)] old_pages = [pages_from_file[0], pages_from_file[2]] packets = OggPage.to_packets(old_pages, strict=True) self.assertEqual(packets, [b"foo", b"baz"]) new_packets = [b"111", b"222"] new_pages = OggPage._from_packets_try_preserve(new_packets, old_pages) self.assertEqual(len(new_pages), 2) # remove insert_bytes, so we can be sure the fast path was taken old_insert_bytes = _util.insert_bytes _util.insert_bytes = None try: OggPage.replace(fileobj, old_pages, new_pages) finally: _util.insert_bytes = old_insert_bytes # validate that the new data was written and the other pages # are untouched fileobj.seek(0, 0) pages_from_file = [OggPage(fileobj), OggPage(fileobj), OggPage(fileobj)] packets = OggPage.to_packets( [pages_from_file[0], pages_from_file[2]], strict=True) self.assertEqual(packets, [b"111", b"222"]) packets = OggPage.to_packets([pages_from_file[1]], strict=True) self.assertEqual(packets, [b"bar"]) def test_replace_continued(self): # take a partial packet and replace it with a new page # replace() should make it spanning again fileobj = BytesIO() pages = [OggPage(), OggPage()] pages[0].serial = 1 pages[0].sequence = 0 pages[0].complete = False pages[0].packets = [b"foo"] pages[1].serial = 1 pages[1].sequence = 1 pages[1].continued = True pages[1].packets = [b"bar"] fileobj = BytesIO() for page in pages: fileobj.write(page.write()) fileobj.seek(0, 0) pages_from_file = [OggPage(fileobj), OggPage(fileobj)] self.assertEqual(OggPage.to_packets(pages_from_file), [b"foobar"]) packets_part = OggPage.to_packets([pages_from_file[0]]) self.assertEqual(packets_part, [b"foo"]) new_pages = OggPage.from_packets([b"quuux"]) OggPage.replace(fileobj, [pages_from_file[0]], new_pages) fileobj.seek(0, 0) written = OggPage.to_packets([OggPage(fileobj), OggPage(fileobj)]) self.assertEquals(written, [b"quuuxbar"]) def test_renumber(self): self.failUnlessEqual( [page.sequence for page in self.pages], [0, 1, 2]) fileobj = BytesIO() 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 xrange(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 xrange(3)] self.failUnlessEqual([page.sequence for page in pages], [20, 21, 22]) def test_renumber_extradata(self): fileobj = BytesIO() for page in self.pages: fileobj.write(page.write()) fileobj.write(b"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 xrange(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(), b"left over data") def test_renumber_reread(self): try: fd, filename = mkstemp(suffix=".ogg") os.close(fd) shutil.copy(os.path.join(DATA_DIR, "multipagecomment.ogg"), filename) with open(filename, "rb+") as fileobj: OggPage.renumber(fileobj, 1002429366, 20) with open(filename, "rb+") as fileobj: OggPage.renumber(fileobj, 1002429366, 0) finally: try: os.unlink(filename) except OSError: pass def test_renumber_muxed(self): pages = [OggPage() for i in xrange(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 = BytesIO(b"".join([page.write() for page in pages])) OggPage.renumber(data, 0, 20) data.seek(0) pages = [OggPage(data) for i in xrange(10)] self.failUnlessEqual(pages[1].serial, 2) self.failUnlessEqual(pages[1].sequence, 100) pages.pop(1) self.failUnlessEqual( [page.sequence for page in pages], list(xrange(20, 29))) def test_to_packets(self): self.failUnlessEqual( [b"foo", b"bar", b"baz"], OggPage.to_packets(self.pages)) self.pages[0].complete = False self.pages[1].continued = True self.failUnlessEqual( [b"foobar", b"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), [b"foo", b"bar", b"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 = [b"1" * 200, b"2" * 200, b"3" * 200] pages = OggPage.from_packets(packets) self.failUnlessEqual(OggPage.to_packets(pages), packets) def test_from_packets_position(self): packets = [b"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 = [b"1" * 100000, b"2" * 100000, b"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__from_packets_try_preserve(self): # if the packet layout matches, just create pages with # the same layout and copy things over packets = [b"1" * 100000, b"2" * 100000, b"3" * 100000] pages = OggPage.from_packets(packets, sequence=42, default_size=977) new_pages = OggPage._from_packets_try_preserve(packets, pages) self.assertEqual(pages, new_pages) # zero case new_pages = OggPage._from_packets_try_preserve([], pages) self.assertEqual(new_pages, []) # if the layout doesn't match we should fall back to creating new # pages starting with the sequence of the first given page other_packets = list(packets) other_packets[1] += b"\xff" other_pages = OggPage.from_packets(other_packets, 42) new_pages = OggPage._from_packets_try_preserve(other_packets, pages) self.assertEqual(new_pages, other_pages) 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 xrange(10): num_packets = random.randrange(2, 100) lengths = [random.randrange(10, 10000) for i in xrange(num_packets)] packets = list(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 = [b"1" * 255] page.complete = False page2 = OggPage() page2.packets = [b""] page2.sequence = 1 page2.continued = True self.failUnlessEqual( [b"1" * 255], OggPage.to_packets([page, page2])) def test_page_max_size_alone_too_big(self): page = OggPage() page.packets = [b"1" * 255 * 255] page.complete = True self.failUnlessRaises(ValueError, page.write) def test_page_max_size(self): page = OggPage() page.packets = [b"1" * 255 * 255] page.complete = False page2 = OggPage() page2.packets = [b""] page2.sequence = 1 page2.continued = True self.failUnlessEqual( [b"1" * 255 * 255], OggPage.to_packets([page, page2])) def test_complete_zero_length(self): packets = [b""] * 20 page = OggPage.from_packets(packets)[0] new_page = OggPage(BytesIO(page.write())) self.failUnlessEqual(new_page, page) self.failUnlessEqual(OggPage.to_packets([new_page]), packets) def test_too_many_packets(self): packets = [b"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 = [b"1" * 255 * 255] page.complete = False page2 = OggPage() page2.packets = [b"", b"foo"] page2.sequence = 1 page2.continued = True data = page.write() + page2.write() fileobj = BytesIO(data) self.failUnlessEqual(OggPage(fileobj), page) self.failUnlessEqual(OggPage(fileobj), page2) self.failUnlessRaises(EOFError, OggPage, fileobj) def test_invalid_version(self): page = OggPage() OggPage(BytesIO(page.write())) page.version = 1 self.failUnlessRaises(OggError, OggPage, BytesIO(page.write())) def test_not_enough_lacing(self): data = OggPage().write()[:-1] + b"\x10" self.failUnlessRaises(OggError, OggPage, BytesIO(data)) def test_not_enough_data(self): data = OggPage().write()[:-1] + b"\x01\x10" self.failUnlessRaises(OggError, OggPage, BytesIO(data)) def test_not_equal(self): self.failIfEqual(OggPage(), 12) def test_find_last(self): pages = [OggPage() for i in xrange(10)] for i, page in enumerate(pages): page.sequence = i data = BytesIO(b"".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 xrange(10)] pages[-1].last = True for i, page in enumerate(pages): page.sequence = i data = BytesIO(b"".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 xrange(10)] for i, page in enumerate(pages): page.sequence = i pages[-2].last = True pages[-1].serial = pages[0].serial + 1 data = BytesIO(b"".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 xrange(10)] for i, page in enumerate(pages): page.sequence = i data = BytesIO(b"".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 = BytesIO(b"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 = BytesIO("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 = BytesIO(b"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 = [b"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() class TOggFileTypeMixin(object): PADDING_SUPPORT = True def scan_file(self): with open(self.filename, "rb") as fileobj: try: while True: OggPage(fileobj) except EOFError: pass 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_delete_remove_padding(self): if not self.PADDING_SUPPORT: return self.audio.clear() self.audio.save(padding=lambda x: 0) filesize = os.path.getsize(self.audio.filename) self.audio.delete() # deleting shouldn't add padding self.assertTrue(os.path.getsize(self.audio.filename) <= filesize) 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(DATA_DIR, 'xing.mp3')) def test_invalid_delete(self): self.failUnlessRaises(IOError, self.audio.delete, os.path.join(DATA_DIR, 'xing.mp3')) def test_invalid_save(self): self.failUnlessRaises(IOError, self.audio.save, os.path.join(DATA_DIR, 'xing.mp3')) def ogg_reference(self, filename): self.scan_file() if have_ogginfo: self.assertEqual(call_ogginfo(filename), 0, msg="ogginfo failed on %s" % filename) if have_oggz_validate: if filename.endswith(".opus") and not have_oggz_validate_opus: return self.assertEqual(call_oggz_validate(filename), 0, msg="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 test_padding(self): if not self.PADDING_SUPPORT: return self.audio.clear() self.audio["foo"] = ["bar"] for i in [0, 1, 2, 42, 5000, 4999]: self.audio.save(padding=lambda x: i) new = self.Kind(self.filename) self.assertEqual(new.tags._padding, i) self.assertEqual(new["foo"], ["bar"]) self.ogg_reference(self.filename) def call_ogginfo(*args): with open(os.devnull, 'wb') as null: return subprocess.call( ["ogginfo"] + list(args), stdout=null, stderr=subprocess.STDOUT) def call_oggz_validate(*args): with open(os.devnull, 'wb') as null: return subprocess.call( ["oggz-validate"] + list(args), stdout=null, stderr=subprocess.STDOUT) def get_oggz_validate_version(): """A version tuple or OSError if oggz-validate isn't available""" process = subprocess.Popen(["oggz-validate", "--version"], stdout=subprocess.PIPE) output, unused_err = process.communicate() retcode = process.poll() if retcode != 0: return (0,) lines = output.splitlines() if not lines: return (0,) parts = lines[0].split() if not parts: return (0,) try: return tuple(map(int, parts[-1].split(b"."))) except ValueError: return (0,) have_ogginfo = True try: call_ogginfo() except OSError: have_ogginfo = False print("WARNING: Skipping ogginfo reference tests.") have_oggz_validate = True have_oggz_validate_opus = True try: call_oggz_validate() except OSError: have_oggz_validate = False print("WARNING: Skipping oggz-validate reference tests.") else: if get_oggz_validate_version() <= (0, 9, 9): have_oggz_validate_opus = False print("WARNING: Skipping oggz-validate reference tests for opus") mutagen-1.31/tests/test_optimfrog.py0000644000175000017500000000223112574061222020104 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os from mutagen.optimfrog import OptimFROG, OptimFROGHeaderError from tests import TestCase, DATA_DIR class TOptimFROG(TestCase): def setUp(self): self.ofr = OptimFROG(os.path.join(DATA_DIR, "empty.ofr")) self.ofs = OptimFROG(os.path.join(DATA_DIR, "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(DATA_DIR, "empty.ogg")) self.failUnlessRaises( OptimFROGHeaderError, OptimFROG, os.path.join(DATA_DIR, "click.mpc")) def test_pprint(self): self.failUnless(self.ofr.pprint()) self.failUnless(self.ofs.pprint()) mutagen-1.31/tests/test_tools_mid3iconv.py0000644000175000017500000000570212574061222021217 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os from tempfile import mkstemp import shutil from mutagen.id3 import ID3 from mutagen._toolsutil import fsnative as fsn from mutagen._compat import text_type from tests.test_tools import _TTools from tests import DATA_DIR AMBIGUOUS = b"\xc3\xae\xc3\xa5\xc3\xb4\xc3\xb2 \xc3\xa0\xc3\xa9\xc3\xa7\xc3" \ b"\xa5\xc3\xa3 \xc3\xb9\xc3\xac \xc3\xab\xc3\xa5\xc3\xa5\xc3\xb8" \ b"\xc3\xba" CODECS = ["utf8", "latin-1", "Windows-1255", "gbk"] class TMid3Iconv(_TTools): TOOL_NAME = u"mid3iconv" def setUp(self): super(TMid3Iconv, self).setUp() original = os.path.join(DATA_DIR, fsn(u'silence-44-s.mp3')) fd, self.filename = mkstemp(suffix=fsn(u'.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(fsn(u"-d"), fsn(u"-p"), self.filename) self.failIf(res) self.assertFalse("b'" in out) self.failUnless("TCON=Silence" in out) def test_quiet(self): res, out = self.call(fsn(u"-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( fsn(u"-d"), fsn(u"-e"), fsn(text_type(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( fsn(u"-d"), fsn(u"-e"), fsn(text_type(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(fsn(u"--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)) mutagen-1.31/tests/test_encoding.py0000644000175000017500000000277012574061222017674 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2014 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 import os import re from tests import TestCase import mutagen class TSourceEncoding(TestCase): """Enforce utf-8 source encoding everywhere. Plus give helpful message for fixing it. """ def _check_encoding(self, path): with open(path, "r") as h: match = None for i, line in enumerate(h): # https://www.python.org/dev/peps/pep-0263/ match = match or re.search("coding[:=]\s*([-\w.]+)", line) if i >= 2: break if match: match = match.group(1) self.assertEqual(match, "utf-8", msg="%s has no utf-8 source encoding set\n" "Insert:\n# -*- coding: utf-8 -*-" % path) def test_main(self): root = os.path.dirname(mutagen.__path__[0]) skip = [os.path.join(root, "docs")] for dirpath, dirnames, filenames in os.walk(root): if any((dirpath.startswith(s + os.sep) or s == dirpath) for s in skip): continue for filename in filenames: if filename.endswith('.py'): path = os.path.join(dirpath, filename) self._check_encoding(path) mutagen-1.31/tests/test_mp3.py0000644000175000017500000003210212574061222016575 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os import shutil from tests import TestCase, DATA_DIR from mutagen._compat import cBytesIO, text_type from mutagen.mp3 import MP3, error as MP3Error, delete, MPEGInfo, EasyMP3, \ BitrateMode from mutagen._mp3util import XingHeader, XingHeaderError, VBRIHeader, \ VBRIHeaderError, LAMEHeader, LAMEError from mutagen.id3 import ID3 from tempfile import mkstemp class TMP3(TestCase): silence = os.path.join(DATA_DIR, 'silence-44-s.mp3') silence_nov2 = os.path.join(DATA_DIR, 'silence-44-s-v1.mp3') silence_mpeg2 = os.path.join(DATA_DIR, 'silence-44-s-mpeg2.mp3') silence_mpeg25 = os.path.join(DATA_DIR, 'silence-44-s-mpeg25.mp3') lame = os.path.join(DATA_DIR, 'lame.mp3') lame_peak = os.path.join(DATA_DIR, 'lame-peak.mp3') def setUp(self): original = os.path.join(DATA_DIR, "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) self.mp3_lame = MP3(self.lame) self.mp3_lame_peak = MP3(self.lame_peak) 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_replaygain(self): self.assertEqual(self.mp3_3.info.track_gain, 51.0) self.assertEqual(self.mp3_4.info.track_gain, 51.0) self.assertEqual(self.mp3_lame.info.track_gain, 6.0) self.assertAlmostEqual(self.mp3_lame_peak.info.track_gain, 6.8, 1) self.assertAlmostEqual(self.mp3_lame_peak.info.track_peak, 0.21856, 4) self.assertTrue(self.mp3.info.track_gain is None) self.assertTrue(self.mp3.info.track_peak is None) self.assertTrue(self.mp3.info.album_gain is None) def test_channels(self): self.assertEqual(self.mp3.info.channels, 2) self.assertEqual(self.mp3_2.info.channels, 2) self.assertEqual(self.mp3_3.info.channels, 2) self.assertEqual(self.mp3_4.info.channels, 2) def test_encoder_info(self): self.assertEqual(self.mp3.info.encoder_info, u"") self.assertTrue(isinstance(self.mp3.info.encoder_info, text_type)) self.assertEqual(self.mp3_2.info.encoder_info, u"") self.assertEqual(self.mp3_3.info.encoder_info, u"LAME 3.98.1+") self.assertEqual(self.mp3_4.info.encoder_info, u"LAME 3.98.1+") self.assertTrue(isinstance(self.mp3_4.info.encoder_info, text_type)) def test_bitrate_mode(self): self.failUnlessEqual(self.mp3.info.bitrate_mode, BitrateMode.UNKNOWN) self.failUnlessEqual(self.mp3_2.info.bitrate_mode, BitrateMode.UNKNOWN) self.failUnlessEqual(self.mp3_3.info.bitrate_mode, BitrateMode.VBR) self.failUnlessEqual(self.mp3_4.info.bitrate_mode, BitrateMode.VBR) 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.68475, 4) self.assertAlmostEquals(self.mp3_4.info.length, 3.68475, 4) 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, 18602) self.failUnlessEqual(self.mp3_4.info.bitrate, 9691) def test_notmp3(self): self.failUnlessRaises( MP3Error, MP3, os.path.join(DATA_DIR, 'empty.ofr')) 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(DATA_DIR, "silence-44-s.flac")) self.failUnless(notmp3.info.sketchy) def test_pprint(self): self.failUnless(self.mp3.pprint()) def test_info_pprint(self): res = self.mp3.info.pprint() self.assertTrue(res) self.assertTrue(isinstance(res, text_type)) self.assertTrue(res.startswith(u"MPEG 1 layer 3")) def test_pprint_no_tags(self): self.mp3.tags = None self.failUnless(self.mp3.pprint()) def test_xing(self): mp3 = MP3(os.path.join(DATA_DIR, "xing.mp3")) self.assertAlmostEqual(mp3.info.length, 2.052, 3) self.assertEqual(mp3.info.bitrate, 32000) def test_vbri(self): mp3 = MP3(os.path.join(DATA_DIR, "vbri.mp3")) self.assertAlmostEqual(mp3.info.length, 222.19755, 3) self.assertEqual(mp3.info.bitrate, 233260) def test_empty_xing(self): mp3 = MP3(os.path.join(DATA_DIR, "bad-xing.mp3")) self.assertEqual(mp3.info.length, 0) self.assertEqual(mp3.info.bitrate, 48000) 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_save_padding(self): self.mp3.save(padding=lambda x: 42) self.assertEqual(MP3(self.filename).tags._padding, 42) def test_load_non_id3(self): filename = os.path.join(DATA_DIR, "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(DATA_DIR, "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(DATA_DIR, "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.mp3.save() self.assertTrue(self.mp3.tags is None) def test_mime(self): self.failUnless("audio/mp3" in self.mp3.mime) # XXX self.mp3.info.layer = 2 self.failIf("audio/mp3" in self.mp3.mime) self.failUnless("audio/mp2" in self.mp3.mime) def tearDown(self): os.unlink(self.filename) class TMPEGInfo(TestCase): def test_not_real_file(self): filename = os.path.join(DATA_DIR, "silence-44-s-v1.mp3") fileobj = cBytesIO(open(filename, "rb").read(20)) MPEGInfo(fileobj) def test_empty(self): fileobj = cBytesIO(b"") self.failUnlessRaises(IOError, MPEGInfo, fileobj) class TEasyMP3(TestCase): def setUp(self): original = os.path.join(DATA_DIR, "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) class Issue72_TooShortFile(TestCase): def test_load(self): mp3 = MP3(os.path.join(DATA_DIR, 'too-short.mp3')) self.failUnlessEqual(mp3["TIT2"], "Track 10") self.failUnlessAlmostEqual(mp3.info.length, 0.03, 2) class TXingHeader(TestCase): def test_valid_info_header(self): data = (b'Info\x00\x00\x00\x0f\x00\x00:>\x00\xed\xbd8\x00\x03\x05\x07' b'\n\r\x0f\x12\x14\x17\x1a\x1c\x1e"$&)+.1359;=@CEGJLORTVZ\\^ac' b'fikmqsux{}\x80\x82\x84\x87\x8a\x8c\x8e\x92\x94\x96\x99\x9c' b'\x9e\xa1\xa3\xa5\xa9\xab\xad\xb0\xb3\xb5\xb8\xba\xbd\xc0\xc2' b'\xc4\xc6\xca\xcc\xce\xd1\xd4\xd6\xd9\xdb\xdd\xe1\xe3\xe5\xe8' b'\xeb\xed\xf0\xf2\xf5\xf8\xfa\xfc\x00\x00\x009') fileobj = cBytesIO(data) xing = XingHeader(fileobj) self.assertEqual(xing.bytes, 15580472) self.assertEqual(xing.frames, 14910) self.assertEqual(xing.vbr_scale, 57) self.assertTrue(xing.toc) self.assertEqual(len(xing.toc), 100) self.assertEqual(sum(xing.toc), 12625) # only for coverage.. self.assertEqual(xing.is_info, True) XingHeader(cBytesIO(data.replace(b'Info', b'Xing'))) def test_invalid(self): self.assertRaises(XingHeaderError, XingHeader, cBytesIO(b"")) self.assertRaises(XingHeaderError, XingHeader, cBytesIO(b"Xing")) self.assertRaises(XingHeaderError, XingHeader, cBytesIO(b"aaaa")) def test_get_offset(self): mp3 = MP3(os.path.join(DATA_DIR, "silence-44-s.mp3")) self.assertEqual(XingHeader.get_offset(mp3.info), 36) class TVBRIHeader(TestCase): def test_valid(self): # parts of the trailing toc zeroed... data = (b'VBRI\x00\x01\t1\x00d\x00\x0c\xb05\x00\x00\x049\x00\x87\x00' b'\x01\x00\x02\x00\x08\n0\x19H\x18\xe0\x18x\x18\xe0\x18x\x19H' b'\x18\xe0\x19H\x18\xe0\x18\xe0\x18x' + b'\x00' * 300) fileobj = cBytesIO(data) vbri = VBRIHeader(fileobj) self.assertEqual(vbri.bytes, 831541) self.assertEqual(vbri.frames, 1081) self.assertEqual(vbri.quality, 100) self.assertEqual(vbri.version, 1) self.assertEqual(vbri.toc_frames, 8) self.assertTrue(vbri.toc) self.assertEqual(len(vbri.toc), 135) self.assertEqual(sum(vbri.toc), 72656) def test_invalid(self): self.assertRaises(VBRIHeaderError, VBRIHeader, cBytesIO(b"")) self.assertRaises(VBRIHeaderError, VBRIHeader, cBytesIO(b"VBRI")) self.assertRaises(VBRIHeaderError, VBRIHeader, cBytesIO(b"Xing")) def test_get_offset(self): mp3 = MP3(os.path.join(DATA_DIR, "silence-44-s.mp3")) self.assertEqual(VBRIHeader.get_offset(mp3.info), 36) class TLAMEHeader(TestCase): def test_version(self): def parse(data): data = cBytesIO(data + b"\x00" * (20 - len(data))) return LAMEHeader.parse_version(data) self.assertEqual(parse(b"LAME3.80"), (u"3.80", False)) self.assertEqual(parse(b"LAME3.80 "), (u"3.80", False)) self.assertEqual(parse(b"LAME3.88 (beta)"), (u"3.88 (beta)", False)) self.assertEqual(parse(b"LAME3.90 (alpha)"), (u"3.90 (alpha)", False)) self.assertEqual(parse(b"LAME3.90 "), (u"3.90.0+", True)) self.assertEqual(parse(b"LAME3.96a"), (u"3.96 (alpha)", True)) self.assertEqual(parse(b"LAME3.96b"), (u"3.96 (beta)", True)) self.assertEqual(parse(b"LAME3.96x"), (u"3.96 (?)", True)) self.assertEqual(parse(b"LAME3.98 "), (u"3.98.0", True)) self.assertEqual(parse(b"LAME3.96r"), (u"3.96.1+", True)) self.assertEqual(parse(b"L3.99r"), (u"3.99.1+", True)) self.assertEqual(parse(b"LAME3100r"), (u"3.100.1+", True)) self.assertEqual(parse(b"LAME3.90.\x03\xbe\x00"), (u"3.90.0+", True)) def test_invalid(self): def parse(data): data = cBytesIO(data + b"\x00" * (20 - len(data))) return LAMEHeader.parse_version(data) self.assertRaises(LAMEError, parse, b"") self.assertRaises(LAMEError, parse, b"LAME") self.assertRaises(LAMEError, parse, b"LAME3.999") def test_real(self): with open(os.path.join(DATA_DIR, "lame.mp3"), "rb") as h: h.seek(36, 0) xing = XingHeader(h) self.assertEqual(xing.lame_version, u"3.99.1+") self.assertTrue(xing.lame_header) self.assertEqual(xing.lame_header.track_gain_adjustment, 6.0) def test_length(self): mp3 = MP3(os.path.join(DATA_DIR, "lame.mp3")) self.assertAlmostEqual(mp3.info.length, 0.06160, 4) mutagen-1.31/tests/test_m4a.py0000644000175000017500000000160312574061222016561 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os import warnings from tests import TestCase, DATA_DIR with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) from mutagen.m4a import (M4A, M4ATags, M4AInfo, delete, M4ACover, error) class TM4ADeprecation(TestCase): SOME_FILE = os.path.join(DATA_DIR, "no-tags.m4a") def test_fail(self): self.assertRaises(error, M4A, self.SOME_FILE) self.assertRaises(error, delete, self.SOME_FILE) self.assertRaises(error, delete, self.SOME_FILE) M4AInfo # pyflakes a = M4A() a.add_tags() self.assertEqual(a.tags.items(), []) some_cover = M4ACover(b"foo", M4ACover.FORMAT_JPEG) self.assertEqual(some_cover.imageformat, M4ACover.FORMAT_JPEG) tags = M4ATags() self.assertRaises(error, tags.save, self.SOME_FILE) mutagen-1.31/tests/test_oggtheora.py0000644000175000017500000000460712574061222020066 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os import shutil from tempfile import mkstemp from mutagen._compat import cBytesIO from mutagen.oggtheora import OggTheora, OggTheoraInfo, delete from mutagen.ogg import OggPage from tests import TestCase, DATA_DIR from tests.test_ogg import TOggFileTypeMixin class TOggTheora(TestCase, TOggFileTypeMixin): Kind = OggTheora def setUp(self): original = os.path.join(DATA_DIR, "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(DATA_DIR, "sample_length.oggtheora")) self.audio3 = OggTheora( os.path.join(DATA_DIR, "sample_bitrate.oggtheora")) def tearDown(self): os.unlink(self.filename) def test_theora_bad_version(self): page = OggPage(open(self.filename, "rb")) packet = page.packets[0] packet = packet[:7] + b"\x03\x00" + packet[9:] page.packets = [packet] fileobj = cBytesIO(page.write()) self.failUnlessRaises(IOError, OggTheoraInfo, fileobj) def test_theora_not_first_page(self): page = OggPage(open(self.filename, "rb")) page.first = False fileobj = cBytesIO(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(DATA_DIR, '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) def test_init_padding(self): self.assertEqual(self.audio.tags._padding, 0) mutagen-1.31/tests/test_easymp4.py0000644000175000017500000001157612574061222017474 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os import shutil from tests import TestCase, DATA_DIR 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(DATA_DIR, '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("foo" in self.mp4) def test_empty_file(self): empty = os.path.join(DATA_DIR, 'emptyfile.mp3') self.assertRaises(MP4Error, EasyMP4, filename=empty) def test_nonexistent_file(self): empty = os.path.join(DATA_DIR, '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) mutagen-1.31/tests/test_wavpack.py0000644000175000017500000000276212574061222017543 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os from mutagen.wavpack import WavPack from tests import TestCase, DATA_DIR class TWavPack(TestCase): def setUp(self): self.audio = WavPack(os.path.join(DATA_DIR, "silence-44-s.wv")) def test_version(self): self.failUnlessEqual(self.audio.info.version, 0x403) 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(DATA_DIR, "empty.ogg")) def test_pprint(self): self.audio.pprint() def test_mime(self): self.failUnless("audio/x-wavpack" in self.audio.mime) class TWavPackNoLength(TestCase): def setUp(self): self.audio = WavPack(os.path.join(DATA_DIR, "no_length.wv")) def test_version(self): self.failUnlessEqual(self.audio.info.version, 0x407) 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.705, 3) def test_pprint(self): self.audio.pprint() def test_mime(self): self.failUnless("audio/x-wavpack" in self.audio.mime) mutagen-1.31/tests/test_oggopus.py0000644000175000017500000000520112574061222017561 0ustar lazkalazka00000000000000# -*- coding: utf-8 -*- import os import shutil from tempfile import mkstemp from mutagen._compat import BytesIO from mutagen.oggopus import OggOpus, OggOpusInfo, delete from mutagen.ogg import OggPage from tests import TestCase, DATA_DIR from tests.test_ogg import TOggFileTypeMixin class TOggOpus(TestCase, TOggFileTypeMixin): Kind = OggOpus def setUp(self): original = os.path.join(DATA_DIR, "example.opus") fd, self.filename = mkstemp(suffix='.opus') os.close(fd) shutil.copy(original, self.filename) self.audio = self.Kind(self.filename) def tearDown(self): os.unlink(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, BytesIO(page.write())) def test_unsupported_version(self): page = OggPage(open(self.filename, "rb")) data = bytearray(page.packets[0]) data[8] = 0x03 page.packets[0] = bytes(data) OggOpusInfo(BytesIO(page.write())) data[8] = 0x10 page.packets[0] = bytes(data) self.failUnlessRaises(IOError, OggOpusInfo, BytesIO(page.write())) def test_preserve_non_padding(self): self.audio["FOO"] = ["BAR"] self.audio.save() extra_data = b"\xde\xad\xbe\xef" with open(self.filename, "r+b") as fobj: OggPage(fobj) # header page = OggPage(fobj) data = OggPage.to_packets([page])[0] data = data.rstrip(b"\x00") + b"\x01" + extra_data new_pages = OggPage.from_packets([data], page.sequence) OggPage.replace(fobj, [page], new_pages) OggOpus(self.filename).save() with open(self.filename, "rb") as fobj: OggPage(fobj) # header page = OggPage(fobj) data = OggPage.to_packets([page])[0] self.assertTrue(data.endswith(b"\x01" + extra_data)) self.assertEqual(OggOpus(self.filename).tags._padding, 0) def test_init_padding(self): self.assertEqual(self.audio.tags._padding, 196) mutagen-1.31/man/0000755000175000017500000000000012574256060014106 5ustar lazkalazka00000000000000mutagen-1.31/man/mid3v2.10000644000175000017500000001056512574061222015275 0ustar lazkalazka00000000000000.\" Man page generated from reStructuredText. . .TH MID3V2 1 "" "" "" .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 \fBmid3v2\fP is a Mutagen\-based replacement for id3lib\(aqs id3v2. It supports ID3v2.4 and more frames; it also does not have the numerous bugs that plague id3v2. .sp This program exists mostly for compatibility with programs that want to tag files using id3v2. For a more usable interface, we recommend Ex Falso. .SH OPTIONS .INDENT 0.0 .TP .B \-q\fP,\fB \-\-quiet Be quiet: do not mention file operations that perform the user\(aqs request. Warnings will still be printed. .TP .B \-v\fP,\fB \-\-verbose Be verbose: state all operations performed. This is the opposite of \-\-quiet. This is the default. .TP .B \-e\fP,\fB \-\-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\fP,\fB \-\-list\-frames Display all supported ID3v2.3/2.4 frames and their meanings. .TP .B \-L\fP,\fB \-\-list\-genres List all ID3v1 numeric genres. These can be used to set TCON frames, but it is not recommended. .TP .B \-l\fP,\fB \-\-list List all tags in the files. The output format is \fInot\fP the same as id3v2\(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\fP,\fB \-\-delete\-v2 Delete ID3v2 tags. .TP .B \-s\fP,\fB \-\-delete\-v1 Delete ID3v1 tags. .TP .B \-D\fP,\fB \-\-delete\-all Delete all ID3 tags. .TP .BI \-\-delete\-frames\fB= FRAMES Delete specific ID3v2 frames (or groups of frames) from the files. \fIFRAMES\fP is a "," separated list of frame names e.g. \fB"TPE1,TALB"\fP .TP .B \-C\fP,\fB \-\-convert Convert ID3v1 tags to ID3v2 tags. This will also happen automatically during any editing. .TP .BI \-a\fP,\fB \-\-artist\fB= ARTIST Set the artist information (TPE1). .TP .BI \-A\fP,\fB \-\-album\fB= ALBUM Set the album information (TALB). .TP .BI \-t\fP,\fB \-\-song\fB= TITLE Set the title information (TIT2). .TP .BI \-c\fP,\fB \-\-comment\fB= 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 .BI \-g\fP,\fB \-\-genre\fB= GENRE Set the genre information (TCON). .TP .BI \-y\fP,\fB \-\-year\fB= \fP,\fB \ \-\-date\fB= Set the year/date information (TDRC). .TP .BI \-T\fP,\fB \-\-track\fB= 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.31/man/mid3iconv.10000644000175000017500000000301712574061222016056 0ustar lazkalazka00000000000000.\" Man page generated from reStructuredText. . .TH MID3ICONV 1 "" "" "" .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\fP,\fB \-d Print updated tags .TP .B \-\-dry\-run\fP,\fB \-p Do not actually modify files .TP .B \-\-encoding\fP,\fB \-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\fP,\fB \-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.31/man/mutagen-pony.10000644000175000017500000000214012574061222016602 0ustar lazkalazka00000000000000.\" Man page generated from reStructuredText. . .TH MUTAGEN-PONY 1 "" "" "" .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.31/man/mid3cp.10000644000175000017500000000303012574061222015335 0ustar lazkalazka00000000000000.\" Man page generated from reStructuredText. . .TH MID3CP 1 "" "" "" .SH NAME mid3cp \- copy ID3 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 \fBmid3cp\fP [\fIoptions\fP] \fIsource\fP \fIdest\fP .SH DESCRIPTION .sp \fBmid3cp\fP copies the ID3 tags from a source file to a destination file. .sp It is designed to provide similar functionality to id3lib\(aqs id3cp tool, and can optionally write ID3v1 tags. It can also exclude specific tags from being copied. .SH OPTIONS .INDENT 0.0 .TP .B \-\-verbose\fP,\fB \-v Be verbose: state all operations performed, and list tags in source file. .TP .B \-\-write\-v1 Write ID3v1 tags to the destination file, derived from the ID3v2 tags. .TP .B \-\-exclude\-tag\fP,\fB \-x Exclude a specific tag from being copied. Can be specified multiple times. .UNINDENT .SH AUTHOR .sp Marcus Sundman. .sp Based on id3cp (part of id3lib) by Dirk Mahoney and Scott Thomas Haug. .\" Generated by docutils manpage writer. . mutagen-1.31/man/moggsplit.10000644000175000017500000000323412574061222016171 0ustar lazkalazka00000000000000.\" Man page generated from reStructuredText. . .TH MOGGSPLIT 1 "" "" "" .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.31/man/mutagen-inspect.10000644000175000017500000000215512574061222017270 0ustar lazkalazka00000000000000.\" Man page generated from reStructuredText. . .TH MUTAGEN-INSPECT 1 "" "" "" .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.31/setup.py0000755000175000017500000002071512574061222015047 0ustar lazkalazka00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # 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 import tarfile from distutils.core import setup, Command from distutils import dir_util from distutils.command.clean import clean as distutils_clean from distutils.command.sdist import 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 as 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 distcheck(sdist): def _check_manifest(self): assert self.get_archive_files() # 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 assert included_files process = subprocess.Popen(["hg", "locate"], stdout=subprocess.PIPE, universal_newlines=True) out, err = process.communicate() assert process.returncode == 0 tracked_files = out.splitlines() for ignore in [".hgignore", ".hgtags"]: tracked_files.remove(ignore) diff = set(tracked_files) - set(included_files) assert not diff, ( "Not all tracked files included in tarball, check MANIFEST.in", diff) def _check_dist(self): assert self.get_archive_files() distcheck_dir = os.path.join(self.dist_dir, "distcheck") if os.path.exists(distcheck_dir): dir_util.remove_tree(distcheck_dir) self.mkpath(distcheck_dir) archive = self.get_archive_files()[0] tfile = tarfile.open(archive, "r:gz") tfile.extractall(distcheck_dir) tfile.close() name = self.distribution.get_fullname() extract_dir = os.path.join(distcheck_dir, name) old_pwd = os.getcwd() os.chdir(extract_dir) self.spawn([sys.executable, "setup.py", "test"]) self.spawn([sys.executable, "setup.py", "build"]) self.spawn([sys.executable, "setup.py", "build_sphinx"]) self.spawn([sys.executable, "setup.py", "install", "--prefix", "../prefix", "--record", "../log.txt"]) os.chdir(old_pwd) def run(self): sdist.run(self) self._check_manifest() self._check_dist() 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)"), ("exitfirst", "x", "stop after first failing test"), ] def initialize_options(self): self.to_run = [] self.exitfirst = False def finalize_options(self): if self.to_run: self.to_run = self.to_run.split(",") self.exitfirst = bool(self.exitfirst) def run(self): import tests count, failures = tests.unit(self.to_run, self.exitfirst) if failures: print("%d out of %d failed" % (failures, count)) raise SystemExit("Test failures are listed above.") class quality_cmd(Command): description = "run pyflakes/pep8 tests" user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): import tests count, failures = tests.check() if failures: print("%d out of %d failed" % (failures, count)) raise SystemExit("Test failures are listed above.") class coverage_cmd(Command): description = "generate test coverage data" user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): try: from coverage import coverage except ImportError: raise SystemExit( "Missing 'coverage' module. See " "https://pypi.python.org/pypi/coverage or try " "`apt-get install python-coverage python3-coverage`") for key in list(sys.modules.keys()): if key.startswith('mutagen'): del(sys.modules[key]) cov = coverage() cov.start() cmd = self.reinitialize_command("test") cmd.ensure_finalized() cmd.run() dest = os.path.join(os.getcwd(), "coverage") cov.stop() cov.html_report( directory=dest, ignore_errors=True, include=["mutagen/*", "tools/*"]) print("Coverage summary: file://%s/index.html" % dest) 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, "quality": quality_cmd, "coverage": coverage_cmd, "distcheck": distcheck, "build_sphinx": build_sphinx, } setup(cmdclass=cmd_classes, name="mutagen", version=version_string, url="https://bitbucket.org/lazka/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 :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', '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", "mutagen.id3", "mutagen.mp4", "mutagen.asf"], data_files=data_files, scripts=[os.path.join("tools", name) for name in [ "mid3cp", "mid3iconv", "mid3v2", "moggsplit", "mutagen-inspect", "mutagen-pony", ]], 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.31/PKG-INFO0000644000175000017500000000265012574256060014433 0ustar lazkalazka00000000000000Metadata-Version: 1.1 Name: mutagen Version: 1.31 Summary: read and write audio tags for many formats Home-page: https://bitbucket.org/lazka/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 :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 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.31/MANIFEST.in0000644000175000017500000000051312574061222015062 0ustar lazkalazka00000000000000include COPYING include NEWS include README.rst include MANIFEST.in include tests/data/* include tests/quality/* include tests/*.py include man/*.1 include docs/Makefile include docs/*.py include docs/*.rst include docs/images/*.svg include docs/images/*.png include docs/api/*.rst include docs/man/*.rst include docs/man/Makefile mutagen-1.31/tools/0000755000175000017500000000000012574256060014473 5ustar lazkalazka00000000000000mutagen-1.31/tools/moggsplit0000755000175000017500000000466212574061222016430 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 import mutagen.ogg from mutagen._toolsutil import SignalHandler, get_win32_unicode_argv, \ OptionParser _sig = SignalHandler() 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: with _sig.block(): 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()) for f in fileobjs.values(): f.close() if __name__ == "__main__": argv = get_win32_unicode_argv() _sig.init() main(argv) mutagen-1.31/tools/mutagen-inspect0000755000175000017500000000233212574061222017516 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 mutagen._toolsutil import SignalHandler, get_win32_unicode_argv, print_, \ OptionParser from mutagen._compat import text_type 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) for filename in args: print_(u"--", filename) try: print_(u"-", File(filename).pprint()) except AttributeError: print_(u"- Unknown file type") except Exception as err: print_(text_type(err)) print_(u"") if __name__ == "__main__": argv = get_win32_unicode_argv() SignalHandler().init() main(argv) mutagen-1.31/tools/mid3iconv0000755000175000017500000001221512574061222016307 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 import mutagen import mutagen.id3 from mutagen._compat import PY3, text_type from mutagen._toolsutil import SignalHandler, get_win32_unicode_argv, print_, \ fsnative as fsn, OptionParser VERSION = (0, 3) _sig = SignalHandler() def getpreferredencoding(): return locale.getpreferredencoding() or "utf-8" def isascii(string): """Checks whether a unicode string is non-empty and contains only ASCII characters. """ if not string: return False try: string.encode('ascii') except UnicodeEncodeError: return False return True 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 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: with _sig.block(): if verbose != "quiet": print_(u"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_(u"No ID3 header found; skipping...") continue except Exception as err: print_(text_type(err), file=sys.stderr) 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 = [conv(x) for x in 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 if verbose == "debug": print_(id3.pprint()) if not noupdate: if remove_v1: id3.save(filename, v1=False) else: id3.save(filename) def has_id3v1(filename): try: with open(filename, 'rb+') as f: f.seek(-128, 2) return f.read(3) == b"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)" % ( 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(argv): if arg == "-v1": argv[i] = fsn(u"--force-v1") elif arg == "-removev1": argv[i] = fsn(u"--remove-v1") (options, args) = parser.parse_args(argv[1:]) if args: update(options, args) else: parser.print_help() if __name__ == "__main__": argv = get_win32_unicode_argv() _sig.init() main(argv) mutagen-1.31/tools/mutagen-pony0000755000175000017500000000635412574061222017046 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 from mutagen._toolsutil import SignalHandler, get_win32_unicode_argv, print_ 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 = list(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 = list(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.mp3 import MP3 rep = Report(path) print_(u"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) 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_(u"Usage:", argv[0], u"directory ...") else: for path in argv[1:]: check_dir(path) if __name__ == "__main__": argv = get_win32_unicode_argv() SignalHandler().init() main(argv) mutagen-1.31/tools/mid3v20000755000175000017500000003466212574061222015532 0ustar lazkalazka00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # 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 os import sys import locale import codecs from optparse import SUPPRESS_HELP import mutagen import mutagen.id3 from mutagen._toolsutil import split_escape, SignalHandler, OptionParser,\ get_win32_unicode_argv, fsnative, is_fsnative, fsencoding, print_ from mutagen._compat import PY2, PY3, text_type VERSION = (1, 3) _sig = SignalHandler() 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 list_frames(option, opt, value, parser): items = mutagen.id3.Frames.items() for name, frame in sorted(items): print_(u" --%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_(u" --%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_(u"%3d: %s" % (i, genre)) raise SystemExit def delete_tags(filenames, v1, v2): for filename in filenames: with _sig.block(): if verbose: print_(u"deleting ID3 tag info in", filename, file=sys.stderr) mutagen.id3.delete(filename, v1, v2) def delete_frames(deletes, filenames): try: deletes = frame_from_fsnative(deletes) except ValueError as err: print_(text_type(err), file=sys.stderr) frames = deletes.split(",") for filename in filenames: with _sig.block(): if verbose: print_(u"deleting %s from" % deletes, filename, file=sys.stderr) try: id3 = mutagen.id3.ID3(filename) except mutagen.id3.ID3NoHeaderError: if verbose: print_(u"No ID3 header found; skipping.", file=sys.stderr) except Exception as err: print_(text_type(err), file=sys.stderr) raise SystemExit(1) else: for frame in frames: id3.delall(frame) id3.save() def frame_from_fsnative(arg): """Takes item from argv and returns ascii native str or raises ValueError. """ assert is_fsnative(arg) if os.name == "nt": # unicode if PY2: return arg.encode("ascii") else: return arg.encode("ascii").decode("ascii") else: if PY2: # bytes return arg.decode(fsencoding()).encode("ascii") else: # unicode + surrogate return arg.encode("ascii", "surrogateescape").decode("ascii") def value_from_fsnative(arg, escape): """Takes an item from argv and returns a text_type value without surrogate escapes or raises ValueError. """ assert is_fsnative(arg) if os.name == "nt": if not escape: return arg if PY2: return arg.encode("utf-8").decode("string_escape").decode("utf-8") else: return codecs.escape_decode(arg.encode("utf-8"))[0].decode("utf-8") else: enc = fsencoding() if PY2: if escape: arg = arg.decode("string_escape") return arg.decode(enc) else: if not escape: # make sure no surrogateescapes arg.encode("utf-8") return arg return codecs.escape_decode( arg.encode(enc, "surrogateescape"))[0].decode(enc) def write_files(edits, filenames, escape): # unescape escape sequences and decode values encoded_edits = [] for frame, value in edits: if not value: continue try: frame = frame_from_fsnative(frame) except ValueError as err: print_(text_type(err), file=sys.stderr) assert isinstance(frame, str) # strip "--" frame = frame[2:] try: value = value_from_fsnative(value, escape) except ValueError as err: print_(u"%s: %s" % (frame, text_type(err)), file=sys.stderr) raise SystemExit(1) assert isinstance(value, text_type) 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: with _sig.block(): if verbose: print_(u"Writing", filename, file=sys.stderr) try: id3 = mutagen.id3.ID3(filename) except mutagen.id3.ID3NoHeaderError: if verbose: print_(u"No ID3 header found; creating a new tag", file=sys.stderr) id3 = mutagen.id3.ID3() except Exception as err: print_(str(err), file=sys.stderr) 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): for filename in filenames: print_("IDv2 tag info for", filename) try: id3 = mutagen.id3.ID3(filename, translate=False) except mutagen.id3.ID3NoHeaderError: print_(u"No ID3 header found; skipping.") except Exception as err: print_(text_type(err), file=sys.stderr) raise SystemExit(1) else: print_(id3.pprint()) def list_tags_raw(filenames): for filename in filenames: print_("Raw IDv2 tag info for", filename) try: id3 = mutagen.id3.ID3(filename, translate=False) except mutagen.id3.ID3NoHeaderError: print_(u"No ID3 header found; skipping.") except Exception as err: print_(text_type(err), file=sys.stderr) raise SystemExit(1) else: for frame in id3.values(): print_(text_type(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((fsnative(u"--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((fsnative(u"--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((fsnative(u"--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((fsnative(u"--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((fsnative(u"--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((fsnative(u"--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((fsnative(u"--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__": argv = get_win32_unicode_argv() _sig.init() main(argv) mutagen-1.31/tools/mid3cp0000755000175000017500000000727512574061222015605 0ustar lazkalazka00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright 2014 Marcus Sundman # 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. """A program replicating the functionality of id3lib's id3cp, using mutagen for tag loading and saving. """ import sys import os.path import mutagen import mutagen.id3 from mutagen._toolsutil import SignalHandler, get_win32_unicode_argv, print_, \ OptionParser from mutagen._compat import text_type VERSION = (0, 1) _sig = SignalHandler() def printerr(*args, **kwargs): kwargs.setdefault("file", sys.stderr) print_(*args, **kwargs) class ID3OptionParser(OptionParser): def __init__(self): mutagen_version = mutagen.version_string my_version = ".".join(map(str, VERSION)) version = "mid3cp %s\nUses Mutagen %s" % (my_version, mutagen_version) self.disable_interspersed_args() OptionParser.__init__( self, version=version, usage="%prog [option(s)] ", description=("Copies ID3 tags from to . Mutagen-based " "replacement for id3lib's id3cp.")) def copy(src, dst, write_v1=True, excluded_tags=None, verbose=False): """Returns 0 on success""" if excluded_tags is None: excluded_tags = [] try: id3 = mutagen.id3.ID3(src, translate=False) except mutagen.id3.ID3NoHeaderError: print_(u"No ID3 header found in ", src, file=sys.stderr) return 1 except StandardError as err: print_(str(err), file=sys.stderr) return 1 else: if verbose: print_(u"File", src, u"contains:", file=sys.stderr) print_(id3.pprint(), file=sys.stderr) for tag in excluded_tags: id3.delall(tag) # if the source is 2.3 save it as 2.3 if id3.version < (2, 4, 0): id3.update_to_v23() v2_version = 3 else: id3.update_to_v24() v2_version = 4 try: id3.save(dst, v1=(2 if write_v1 else 0), v2_version=v2_version) except StandardError as err: print_(u"Error saving", dst, u":\n%s" % text_type(err), file=sys.stderr) return 1 else: if verbose: print_(u"Successfully saved", dst, file=sys.stderr) return 0 def main(argv): parser = ID3OptionParser() parser.add_option("-v", "--verbose", action="store_true", dest="verbose", help="print out saved tags", default=False) parser.add_option("--write-v1", action="store_true", dest="write_v1", default=False, help="write id3v1 tags") parser.add_option("-x", "--exclude-tag", metavar="TAG", action="append", dest="x", help="exclude the specified tag", default=[]) (options, args) = parser.parse_args(argv[1:]) if len(args) != 2: parser.print_help(file=sys.stderr) return 1 (src, dst) = args if not os.path.isfile(src): print_(u"File not found:", src, file=sys.stderr) parser.print_help(file=sys.stderr) return 1 if not os.path.isfile(dst): printerr(u"File not found:", dst, file=sys.stderr) parser.print_help(file=sys.stderr) return 1 # Strip tags - "-x FOO" adds whitespace at the beginning of the tag name excluded_tags = [x.strip() for x in options.x] with _sig.block(): return copy(src, dst, options.write_v1, excluded_tags, options.verbose) if __name__ == "__main__": get_win32_unicode_argv() _sig.init() sys.exit(main(sys.argv)) mutagen-1.31/NEWS0000644000175000017500000004667212574255552014056 0ustar lazkalazka000000000000001.31 - 2015.09.10 ----------------- * New padding control API for flac/id3/mp4/asf/ogg/aiff and everything based on it, except oggflac :bug:`229` * Mutagen will now reduce padding on save if there is lots of it. * delete() will remove padding in addition to tags. * ASF: * Padding support :bug:`201` * Don't report negative lengths for some broken files * New :class:`asf.ASFInfo`.codec_type/codec_name/codec_description * Implement ASF.delete() * OGG: Padding support for Opus/Vorbis/Theora/Speex * M4A: Implementation removed. Every operation will raise. Use mp4 instead. * Tools: Support Unicode output under Windows :bug:`232` 1.30 - 2015.08.22 ----------------- * FLAC: * Fix :meth:`flac.FLAC.save` in case the source contained a too large (invalid but recovered) image block :bug:`226` * MP3: * Improved length and bitrate accuracy: * Read lame "Info" tags for improved bitrate/length accuracy * Use bytes info of VBRI headers for improved bitrate accuracy * Subtract encoder delay/padding from length for improved length accuracy (especially for short tracks) * Fix rare false identification of Xing headers :bug:`182` * New :class:`mp3.MPEGInfo`.encoder_info attribute containing the encoder name and version :bug:`66` * New :class:`mp3.MPEGInfo`.bitrate_mode attribute exposing if the file is VBR, ABR or CBR :bug:`24` :bug:`66` * New :class:`mp3.MPEGInfo`.channels attribute providing the channel count * New :class:`mp3.MPEGInfo`.track_gain/track_peak/album_gain values exposing the replaygain info provided by the lame header :bug:`36` * ID3: * New :class:`id3.PictureType` enum for the picture type used in APIC frames :bug:`222` * MP4: * Fix MP4FreeForm.__eq__ and MP4Cover.__eq__ when comparing with bytes :bug:`218` * Don't raise on :meth:`FileType.save` if there are no tags. :bug:`227` * Minor fixes: :bug:`228` 1.29 - 2015.05.09 ----------------- * mid3v2: Fix an error under Python 3 with files without tags :bug:`219` * mid3v2: Various Windows+Python2+Unicode fixes :bug:`214` * Don't emit warnings during loading (ID3Warning) :bug:`223` * py.test support 1.28 - 2015.03.06 ----------------- * Various minor fixes to make mutagen behave the same under Python3 as under Python2. * Update gpl text :bug:`205` * Documentation: Add example for how to create a new flac.Picture :bug:`209` * ID3: * Various error handling fixes (:bug:`110`, :bug:`211`, ...) * Don't hide ID3 loading errors with ID3FileType. * In case a synch safe marked frame isn't sync safe, only warn :bug:`210` * Removed PEDANTIC mode * Tools: * Add signal handling :bug:`170` * mid3cp: Make it work under Windows. * mutagen-inspect: Make it work under Windows+Python3 :bug:`216` * Support unicode file paths under Windows+Python2 :bug:`214` * Support file paths with invalid encoding under Unix+Python3. 1.27 - 2014.11.28 ----------------- * MP4: * New ``MP4Info.codec`` for identifying the contained audio codec e.g. ``"mp4a"``, ``"alac"``, ``"mp4a.40.2"``, ``"ac-3"`` etc. :pr:`6` * New ``MP4Info.codec_description``: name of the audio codec e.g. ``"ALAC"``, ``"AAC LC"``, ``"AC-3"`` * OggOpus: * Preserve data after vorbis comment ( See https://tools.ietf.org/html/draft-ietf-codec-oggopus-05#section-5.2) :bug:`202` * AAC: * New AAC FileType. Supports loading ADTS/ADIF AAC files. :bug:`15` 1.26 - 2014.11.10 ----------------- * MP4: * Parse channels/sample_rate/bits_per_sample/bitrate for ALAC files :bug:`199` :pr:`5` (Adrian Sampson, Christoph Reiter) * ASF: * Support writing multiple values for Author/Title/Copyright/Description/Rating :bug:`151` * Fix read order for multi value tags * Various Python3 fixes * EasyID3: Add more tag mappings :bug:`136` (Ben Ockmore) * MPC/SV8: Fix parsing of SH packets with padding :bug:`198` * docs: * New logo :pr:`4` (Samuel Messner) * Add examples for handling cover art in vorbiscomment :bug:`200` * Add examples for id3v2.3 1.25.1 - 2014.10.13 ------------------- * ID3: Fix parsing of some files with Python 3 :bug:`194` 1.25 - 2014.10.03 ----------------- * Python 3 support (Ben Ockmore et al) :bug:`27` Supported: Python 2.6, 2.7, 3.3, 3.4 (CPython and PyPy) * All custom exceptions now have a common mutagen.MutagenError base class * mutagen.File: prefer theora over vorbis/flac streams in ogg :bug:`184` * New mid3cp script for copying id3 tags :bug:`178` (Marcus Sundman, Ben Ockmore) * ID3: * Parse 2.3/4 frames with 2.2 names :bug:`177` * Try to detect apev2 tags when looking for id3v1 tags :bug:`122` * New id3.Encoding, id3.ID3v1SaveOptions enums :bug:`190` * ASF: * Raise a proper exception on invalid utf-16 :bug:`127` * APEv2: * Fix UnicodeDecodeError during parsing :bug:`174` * MP4: * Fix struct.error exception during parsing :bug:`119` * New AtomDataType enum for MP4FreeForm.dataformat values * Read some previously ignored purl/egit atoms * Read multi value reverse DNS tags written by foobar2000 * Read multi value atoms written by MusicBee :bug:`165` * Write back unknown atoms and ones that failed to parse. 1.24 - 2014.08.13 ----------------- * Moved to Bitbucket: https://bitbucket.org/lazka/mutagen * ID3: * Parse utf-16 text frames with wrong termination :bug:`169` * Fix parsing of utf-16 SYLT frames :bug:`173` * WavPack: * Fix length calculation if sample count is missing in the header :bug:`180` * setup.py: Don't install leftover files produced by the test suite :bug:`179` * tests: Fix error with POSIX locale :bug:`181` 1.23 - 2014.05.14 ----------------- * tools: Don't crash in misconfigured envs, fall back to utf-8. * mp3: Return correct mimetype for MP2 files. :bug:`163` * id3: deterministic sorting of frames. :bug:`166` * AIFF support :bug:`146` (Evan Purkhiser) 1.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. :bug:`135` * ID3: * id3v2.3 writing support :bug:`85` * Add iTunes podcast frames (TGID, TDES, WFED) :bug:`141` * Updated id3v1 genre list * MP4: * add_tags() will not replace existing tags. :bug:`101` * Don't ignore tags if parsing unknown atoms fails. * Raise on invalid 64bit atom size :bug:`132` (Sidnei da Silva) * APEv2: * Handle invalid tag item count. :bug:`145` (Dawid Zamirski) * Ogg: * Faster parsing of files with large packets. * VComment: * Preserve text case for field names added through the dict interface :bug:`152` * mid3v2: * New -e,--escape switch to enable interpretation of escape sequences and makes escaping of the colon separator possible. :bug:`159` * mid3iconv: * Convert COMM frames :bug:`128` 1.21 - 2013.01.30 ----------------- * Fix Python 2.3 compatibility (broken in 1.19). * Fix many warnings triggered by -3. :bug:`27` * mid3v2: * Add --TXXX support. :bug:`62` (Tim Phipps) * Add --POPM support. :bug:`71` * Allow setting multiple COMM or TXXX frames with one command line. * FLAC: * Try to handle corrupt Vorbis comment block sizes. :bug:`52` * Try to handle corrupt Picture block sizes :bug:`106` (Christoph Reiter) * Don't leak file handle with PyPy :bug:`111` (Marien Zwart) * ID3: * MakeID3v1: Do not generate bad tags when given short dates. :bug:`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 :bug:`97` (Christoph Reiter) * Fix handling of invalid SYLT frames :bug:`105` (Christoph Reiter) * MP3: * Fix error when loading extremely small MP3s. :bug:`72` * Fix rounding error in CBR length calculation :bug:`93` (Christoph Reiter) * Use 'open' rather than 'file' everywhere. :bug:`74` (Dan Callahan) * mid3iconv: * Accurately copy QL-style frame encoding behavior. :bug:`75` * Skip unopenable files. :bug:`79` * ID3FileType: * Remember which tag type load() was called with even if the file doesn't yet have any ID3 tags. :bug:`89` * VComment: * Prevent MemoryError when parsing invalid header :bug:`112` (Jyrki Pulliainen) * ASF: * Don't corrupt files on the second save() call :bug:`81` (Christoph Reiter) * Always store GUID objects in the MetadataLibraryBlock :bug:`81` * OggTheora: Fix length/bitrate calculation. :bug:`99` (Christoph Reiter) * MP4: * Less strict MP4 covr atom parsing. :bug:`86` (Lukáš Lalinský) * Support atoms that extend to the end of the file. :bug:`109` (Sidnei da Silva) * Preserve freeform format flags :bug:`103` (Christoph Reiter) * OggOpus support. :bug:`115` (Christoph Reiter) * Musepack: * Fix SV7 bitrate calculation :bug:`7` (Christoph Reiter) * Support SV8 :bug:`7` (Christoph Reiter) 1.20 - 2010.08.04 ----------------- * ASF: Don't store blocks over 64K in the MetadataObject block; use the MetadataLibraryBlock instead. :bug:`60` (Lukáš Lalinský) * ID3: Faster parsing of files with lots of padding. :bug:`65` (Christoph Reiter) * FLAC: Correct check for audio data start. :bug:`67` 1.19 - 2010.02.18 ----------------- * ID3: * POPM: 'count' is optional; the attribute may not exist. :bug:`33` * TimeStampTextFrame: Fix a TypeError in unicode comparisons. :bug:`43` * MakeID3v1: Translate TYER into ID3v1 year if TDRC is not present. :bug:`42` * mid3v2: * Allow --delete followed by --frame, and --genre 1 --genre 2. :bug:`37` * Add --quiet and --verbose flags. :bug:`40` * moggsplit: --m3u option to write an M3U playlist of the new files. :bug:`39` * mid3iconv: Fix crash when processing TCML or TIPL frames. :bug:`41` * VCommentDict: Correctly normalize key names for .keys() iterator. :bug:`45` * MP3: Correct length calculation for MPEG-2 files. :bug:`46` * oggflac: Fix typo in docstring. :bug:`53` * EasyID3: Force UTF-8 encoding. :bug:`54` * EasyMP4: Fix 'genre' translation. :bug:`56` 1.18 - 2009.10.22 ----------------- * ASF: * Distinguish between empty and absent tag values in ContentDescriptionObjects. :bug:`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. :bug:`32` 1.17 - 2009.10.07 ----------------- * ID3: * Support for the iTunes non-standard TSO2 and TSOC frames. * Attempt to recover from bad SYLT frames. :bug:`2` * Attempt to recover from faulty extended header flags. :bug:`4` :bug:`21` * Fix a bug in ID3v2.4 footer flag detection, :bug:`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. :bug:`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. :bug:`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). :bug:`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.31/README.rst0000644000175000017500000000271112574061222015015 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, OptimFROG, and AIFF 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, 2.7, 3.3, 3.4 (CPython and PyPy) and has no dependencies outside the Python 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.31/COPYING0000644000175000017500000004325412574061222014370 0ustar lazkalazka00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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 Lesser 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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 Lesser General Public License instead of this License.