././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1596184724.827105 mutagen-1.45.1/0000775000175000017500000000000000000000000013473 5ustar00lazkalazka00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/COPYING0000644000175000017500000004325400000000000014534 0ustar00lazkalazka00000000000000 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594483485.0 mutagen-1.45.1/MANIFEST.in0000664000175000017500000000051000000000000015225 0ustar00lazkalazka00000000000000include COPYING include NEWS include README.rst include setup.cfg include MANIFEST.in include tests/data/* include tests/quality/* include tests/*.py include man/*.1 recursive-include mutagen *.pyi *.typed recursive-include mutagen README.rst recursive-include docs *.py Makefile *.rst *.png *.svg *.ico *.css prune docs/_build ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1596184570.0 mutagen-1.45.1/NEWS0000664000175000017500000007413500000000000014204 0ustar00lazkalazka00000000000000.. _release-1.45.1: 1.45.1 - 2020-07-31 ------------------- * Fix flake8 tests when run after calling ``setup.py build`` :bug:`482` * No longer use mmap when rewriting files. Fixes slow save performance with Windows network shares, ZFS and more :pr:`483` :pr:`484` .. _release-1.45.0: 1.45.0 - 2020-07-11 ------------------- * WAVE support with ID3 tags :pr:`408` (:user:`Philipp Wolfer `, :user:`Borewit`) * DSDIFF support with ID3 tags :pr:`473` :pr:`472` (:user:`Philipp Wolfer `) * MP4: Add support for nero chapters :pr:`398` (:user:`Martin Weinelt `) * wavpack: add support for wavpack DSD :pr:`464` (:user:`Timothy Redaelli `) * wavpack: add bits_per_sample :pr:`467` (:user:`Timothy Redaelli `) * MP4: handle DecoderSpecificInfo with wrong instance size :pr:`465` * docs: various fixes :pr:`461` (:user:`Terence Eden `), :pr:`474` (:user:`naglis`) * tests: depend on flake8 now (instead of pycodestyle and pyflakes) * docs: fix warnings with sphinx v3 Fuzzing related: * Fuzzing integration with `python-afl `__ :pr:`449` * Fix various unhandled error cases in ogg, asf, oggvorbis, id3 :pr:`441`, :pr:`445`, :pr:`446`, :pr:`447`, :pr:`448`, :pr:`454` (:user:`Julien Voisin `) * aac: Fix ZeroDivisionError in case frequency is unknown * musepack: handle truncated stream header * musepack: handle invalid sample rate index * musepack: handle duplicate RG/SH packets * oggtheora: handle truncated header packet * oggtheora: fail if FRN in the header packet is zero * oggtheora: handle empty pages in more cases * ogg: handle empty pages in to_packets() * aiff: handle overflow in read_float() :pr:`456` .. _release-1.43.1: 1.43.1 - 2020-07-11 ------------------- * Add pickle support for enum types :pr:`477` * docs: fix various warnings with sphinx v3 .. _release-1.44.0: 1.44.0 - 2020-02-10 ------------------- * Python 2 is no longer supported :bug:`410` * mp4: Fix some infinite loops in the mp4 parser :pr:`429` (:user:`Julien Voisin `) * flac: Fix pickle support for SeekPoint :pr:`428` (:user:`point-source`) * aiff: Fix a division by zero :pr:`431` (:user:`Julien Voisin `) * asf: Catch a MemoryError :pr:`432` (:user:`Julien Voisin `) * oggtheora: Fix a division by zero :pr:`430` (:user:`Julien Voisin `) .. _release-1.43.0: 1.43.0 - 2019-11-17 ------------------- * **Note: 1.43.x might be the last version supporting Python 2** * Python 3.4 is no longer supported * Building requires 'setuptools' now, CLI tools depend on 'pkg_resources' * CLI tools are setuptools entry points now .. * Fix collections ABCs deprecation warning :pr:`371` (:user:`Ken Sato `) * Minor typo fixes :pr:`375` (:user:`Nicholas Chammas `) * MP3: increase max initial wrong syncs from 1000 to 1500 :pr:`376` (:user:`Hamid Alaei Varnosfaderani `) * FLAC: support files with multiple VORBIS_COMMENT blocks like libflac :pr:`378` * ID3: Improved TYER/TDAT/TIME upgrade to TDRC :pr:`385` * MP4: Add support for iTunes HD Video tag (hdvd) :pr:`386` (:user:`Jay Sandhu `) * Add AC3 file type :pr:`400` (:user:`Philipp Wolfer `) * AIFF: renamed sample_size to bits_per_sample (sample_size still works) :pr:`403` (:user:`Philipp Wolfer `) * API doc fixes :pr:`404` :pr:`407` (:user:`Philipp Wolfer `) * Add support for Tom's lossless Audio Kompressor (TAK) :pr:`405` (:user:`Philipp Wolfer `) * OptimFROG: support encoder version >= 5.100 :pr:`406` (:user:`Philipp Wolfer `) * AIFF: Fix handling of padding bytes, safe chunk manipulation :pr:`409` (:user:`Philipp Wolfer `) * Fix typos :pr:`412` (:user:`Tim Gates `) .. _release-1.42.0: 1.42.0 - 2018-12-26 ------------------- * ID3: Always read id3v1 tags and include them when no id3v2 equivalent exists. Can be disabled with the new ``load_v1`` option, see :meth:`id3.ID3.load` :pr:`357` (:user:`Fredrik Strupe `) * ID3: Add a pretty print implementation for SYLT :pr:`359` (:user:`Hamid Alaei Varnosfaderani `) * vorbis: Improved error messages when validating keys/values :pr:`356` (:user:`Michael Booth `) * Fix pylint warnings when using the various ``save()`` methods :pr:`364` .. _release-1.41.1: 1.41.1 - 2018-08-11 ------------------- * MP4: fix rtng, stik, shwm getting saved as 16bit ints instead of 8bit :bug:`349` .. _release-1.41.0: 1.41.0 - 2018-07-15 ------------------- * Documentation fixes :pr:`342` (:user:`Jakub Wilk `) * mid3v2: Add support for WXXX frames :bug:`344` :bug:`348` * Fix decoding of track_peak from MP3 Info Tag :pr:`345` (:user:`Anton Yuzhaninov `) * MonkeysAudio: set bits_per_sample for older files :bug:`347` .. _release-1.40.0: 1.40.0 - 2018-01-25 ------------------- * APEv2: Ensures tags are saved in a deterministic way :pr:`329` (:user:`cushy007`) * Restore WinXP support for the CLI tools :bug:`332` * easymp4: Fix EasyMP4.add_tags() when no tags exist :bug:`334` * id3: Fix PyCharm not being able to resolve id3 frame class references :bug:`336` * Support pathlib.Path objects for paths (PEP 519) :bug:`337` :pr:`338` (:user:`Andrew Rabert `) * Use semver for versioning 1.39 - 2017-11-05 ----------------- * Tests: * Require `hypothesis `__ * Run pycodestyle/pyflakes tests by default. Skip with ``--no-quality`` or ``-m no quality`` when using pytest directly. * Python 3.3 is no longer supported * MP3: Improved bitrate accuracy for files with XING header :bug:`328` (thanks :user:`Michaël Defferrard `) * ASF: Fix case where some tags resulted in broken ASFUnicodeAttribute instances :bug:`324` * Add support for filesystems which don't support opening files read/write (gvfs over fuse for example) :bug:`300` * mid3v2: Add support for USLT :bug:`306` * Minor improvements by :user:`Borewit` and :user:`Evan Purkhiser ` 1.38 - 2017-06-01 ----------------- * Note: New release tarballs are now hosted on github: https://github.com/quodlibet/mutagen/releases * ID3: * Add iTunes grouping frame `id3.GRP1` :bug:`304` * Fix exposing text frames where the text can't be encoded with the reported encoding due to merging of frames :bug:`307` * OGG: Fix wrong StreamInfo.length (small negative value) for all ogg based formats in rare cases. :bug:`308` 1.37 - 2017.02.24 ----------------- * Relicense "GPLv2" → "GPLv2 or later" :bug:`291` * DSF: add `mutagen.dsf` module for DSF (DSD Stream File) support :pr:`283` (Boris Pruessmann) * MP3: Add `mp3.MPEGInfo.encoder_settings` containing a guess of the encoder settings used, for example ``"-V2"`` for LAME :bug:`66` * ID3: add iTunes movement related frames `id3.MVIN` and `id3.MVNM` * MP4: support ``©mvi``, ``©mvc``, ``shwm``, ``stik``, ``rtng``, ``tves``, ``tvsn``, ``plID``, ``cnID``, ``geID``, ``atID``, ``sfID``, ``cmID``, ``akID`` :bug:`130` 1.36.3 - 2017.02.24 ------------------- * MP3: fix error with xing frames without a frame count :bug:`292` 1.36.2 - 2017.01.25 ------------------- * ID3: Always write little endian utf-16 with BOM. Fixes tests on big endian machines :pr:`289` 1.36.1 - 2017.01.22 ------------------- * Support GAE runtime :bug:`286` * FLAC: Fix crash when loading files with zero samples :bug:`287` * MP3: Handle broken lame tags written by older lame versions 1.36 - 2016.12.22 ----------------- * ID3: Ignore trailing empty values for v2.3 text frames :bug:`276` * ID3: Write large APIC frames last :bug:`278` * EasyID3: support saving as v2.3 :bug:`188` * FLAC: Add StreamInfo.bitrate :bug:`279` * mid3cp: Add ``--merge`` option :bug:`277` * MP4: Allow loading files without audio tracks :bug:`272` 1.35.1 - 2016.11.09 ------------------- * Revert back to distutils :bug:`273` 1.35 - 2016.11.02 ----------------- * Tests: Require pytest * Tools: Install .exe launchers on Windows * setup.py: Require setuptools * ID3: * Fix loading files with CRM frames :bug:`239` * Fix loading AENC, LINK, GRID frames with no payload * Merge duplicate text frames with same key on load :bug:`172` * Allow parsing of duplicate APIC frames :bug:`172` * Parse utf-16 text fields with missing BOM :bug:`267` * Increase max resyncs for the mpeg frame search :bug:`268` 1.34.1 - 2016.08.13 ------------------- * ID3: Expose some internals again to make Picard (mostly) work again. * http://tickets.musicbrainz.org/browse/PICARD-833 * https://github.com/metabrainz/picard/pull/479 1.34 - 2016.07.20 ----------------- * ID3: * Add `CTOC ` and `CHAP ` frames. New classes: `ID3Tags `, `CTOCFlags `. :bug:`6` * Add `TCAT `, `TKWD `, `PCST ` frames. :bug:`249` * Validate user provided LNK/LINK frameid. :bug:`242` * Add `RVAD `, RVA frames * Add TST, TSA, TS2, TSP and TSC frames * Fix not writing optional fields when saving to v2.3 * Add default field values for all frames * Drop Python 2.6 support * EasyID3: Fix TXXX frame encoding when setting a non-latin1 encodable value after a latin1 one. :bug:`263` 1.33.2 - 2016.07.05 ------------------- * Fix loading of small ogg/apev2 files (1.33 regression) 1.33.1 - 2016.06.29 ------------------- * Fix Overeager deprecation warnings :bug:`261` 1.33 - 2016.06.29 ----------------- * FileType, Metadata: File-like object support :bug:`1` * mid3v2: Add APIC support. :bug:`47` * EasyID3: Fix handling of RVA2 frames with non-lowercase description :bug:`215` * mid3v2: Add UFID support. :bug:`234` * ID3: Include human-readable representation of the picture type in APIC._pprint() :bug:`244` * EasyID3: make albumartist use TPE2 and move performer to TXXX. :bug:`252` * ID3: id3.ID3TimeStamp comparator: check type :pr:`260` (Fabian Peter Hammerle) * setup.py: follow PEP440 for the development version * FileType/Metadata.load/save/delete no longer raise IOError or IOError subclasses. They only raise subclasses of MutagenError. 1.32 - 2016.05.02 ----------------- * Add basic SMF (Standard MIDI File) support (:mod:`mutagen.smf`) * FLAC: add ``audio/flac`` mime type. :bug:`235` * ASF: Fixed crash when object size is longer than the header and file length (Ben Ockmore) * ID3: Validate attributes set after frame creation :commit:`69368c31e00` (:user:`Daniel Plachotich `) * MP4: validate values in ``__setitem__`` so things don't fail in save() :bug:`236` * tests: Fix SynchronizedTextSpec test on big-endian machines :bug:`247` (Daniel Plachotich) * ID3: do type checking in ``__setitem__`` :bug:`251` * Building the documentation now requires sphinx >= 1.3 * New :class:`mutagen.Tags` base class for tags * Moved from Bitbucket to GitHub 1.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. :commit:`b2f22b81c77` * 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` :commit:`192cfcaf14` (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 :commit:`b728fa75` (:user:`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-existent 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. ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1596184724.827105 mutagen-1.45.1/PKG-INFO0000664000175000017500000000404200000000000014570 0ustar00lazkalazka00000000000000Metadata-Version: 1.2 Name: mutagen Version: 1.45.1 Summary: read and write audio tags for many formats Home-page: https://github.com/quodlibet/mutagen Author: Christoph Reiter Author-email: reiter.christoph@gmail.com License: GPL-2.0-or-later Description: .. image:: https://raw.githubusercontent.com/quodlibet/mutagen/master/docs/images/logo.svg :align: center :width: 400px | Mutagen is a Python module to handle audio metadata. It supports ASF, FLAC, MP4, 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 with Python 3.5+ (CPython and PyPy) on Linux, Windows and macOS, and has no dependencies outside the Python standard library. Mutagen is licensed under the GPL version 2 or later. For more information visit https://mutagen.readthedocs.org .. image:: https://codecov.io/gh/quodlibet/mutagen/branch/master/graph/badge.svg :target: https://codecov.io/gh/quodlibet/mutagen Platform: UNKNOWN Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+) Classifier: Topic :: Multimedia :: Sound/Audio Requires-Python: >=3.5, <4 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594553816.0 mutagen-1.45.1/README.rst0000664000175000017500000000201100000000000015154 0ustar00lazkalazka00000000000000.. image:: https://raw.githubusercontent.com/quodlibet/mutagen/master/docs/images/logo.svg :align: center :width: 400px | Mutagen is a Python module to handle audio metadata. It supports ASF, FLAC, MP4, 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 with Python 3.5+ (CPython and PyPy) on Linux, Windows and macOS, and has no dependencies outside the Python standard library. Mutagen is licensed under the GPL version 2 or later. For more information visit https://mutagen.readthedocs.org .. image:: https://codecov.io/gh/quodlibet/mutagen/branch/master/graph/badge.svg :target: https://codecov.io/gh/quodlibet/mutagen ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1596184724.8151057 mutagen-1.45.1/docs/0000775000175000017500000000000000000000000014423 5ustar00lazkalazka00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/Makefile0000644000175000017500000000012200000000000016054 0ustar00lazkalazka00000000000000all: python3 -m sphinx -b html -n . _build clean: rm -rf _build .PHONY: clean ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1596184724.8151057 mutagen-1.45.1/docs/api/0000775000175000017500000000000000000000000015174 5ustar00lazkalazka00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/aac.rst0000644000175000017500000000035000000000000016446 0ustar00lazkalazka00000000000000AAC === .. automodule:: mutagen.aac .. autoexception:: mutagen.aac.AACError .. autoclass:: mutagen.aac.AAC(filename) :show-inheritance: :members: .. autoclass:: mutagen.aac.AACInfo() :show-inheritance: :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574016271.0 mutagen-1.45.1/docs/api/ac3.rst0000644000175000017500000000035000000000000016370 0ustar00lazkalazka00000000000000AC3 === .. automodule:: mutagen.ac3 .. autoexception:: mutagen.ac3.AC3Error .. autoclass:: mutagen.ac3.AC3(filename) :show-inheritance: :members: .. autoclass:: mutagen.ac3.AC3Info() :show-inheritance: :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/aiff.rst0000644000175000017500000000025700000000000016635 0ustar00lazkalazka00000000000000AIFF ---- .. automodule:: mutagen.aiff .. autoclass:: mutagen.aiff.AIFF(filename) :show-inheritance: :members: .. autoclass:: mutagen.aiff.AIFFInfo() :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/ape.rst0000644000175000017500000000062400000000000016473 0ustar00lazkalazka00000000000000APEv2 ===== .. automodule:: mutagen.apev2 .. autoexception:: mutagen.apev2.error .. autoexception:: mutagen.apev2.APENoHeaderError .. autoexception:: mutagen.apev2.APEUnsupportedVersionError .. autoexception:: mutagen.apev2.APEBadItemError .. autoclass:: mutagen.apev2.APEv2File :show-inheritance: :members: .. autoclass:: mutagen.apev2.APEv2 :members: :bases: `mutagen.Metadata` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1592725327.0 mutagen-1.45.1/docs/api/asf.rst0000664000175000017500000000203000000000000016472 0ustar00lazkalazka00000000000000ASF === .. currentmodule:: mutagen .. automodule:: mutagen.asf .. autoclass:: mutagen.asf.ASF :show-inheritance: :members: .. autoclass:: mutagen.asf.ASFInfo :show-inheritance: :members: .. autoclass:: mutagen.asf.ASFTags :show-inheritance: :members: .. autoclass:: mutagen.asf.ASFBaseAttribute() :members: .. 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` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/docs/api/base.rst0000664000175000017500000000334600000000000016646 0ustar00lazkalazka00000000000000Main Module ----------- .. automodule:: mutagen :members: File, version, version_string Base Classes ~~~~~~~~~~~~ .. autoclass:: mutagen.FileType :members: pprint, add_tags, mime, save, delete :show-inheritance: .. autoclass:: mutagen.Tags :members: pprint .. autoclass:: mutagen.Metadata :show-inheritance: :members: save, delete .. 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: Other Classes and Functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. currentmodule:: mutagen .. class:: text() This type only exists for documentation purposes. It represents :obj:`python:str` under Python 3. .. class:: bytes() This type only exists for documentation purposes. It represents :obj:`python:bytes` under Python 3. .. class:: fspath() This type only exists for documentation purposes. It represents a file name which can be :obj:`python:str` or :obj:`python:bytes` under Python 3. .. class:: fileobj() This type only exists for documentation purposes. A file-like object. See :doc:`/user/filelike` for more information. .. class:: filething() This type only exists for documentation purposes. Either a `fspath` or a `fileobj`. .. function:: PaddingFunction(info) A function you can implement and pass to various ``save()`` methods for controlling the amount of padding to use. See :doc:`/user/padding` for more information. :param PaddingInfo info: :returns: The amount of padding to use :rtype: int ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/docs/api/dsdiff.rst0000664000175000017500000000026100000000000017164 0ustar00lazkalazka00000000000000DSDIFF ====== .. automodule:: mutagen.dsdiff .. autoclass:: mutagen.dsdiff.DSDIFF :show-inheritance: :members: .. autoclass:: mutagen.dsdiff.DSDIFFInfo :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/dsf.rst0000644000175000017500000000025100000000000016476 0ustar00lazkalazka00000000000000DSF --- .. automodule:: mutagen.dsf .. autoclass:: mutagen.dsf.DSF(filething) :show-inheritance: :members: .. autoclass:: mutagen.dsf.DSFInfo() :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/flac.rst0000644000175000017500000000107100000000000016630 0ustar00lazkalazka00000000000000FLAC ==== .. currentmodule:: mutagen .. automodule:: mutagen.flac .. autoclass:: mutagen.flac.FLAC :show-inheritance: :members: :exclude-members: vc, METADATA_BLOCKS, load, add_vorbiscomment .. autoclass:: mutagen.flac.StreamInfo :members: :bases: `mutagen.StreamInfo` .. autoclass:: mutagen.flac.Picture :members: .. autoclass:: mutagen.flac.CueSheet :members: .. autoclass:: mutagen.flac.CueSheetTrack :members: .. autoclass:: mutagen.flac.CueSheetTrackIndex :members: .. autoclass:: mutagen.flac.SeekTable :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/id3.rst0000644000175000017500000000165600000000000016413 0ustar00lazkalazka00000000000000ID3 === .. automodule:: mutagen.id3 ID3 Frames ---------- .. toctree:: :titlesonly: id3_frames .. autoclass:: mutagen.id3.ID3v1SaveOptions :members: :member-order: bysource .. autoclass:: mutagen.id3.PictureType :members: :member-order: bysource .. autoclass:: mutagen.id3.Encoding :members: :member-order: bysource .. autoclass:: mutagen.id3.CTOCFlags :members: :member-order: bysource ID3 --- .. autoclass:: mutagen.id3.ID3Tags :show-inheritance: :members: :exclude-members: loaded_frame .. autoclass:: mutagen.id3.ID3 :show-inheritance: :members: .. autoclass:: mutagen.id3.ID3FileType :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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/id3_frames.rst0000644000175000017500000003036400000000000017746 0ustar00lazkalazka00000000000000Frame Base Classes ------------------ .. autoclass:: mutagen.id3.Frame() :show-inheritance: :members: .. autoclass:: mutagen.id3.BinaryFrame(data='') :show-inheritance: :members: .. autoclass:: mutagen.id3.PairedTextFrame(encoding=, people=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TextFrame(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.UrlFrame(url=u'') :show-inheritance: :members: .. autoclass:: mutagen.id3.NumericPartTextFrame(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.NumericTextFrame(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TimeStampTextFrame(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.UrlFrameU(url=u'') :show-inheritance: :members: ID3v2.3/4 Frames ---------------- .. autoclass:: mutagen.id3.AENC(owner=u'', preview_start=0, preview_length=0, data='') :show-inheritance: :members: .. autoclass:: mutagen.id3.APIC(encoding=, mime=u'', type=, desc=u'', data='') :show-inheritance: :members: .. autoclass:: mutagen.id3.ASPI(S=0, L=0, N=0, b=0, Fi=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.CHAP(element_id=u'', start_time=0, end_time=0, start_offset=4294967295, end_offset=4294967295, sub_frames={}) :show-inheritance: :members: .. autoclass:: mutagen.id3.COMM(encoding=, lang='XXX', desc=u'', text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.COMR(encoding=, price=u'', valid_until='19700101', contact=u'', format=0, seller=u'', desc=u'') :show-inheritance: :members: .. autoclass:: mutagen.id3.CTOC(element_id=u'', flags=<0: 0>, child_element_ids=[], sub_frames={}) :show-inheritance: :members: .. autoclass:: mutagen.id3.ENCR(owner=u'', method=128, data='') :show-inheritance: :members: .. autoclass:: mutagen.id3.EQU2(method=0, desc=u'', adjustments=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.ETCO(format=1, events=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.GEOB(encoding=, mime=u'', filename=u'', desc=u'', data='') :show-inheritance: :members: .. autoclass:: mutagen.id3.GRID(owner=u'', group=128, data='') :show-inheritance: :members: .. autoclass:: mutagen.id3.GRP1(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.IPLS(encoding=, people=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.LINK(frameid='XXXX', url=u'', data='') :show-inheritance: :members: .. autoclass:: mutagen.id3.MCDI(data='') :show-inheritance: :members: .. autoclass:: mutagen.id3.MLLT(frames=0, bytes=0, milliseconds=0, bits_for_bytes=0, bits_for_milliseconds=0, data='') :show-inheritance: :members: .. autoclass:: mutagen.id3.MVIN(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.MVNM(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.OWNE(encoding=, price=u'', date='19700101', seller=u'') :show-inheritance: :members: .. autoclass:: mutagen.id3.PCNT(count=0) :show-inheritance: :members: .. autoclass:: mutagen.id3.PCST(value=0) :show-inheritance: :members: .. autoclass:: mutagen.id3.POPM(email=u'', rating=0) :show-inheritance: :members: .. autoclass:: mutagen.id3.POSS(format=1, position=0) :show-inheritance: :members: .. autoclass:: mutagen.id3.PRIV(owner=u'', data='') :show-inheritance: :members: .. autoclass:: mutagen.id3.RBUF(size=0) :show-inheritance: :members: .. autoclass:: mutagen.id3.RVA2(desc=u'', channel=1, gain=1, peak=1) :show-inheritance: :members: .. autoclass:: mutagen.id3.RVAD(adjustments=[0, 0]) :show-inheritance: :members: .. autoclass:: mutagen.id3.RVRB(left=0, right=0, bounce_left=0, bounce_right=0, feedback_ltl=0, feedback_ltr=0, feedback_rtr=0, feedback_rtl=0, premix_ltr=0, premix_rtl=0) :show-inheritance: :members: .. autoclass:: mutagen.id3.SEEK(offset=0) :show-inheritance: :members: .. autoclass:: mutagen.id3.SIGN(group=128, sig='') :show-inheritance: :members: .. autoclass:: mutagen.id3.SYLT(encoding=, lang='XXX', format=1, type=0, desc=u'', text=u'') :show-inheritance: :members: .. autoclass:: mutagen.id3.SYTC(format=1, data='') :show-inheritance: :members: .. autoclass:: mutagen.id3.TALB(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TBPM(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TCAT(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TCMP(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TCOM(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TCON(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TCOP(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TDAT(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TDEN(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TDES(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TDLY(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TDOR(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TDRC(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TDRL(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TDTG(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TENC(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TEXT(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TFLT(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TGID(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TIME(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TIPL(encoding=, people=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TIT1(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TIT2(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TIT3(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TKEY(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TKWD(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TLAN(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TLEN(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TMCL(encoding=, people=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TMED(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TMOO(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TOAL(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TOFN(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TOLY(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TOPE(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TORY(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TOWN(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TPE1(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TPE2(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TPE3(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TPE4(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TPOS(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TPRO(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TPUB(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TRCK(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TRDA(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TRSN(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TRSO(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TSIZ(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TSO2(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TSOA(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TSOC(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TSOP(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TSOT(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TSRC(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TSSE(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TSST(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TXXX(encoding=, desc=u'', text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.TYER(encoding=, text=[]) :show-inheritance: :members: .. autoclass:: mutagen.id3.UFID(owner=u'', data='') :show-inheritance: :members: .. autoclass:: mutagen.id3.USER(encoding=, lang='XXX', text=u'') :show-inheritance: :members: .. autoclass:: mutagen.id3.USLT(encoding=, lang='XXX', desc=u'', text=u'') :show-inheritance: :members: .. autoclass:: mutagen.id3.WCOM(url=u'') :show-inheritance: :members: .. autoclass:: mutagen.id3.WCOP(url=u'') :show-inheritance: :members: .. autoclass:: mutagen.id3.WFED(url=u'') :show-inheritance: :members: .. autoclass:: mutagen.id3.WOAF(url=u'') :show-inheritance: :members: .. autoclass:: mutagen.id3.WOAR(url=u'') :show-inheritance: :members: .. autoclass:: mutagen.id3.WOAS(url=u'') :show-inheritance: :members: .. autoclass:: mutagen.id3.WORS(url=u'') :show-inheritance: :members: .. autoclass:: mutagen.id3.WPAY(url=u'') :show-inheritance: :members: .. autoclass:: mutagen.id3.WPUB(url=u'') :show-inheritance: :members: .. autoclass:: mutagen.id3.WXXX(encoding=, desc=u'', url=u'') :show-inheritance: :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/docs/api/index.rst0000664000175000017500000000050600000000000017036 0ustar00lazkalazka00000000000000API Reference ============= .. toctree:: base aac ac3 aiff ape asf dsdiff dsf flac id3 monkeysaudio mp3 mp4 musepack ogg oggflac oggopus oggspeex oggtheora oggvorbis optimfrog smf tak trueaudio vcomment wave wavpack ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/monkeysaudio.rst0000644000175000017500000000036600000000000020440 0ustar00lazkalazka00000000000000Monkey's Audio ============== .. automodule:: mutagen.monkeysaudio .. autoclass:: mutagen.monkeysaudio.MonkeysAudio :show-inheritance: :members: .. autoclass:: mutagen.monkeysaudio.MonkeysAudioInfo :show-inheritance: :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/mp3.rst0000644000175000017500000000051500000000000016424 0ustar00lazkalazka00000000000000MP3 === .. automodule:: mutagen.mp3 .. autoclass:: mutagen.mp3.MP3 :show-inheritance: :members: .. autoclass:: mutagen.mp3.MPEGInfo :show-inheritance: :members: .. autoclass:: mutagen.mp3.BitrateMode :members: .. autoclass:: mutagen.mp3.EasyMP3 :show-inheritance: :members: :exclude-members: ID3 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/mp4.rst0000644000175000017500000000126300000000000016426 0ustar00lazkalazka00000000000000MP4 === .. automodule:: mutagen.mp4 MP4 --- .. autoclass:: mutagen.mp4.MP4 :show-inheritance: :members: :exclude-members: MP4Tags .. autoclass:: mutagen.mp4.MP4Tags :show-inheritance: :members: .. autoclass:: mutagen.mp4.MP4Info :show-inheritance: :members: .. autoclass:: mutagen.mp4.MP4Cover :members: .. autoclass:: mutagen.mp4.MP4FreeForm :members: .. autoclass:: mutagen.mp4.AtomDataType :members: EasyMP4 ------- .. automodule:: mutagen.easymp4 .. autoclass:: mutagen.easymp4.EasyMP4 :show-inheritance: :members: :exclude-members: MP4Tags .. autoclass:: mutagen.easymp4.EasyMP4Tags :show-inheritance: :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/musepack.rst0000644000175000017500000000032600000000000017535 0ustar00lazkalazka00000000000000Musepack ======== .. automodule:: mutagen.musepack .. autoclass:: mutagen.musepack.Musepack :show-inheritance: :members: .. autoclass:: mutagen.musepack.MusepackInfo :show-inheritance: :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/ogg.rst0000644000175000017500000000016200000000000016477 0ustar00lazkalazka00000000000000OGG === .. automodule:: mutagen.ogg .. autoclass:: mutagen.ogg.OggFileType :show-inheritance: :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/oggflac.rst0000644000175000017500000000031100000000000017321 0ustar00lazkalazka00000000000000Ogg FLAC ======== .. automodule:: mutagen.oggflac .. autoclass:: mutagen.oggflac.OggFLAC :show-inheritance: .. autoclass:: mutagen.oggflac.OggFLACStreamInfo :show-inheritance: :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/oggopus.rst0000644000175000017500000000032100000000000017403 0ustar00lazkalazka00000000000000Ogg Opus ======== .. automodule:: mutagen.oggopus .. autoclass:: mutagen.oggopus.OggOpus :show-inheritance: :members: .. autoclass:: mutagen.oggopus.OggOpusInfo :show-inheritance: :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/oggspeex.rst0000644000175000017500000000033000000000000017541 0ustar00lazkalazka00000000000000Ogg Speex ========= .. automodule:: mutagen.oggspeex .. autoclass:: mutagen.oggspeex.OggSpeex :show-inheritance: :members: .. autoclass:: mutagen.oggspeex.OggSpeexInfo :show-inheritance: :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/oggtheora.rst0000644000175000017500000000033700000000000017706 0ustar00lazkalazka00000000000000Ogg Theora ========== .. automodule:: mutagen.oggtheora .. autoclass:: mutagen.oggtheora.OggTheora :show-inheritance: :members: .. autoclass:: mutagen.oggtheora.OggTheoraInfo :show-inheritance: :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/oggvorbis.rst0000644000175000017500000000033700000000000017730 0ustar00lazkalazka00000000000000Ogg Vorbis ---------- .. automodule:: mutagen.oggvorbis .. autoclass:: mutagen.oggvorbis.OggVorbis :show-inheritance: :members: .. autoclass:: mutagen.oggvorbis.OggVorbisInfo :show-inheritance: :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/optimfrog.rst0000644000175000017500000000030600000000000017731 0ustar00lazkalazka00000000000000OptimFROG ========= .. automodule:: mutagen.optimfrog .. autoclass:: mutagen.optimfrog.OptimFROG :show-inheritance: :members: .. autoclass:: mutagen.optimfrog.OptimFROGInfo :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/smf.rst0000644000175000017500000000027200000000000016512 0ustar00lazkalazka00000000000000Standard MIDI File ================== .. automodule:: mutagen.smf .. autoclass:: mutagen.smf.SMF :show-inheritance: :members: .. autoclass:: mutagen.smf.SMFInfo :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574016271.0 mutagen-1.45.1/docs/api/tak.rst0000644000175000017500000000023400000000000016502 0ustar00lazkalazka00000000000000TAK === .. automodule:: mutagen.tak .. autoclass:: mutagen.tak.TAK :show-inheritance: :members: .. autoclass:: mutagen.tak.TAKInfo :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/trueaudio.rst0000644000175000017500000000046500000000000017732 0ustar00lazkalazka00000000000000TrueAudio ========= .. automodule:: mutagen.trueaudio .. autoclass:: mutagen.trueaudio.TrueAudio :show-inheritance: :members: .. autoclass:: mutagen.trueaudio.TrueAudioInfo :members: .. autoclass:: mutagen.trueaudio.EasyTrueAudio :show-inheritance: :members: :exclude-members: ID3 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/vcomment.rst0000644000175000017500000000024600000000000017556 0ustar00lazkalazka00000000000000Vorbis Comment ============== .. autoclass:: mutagen._vorbis.VComment() :show-inheritance: .. autoclass:: mutagen._vorbis.VCommentDict() :show-inheritance: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/docs/api/wave.rst0000664000175000017500000000026500000000000016673 0ustar00lazkalazka00000000000000WAVE ---- .. automodule:: mutagen.wave .. autoclass:: mutagen.wave.WAVE(filename) :show-inheritance: :members: .. autoclass:: mutagen.wave.WaveStreamInfo() :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/api/wavpack.rst0000644000175000017500000000027000000000000017357 0ustar00lazkalazka00000000000000WavPack ======= .. automodule:: mutagen.wavpack .. autoclass:: mutagen.wavpack.WavPack :show-inheritance: :members: .. autoclass:: mutagen.wavpack.WavPackInfo :members: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480683.0 mutagen-1.45.1/docs/changelog.rst0000644000175000017500000000011100000000000017073 0ustar00lazkalazka00000000000000Changelog ========= .. py:currentmodule:: mutagen .. include:: ../NEWS ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/docs/conf.py0000664000175000017500000000241700000000000015726 0ustar00lazkalazka00000000000000# -*- 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_, ".."))) needs_sphinx = "1.3" extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx.ext.intersphinx', 'sphinx.ext.extlinks', ] intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), } source_suffix = '.rst' master_doc = 'index' project = 'mutagen' copyright = u'2016, Joe Wreschnig, Michael Urman, Lukáš Lalinský, ' \ u'Christoph Reiter, Ben Ockmore & others' html_title = project exclude_patterns = ['_build'] extlinks = { 'bug': ('https://github.com/quodlibet/mutagen/issues/%s', '#'), 'pr': ('https://github.com/quodlibet/mutagen/pull/%s', '#pr'), 'commit': ('https://github.com/quodlibet/mutagen/commit/%s', '#'), 'user': ('https://github.com/%s', ''), } autodoc_member_order = "bysource" default_role = "obj" html_theme = "sphinx_rtd_theme" html_favicon = "images/favicon.ico" html_theme_options = { "display_version": False, } html_context = { 'extra_css_files': [ '_static/extra.css', ], } html_static_path = [ "extra.css", "images/logo-small.svg", ] suppress_warnings = ["image.nonlocal_uri"] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/contact.rst0000644000175000017500000000061100000000000016604 0ustar00lazkalazka00000000000000Contact ------- 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 IRC channel, our `issue tracker `_ or the mailing list. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/docs/extra.css0000664000175000017500000000171600000000000016265 0ustar00lazkalazka00000000000000.wy-side-nav-search { background-color: initial; } .wy-nav-side, .wy-nav-top { background-color: #24292E; } .wy-side-nav-search input[type="text"] { border-color: transparent; } .wy-nav-content { margin: initial; } .rst-content div[role=navigation], footer { font-size: 0.85em; color: #999; } .rst-content div[role=navigation] hr { margin-top: 6px; } footer hr { margin-bottom: 6px; } .rst-footer-buttons { display: none; } .versionmodified { color: #008000; } a.icon-home, a.icon-home:hover { display: inline-block; padding: 4px 4px 4px 23px; background: transparent url(logo-small.svg) center left no-repeat; background-size: 1.5em; margin-top: 0.2em; margin-bottom: 1em; } .fa-home::before, .icon-home::before { content: ""; } .wy-nav-top a { margin: -2em; background: transparent url(logo-small.svg) center left no-repeat; background-size: 1.5em; padding: 4px 4px 4px 26px; } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/id3_frames_gen.py0000755000175000017500000000257700000000000017656 0ustar00lazkalazka00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """ ./id3_frames_gen.py > api/id3_frames.rst """ import sys import os sys.path.insert(0, os.path.abspath('../')) import mutagen.id3 from mutagen.id3 import Frames, Frames_2_2, Frame BaseFrames = dict([(k, v) for (k, v) in vars(mutagen.id3).items() if v not in Frames.values() and v not in Frames_2_2.values() and isinstance(v, type) and (issubclass(v, Frame) or v is Frame)]) def print_header(header, type_="-"): print(header) print(type_ * len(header)) print("") def print_frames(frames, sort_mro=False): if sort_mro: # less bases first, then by name sort_func = lambda x: (len(x[1].__mro__), x[0]) else: sort_func = lambda x: x for name, cls in sorted(frames.items(), key=sort_func): print(""" .. autoclass:: mutagen.id3.%s :show-inheritance: :members: """ % repr(cls())) if __name__ == "__main__": print_header("Frame Base Classes") print_frames(BaseFrames, sort_mro=True) print_header("ID3v2.3/4 Frames") print_frames(Frames) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1596184724.8151057 mutagen-1.45.1/docs/images/0000775000175000017500000000000000000000000015670 5ustar00lazkalazka00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/images/Makefile0000644000175000017500000000146000000000000017327 0ustar00lazkalazka00000000000000SOURCE=logo.svg DEST=favicon.ico ARGS=--export-area 19:-4:209:186 --export-png all: $(DEST) favicon-16.png: $(SOURCE) inkscape $(SOURCE) $(ARGS) $@ -w 16 -h 16 favicon-24.png: $(SOURCE) inkscape $(SOURCE) $(ARGS) $@ -w 24 -h 24 favicon-32.png: $(SOURCE) inkscape $(SOURCE) $(ARGS) $@ -w 32 -h 32 favicon-48.png: $(SOURCE) inkscape $(SOURCE) $(ARGS) $@ -w 48 -h 48 favicon-64.png: $(SOURCE) inkscape $(SOURCE) $(ARGS) $@ -w 64 -h 64 $(DEST): favicon-16.png favicon-24.png favicon-32.png favicon-48.png favicon-64.png convert favicon-16.png favicon-24.png favicon-32.png favicon-48.png favicon-64.png $(DEST) rm favicon-16.png favicon-24.png favicon-32.png favicon-48.png favicon-64.png .PHONY: clean clean: rm -f favicon-16.png favicon-24.png favicon-32.png favicon-48.png favicon-64.png favicon.ico ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/images/favicon.ico0000644000175000017500000010327600000000000020020 0ustar00lazkalazka00000000000000 hV   F00 %@@ (BD(  **I*\T?-t"|Kh84vqĻĻŹŞĻĻna76}bkĻĻŤķĻĻr3t5kW,/#IUl.V3##I(0 K$<cf I>r1XSd@D"J55 I!M$G=. P8 (tvƆVƲ+S0UŰWyws+<`0;WZ 5 (@^BPuY ZAr?Z jpR8g#`c( @ UU!"^\H|A T*2]JJH+}[E;]-$wwwwwwwFwwwwwab+fŒ+,fż 0__ 1K2,ďZ+cSfffffDv fffffff(K7IlCQ a`]6,)jE[[t'DDÎÎq q  `pp(0` $~~VH UsG1zFjd7`H3{ gh )l)V:*yhnnyb{Z633333333333Jt333333333 5$ Ŵ&}?tJlhMb Ŭŧ}9`^;yŰ ]ŴPgnFō űƲ4"""""""""Kn"""""""""""6Nr~:jk & 8gw.Le%2i>5/dM08d/[BW???|~?|>?|<@@  <<>~>~?(@ @03/33Cjv6 8[l-ExL !:vn=Fzl /=(ffffL&p@JcpwH!DGfff]{.ffff#_fffT yN>xűrƎE+Ĕrvŀ >f_vġ~> 3JŒ CmqFų}}7;sĥyƁHk@ rcŽƩp~BzsŞd<u Jwwww-wwww0Kqwww(AF%wyBCSBm<fVuI95  FrE=l 8O T lX8jb 7bqg 3=Cx||xx800@ C CC'????? ? ? >??././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/docs/images/logo-small.svg0000664000175000017500000001443300000000000020464 0ustar00lazkalazka00000000000000 image/svg+xml ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/images/logo.svg0000644000175000017500000007251700000000000017363 0ustar00lazkalazka00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/docs/index.rst0000664000175000017500000000450000000000000016263 0ustar00lazkalazka00000000000000.. image:: images/logo.svg :align: center :width: 400px | .. toctree:: :hidden: :titlesonly: :maxdepth: 3 changelog user/index api/index man/index contact .. title:: Overview .. include:: ../README.rst :start-after: | ---- There is a :doc:`brief tutorial with several API examples. ` Installing ---------- :: python3 -m pip install mutagen or :: sudo apt-get install python3-mutagen Where do I get it? ------------------ Mutagen is hosted on `GitHub `_. The `download page `_ or `PyPI `_ will have the latest version or check out the git repository:: $ git clone https://github.com/quodlibet/mutagen.git Why Mutagen? ------------ Quod Libet has more strenuous requirements in a tagging library than most programs that deal with tags. 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 committed 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 * `Exaile `_, a media player aiming to be similar to KDE's AmaroK, but for GTK+ ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1596184724.8151057 mutagen-1.45.1/docs/man/0000775000175000017500000000000000000000000015176 5ustar00lazkalazka00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/man/Makefile0000644000175000017500000000021200000000000016627 0ustar00lazkalazka00000000000000# update man pages all: mid3cp.1 mid3iconv.1 mid3v2.1 moggsplit.1 mutagen-inspect.1 mutagen-pony.1 %.1:%.rst rst2man $< > ../../man/$@ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/man/index.rst0000644000175000017500000000143600000000000017041 0ustar00lazkalazka00000000000000Command Line Tools ================== .. toctree:: :hidden: :titlesonly: mid3cp mid3iconv mid3v2 moggsplit mutagen-inspect mutagen-pony In addition to the Python library mutagen installs some command line tools: :doc:`mid3cp` copies the ID3 tags from a source file to a destination file :doc:`mid3iconv` converts ID3 tags from legacy encodings to Unicode and stores them using the ID3v2 format :doc:`mid3v2` is a Mutagen-based replacement for id3lib’s id3v2 :doc:`moggsplit` splits a multiplexed Ogg stream into separate files :doc:`mutagen-inspect` loads and prints information about an audio file and its tags :doc:`mutagen-pony` scans any directories given and reports on the kinds of tags in the MP3s it finds in them ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/man/mid3cp.rst0000644000175000017500000000173400000000000017112 0ustar00lazkalazka00000000000000======== 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. --merge Copy over frames instead of replacing the whole ID3 tag. The tag version of *dest* will be used. In case *dest* has no ID3 tag this option has no effect. AUTHOR ====== Marcus Sundman. Based on id3cp (part of id3lib) by Dirk Mahoney and Scott Thomas Haug. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/man/mid3iconv.rst0000644000175000017500000000144600000000000017626 0ustar00lazkalazka00000000000000=========== 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/man/mid3v2.rst0000644000175000017500000001011600000000000017031 0ustar00lazkalazka00000000000000========= 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, WXXX, 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. -p, --picture= Set the attached picture (APIC). Everything except the filename can be omitted in which case default values will be used. -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 has the format ; 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 description can be omitted in which case it defaults to an empty string. The WXXX frame has the same format as TXXX but since URLs usually contain a ":" you have provide a description or enable escaping (-e): ``--WXXX "desc:http://foo.bar"`` or ``-e --WXXX "http\\://foo.bar"`` The USLT frame has the format . The language and description may be omitted, in which case the language defaults to English, and the description to an empty string. 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/man/moggsplit.rst0000644000175000017500000000174700000000000017744 0ustar00lazkalazka00000000000000=========== 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/man/mutagen-inspect.rst0000644000175000017500000000075500000000000021040 0ustar00lazkalazka00000000000000================= 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/man/mutagen-pony.rst0000644000175000017500000000073600000000000020357 0ustar00lazkalazka00000000000000============== 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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1596184724.8191056 mutagen-1.45.1/docs/user/0000775000175000017500000000000000000000000015401 5ustar00lazkalazka00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/user/apev2.rst0000644000175000017500000000174400000000000017154 0ustar00lazkalazka00000000000000===== 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. Compatibility / Bugs ^^^^^^^^^^^^^^^^^^^^ * 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/user/classes.rst0000644000175000017500000000522700000000000017574 0ustar00lazkalazka00000000000000============== Class Overview ============== .. currentmodule:: mutagen The main notable classes in mutagen are :class:`FileType`, :class:`StreamInfo`, :class:`Tags`, :class:`Metadata` and for error handling the :class:`MutagenError` exception. FileType -------- A :class:`FileType` is used to represent container formats which contain audio/video and tags. In some cases, like MP4, the tagging format and the container are inseparable, while in other cases, like MP3, the container does not know about tags and the metadata is loosely attached to the file. A :class:`FileType` always represents only one combination of an audio stream (StreamInfo) and tags (Tags). In case there are multiple audio streams in one file, mutagen exposes the primary/first one. In case tags are associated with the audio/video stream the FileType represents the stream and its tags. Given a file containing Ogg Theora video with Vorbis audio, depending on whether you use :class:`oggvorbis.OggVorbis` or :class:`oggtheora.OggTheora` for parsing, you get the respective tags and stream info associated with either the vorbis or the theora stream. It provides a dict-like interface which acts as a proxy to the containing `Tags` instance. :: >>> from mutagen.oggvorbis import OggVorbis >>> f = OggVorbis("11. The Way It Is.ogg") >>> type(f) >>> Tags ---- Each FileType has an attribute tags which holds a :class:`Tags` instance. The Tags interface depends mostly on each format. It exposes a dict-like interface where the type of keys and values depends on the implementation of each format. :: >>> type(f.tags) >>> StreamInfo ---------- Similar to Tags, a FileType also holds a :class:`StreamInfo` instance. It represents one audio/video stream and contains its length, codec information, number of channels, etc. :: >>> type(f.info) >>> print(f.info.pprint()) Ogg Vorbis, 346.43 seconds, 499821 bps >>> Metadata -------- `Metadata` is a mixture between `FileType` and `Tags` and is used for tagging formats which are not depending on a container format. They can be attached to any file. This includes ID3 and APEv2. :: >>> from mutagen.id3 import ID3 >>> m = ID3("08. Firestarter.mp3") >>> type(m) >>> MutagenError ------------ The :class:`MutagenError` exception is the base class for all custom exceptions in mutagen. :: from mutagen import MutagenError try: f = OggVorbis("11. The Way It Is.ogg") except MutagenError: print("Loading failed :(") ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1596184724.8191056 mutagen-1.45.1/docs/user/examples/0000775000175000017500000000000000000000000017217 5ustar00lazkalazka00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1595055359.0 mutagen-1.45.1/docs/user/examples/fileobj-iface.py0000664000175000017500000000540200000000000022251 0ustar00lazkalazka00000000000000class IOInterface(object): """This is the interface mutagen expects from custom file-like objects. For loading read(), tell() and seek() have to be implemented. "name" is optional. For saving/deleting write(), flush() and truncate() have to be implemented in addition. fileno() is optional. """ # For loading def tell(self): """Returns he current offset as int. Always >= 0. Raises IOError in case fetching the position is for some reason not possible. """ raise NotImplementedError def read(self, size=-1): """Returns 'size' amount of bytes or less if there is no more data. If no size is given all data is returned. size can be >= 0. Raises IOError in case reading failed while data was available. """ raise NotImplementedError def seek(self, offset, whence=0): """Move to a new offset either relative or absolute. whence=0 is absolute, whence=1 is relative, whence=2 is relative to the end. Any relative or absolute seek operation which would result in a negative position is undefined and that case can be ignored in the implementation. Any seek operation which moves the position after the stream should succeed. tell() should report that position and read() should return an empty bytes object. Returns Nothing. Raise IOError in case the seek operation asn't possible. """ raise NotImplementedError # For loading, but optional @property def name(self): """Should return text. For example the file name. If not available the attribute can be missing or can return an empty string. Will be used for error messages and type detection. """ raise NotImplementedError # For writing def write(self, data): """Write data to the file. Returns Nothing. Raises IOError """ raise NotImplementedError def truncate(self, size=None): """Truncate to the current position or size if size is given. The current position or given size will never be larger than the file size. This has to flush write buffers in case writing is buffered. Returns Nothing. Raises IOError. """ raise NotImplementedError def flush(self): """Flush the write buffer. Returns Nothing. Raises IOError. """ raise NotImplementedError # For writing, but optional def fileno(self): """Returns the file descriptor (int) or raises IOError if there is none. Will be used for low level operations if available. """ raise NotImplementedError ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/docs/user/filelike.rst0000664000175000017500000000346300000000000017725 0ustar00lazkalazka00000000000000============================== Working with File-like Objects ============================== .. currentmodule:: mutagen The first argument passed to a :class:`FileType` or :class:`Metadata` can either be a file name or a file-like object, such as `BytesIO ` and mutagen will figure out what to do. :: MP3("myfile.mp3") MP3(myfileobj) If for some reason the automatic type detection fails, it's possible to pass them using a named argument which skips the type guessing. :: MP3(filename="myfile.mp3") MP3(fileobj=myfileobj) Mutagen expects the file offset to be at 0 for all file objects passed to it. The file-like object has to implement the following interface (It's a limited subset of real buffered file objects and StringIO/BytesIO) .. literalinclude:: examples/fileobj-iface.py Gio Example Implementation -------------------------- The following implements a file-like object using `PyGObject `__ and `Gio `__. It depends on the `giofile `__ Python library. .. code:: python import mutagen import giofile from gi.repository import Gio gio_file = Gio.File.new_for_uri( "http://people.xiph.org/~giles/2012/opus/ehren-paper_lights-96.opus") cancellable = Gio.Cancellable.new() with giofile.open(gio_file, "rb", cancellable=cancellable) as gfile: print(mutagen.File(gfile).pprint()) .. code:: sh $ python example.py Ogg Opus, 228.11 seconds (audio/ogg) ENCODER=opusenc from opus-tools 0.1.5 artist=Ehren Starks title=Paper Lights album=Lines Build Walls date=2005-09-05 copyright=Copyright 2005 Ehren Starks license=http://creativecommons.org/licenses/by-nc-sa/1.0/ organization=magnatune.com ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/user/gettingstarted.rst0000644000175000017500000000463600000000000021172 0ustar00lazkalazka00000000000000=============== Getting Started =============== .. currentmodule:: mutagen The `File` functions takes any audio file, guesses its type and returns a `FileType` instance or `None`. :: >>> import mutagen >>> mutagen.File("11. The Way It Is.ogg") {'album': [u'Always Outnumbered, Never Outgunned'], 'title': [u'The Way It Is'], 'artist': [u'The Prodigy'], 'tracktotal': [u'12'], 'albumartist': [u'The Prodigy'],'date': [u'2004'], 'tracknumber': [u'11'], >>> _.info.pprint() u'Ogg Vorbis, 346.43 seconds, 499821 bps' >>> The following code loads a FLAC file, sets its title, prints all tag data, then saves the file. :: from mutagen.flac import FLAC audio = FLAC("example.flac") audio["title"] = u"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) print(audio.info.bitrate) The following deletes an ID3 tag from an MP3 file. :: from mutagen.id3 import ID3 audio = ID3("example.mp3") audio.delete() Here we parse a Vorbis file as FLAC, which leads to an `MutagenError` being raised. :: >>> import mutagen.flac >>> mutagen.flac.FLAC("11. The Way It Is.ogg") Traceback (most recent call last): File "", line 1, in File "/usr/lib/python2.7/dist-packages/mutagen/_file.py", line 42, in __init__ self.load(filename, *args, **kwargs) File "/usr/lib/python2.7/dist-packages/mutagen/flac.py", line 759, in load self.__check_header(fileobj) File "/usr/lib/python2.7/dist-packages/mutagen/flac.py", line 867, in __check_header "%r is not a valid FLAC file" % fileobj.name) mutagen.flac.FLACNoHeaderError: '11. The Way It Is.ogg' is not a valid FLAC file 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 filenames.) Multiple Values ^^^^^^^^^^^^^^^ Most tag formats support multiple values for each key, so when you access them (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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/user/id3.rst0000644000175000017500000002312300000000000016611 0ustar00lazkalazka00000000000000.. currentmodule:: mutagen.id3 === 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 details of the ID3v2 standard at http://id3.org/id3v2.4.0-structure. ID3 Dict Interface ^^^^^^^^^^^^^^^^^^ .. code:: pycon >>> mutagen.File("01. On The Road Again.mp3").keys() [u'TXXX:replaygain_album_peak', u'RVA2:track', u'APIC:picture', u'UFID:http://musicbrainz.org', 'TDRC', u'TXXX:replaygain_track_peak', 'TIT2', u'RVA2:album', u'TXXX:replaygain_track_gain', u'TXXX:MusicBrainz Album Id', 'TRCK', 'TPE1', 'TALB', u'TXXX:MusicBrainz Album Artist Id', u'TXXX:replaygain_album_gain'] >>> On the first look the key format in the ID3 dict seem a bit confusing, this is because they are the frame hashes of the corresponding dict values (:obj:`Frame.HashKey`). For example the ID3 specification states that there can't be two APIC frames with the same description, so the frame hash contains the description and adding a new frame with the same description will replace the old one. Only the first four letters always represent the frame type name. In many cases you don't care about the hash and just want to look up all frames of one type. For this use the :meth:`ID3Tags.getall` method: .. code:: pycon >>> for frame in mutagen.File("01. On The Road Again.mp3").tags.getall("TXXX"): ... frame ... TXXX(encoding=, desc=u'replaygain_album_peak', text=[u'1.00000000047']) TXXX(encoding=, desc=u'replaygain_track_peak', text=[u'1.00000000047']) TXXX(encoding=, desc=u'replaygain_track_gain', text=[u'-7.429688 dB']) TXXX(encoding=, desc=u'MusicBrainz Album Id', text=[u'be6fb9b0-5073-4633-aefa-c559554f28e5']) TXXX(encoding=, desc=u'MusicBrainz Album Artist Id', text=[u'815a0279-558c-4522-ac3b-6a1e259e95b5']) TXXX(encoding=, desc=u'replaygain_album_gain', text=[u'-7.429688 dB']) For adding new frames you can use the :meth:`ID3Tags.add` method, which will use the frame hash as key. For example the ID3 spec only allows one TALB frame, so passing a TALB frame to add() will replace the old frame: .. code:: pycon >>> tags.getall("TALB") [TALB(encoding=, text=[u'The Very Best of Canned Heat'])] >>> tags.add(TALB(text=[u"new value"])) >>> tags.getall("TALB") [TALB(encoding=, text=[u'new value'])] >>> There is also a corresponding :meth:`ID3Tags.delall` method for deleting all frames of one type. 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:`ID3Tags.update_to_v24` or :meth:`ID3Tags.update_to_v23` depending on the ``v2_version`` argument (defaults to ``4``) * :meth:`ID3Tags.update_to_v24` which upgrades v2.2/3 frames to v2.4 * :meth:`ID3Tags.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:`ID3Tags.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() Chapter Extension ^^^^^^^^^^^^^^^^^ The following code adds two chapters to a file: :: from mutagen.id3 import ID3, CTOC, CHAP, TIT2, CTOCFlags audio = ID3("example.mp3") audio.add( CTOC(element_id=u"toc", flags=CTOCFlags.TOP_LEVEL | CTOCFlags.ORDERED, child_element_ids=[u"chp1", "chp2"], sub_frames=[ TIT2(text=[u"I'm a TOC"]), ])) audio.add( CHAP(element_id=u"chp1", start_time=0, end_time=42000, sub_frames=[ TIT2(text=[u"I'm the first chapter"]), ])) audio.add( CHAP(element_id=u"chp2", start_time=42000, end_time=84000, sub_frames=[ TIT2(text=[u"I'm the second chapter"]), ])) audio.save() Dealing with Frame Uniqueness of ID3 Frames ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ID3 spec defines for each frame type which combination of the contained data needs to be unique in the whole tag and acts as an identifier for that frame. This manifests itself in mutagen in that adding a frame using :meth:`ID3Tags.add`, which has the same key as an existing frame, will replace the old one. One frame type where this commonly leads to confusing results is the APIC frame, which requires that only the description field needs to be unique in the whole tag and not the description and picture type. If you want to add a new frame without replacing an existing one, check the HashKey property and adjust your new frame until it no longer matches any existing frame. .. code:: python tag = ID3() new = APIC() while new.HashKey in tag: new.desc += u"x" tag.add(new) Compatibility / 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. * 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. * 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/user/index.rst0000644000175000017500000000026200000000000017240 0ustar00lazkalazka00000000000000========== User Guide ========== .. toctree:: :titlesonly: :maxdepth: 2 gettingstarted classes filelike padding id3 vcomment apev2 mp4 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/user/mp4.rst0000644000175000017500000000112400000000000016627 0ustar00lazkalazka00000000000000=== 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. Compatibility / Bugs ^^^^^^^^^^^^^^^^^^^^ * 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/user/padding.rst0000644000175000017500000000307000000000000017537 0ustar00lazkalazka00000000000000=========== Tag Padding =========== .. currentmodule:: mutagen 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 and should be avoided. 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 `FileType` and `Metadata` subclasses provide a ``save()`` method which can be passed a ``padding`` callback. This callback gets called with a `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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/docs/user/vcomment.rst0000644000175000017500000000534600000000000017771 0ustar00lazkalazka00000000000000============== Vorbis Comment ============== 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 (except in FLAC) is to store a base64 encoded FLAC Picture block with the key ``metadata_block_picture`` (see https://wiki.xiph.org/VorbisComment#Cover_art). 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`. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1596184724.8191056 mutagen-1.45.1/man/0000775000175000017500000000000000000000000014246 5ustar00lazkalazka00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/man/mid3cp.10000644000175000017500000000331300000000000015505 0ustar00lazkalazka00000000000000.\" 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. .TP .B \-\-merge Copy over frames instead of replacing the whole ID3 tag. The tag version of \fIdest\fP will be used. In case \fIdest\fP has no ID3 tag this option has no effect. .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. . ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/man/mid3iconv.10000644000175000017500000000301700000000000016222 0ustar00lazkalazka00000000000000.\" 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. . ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/man/mid3v2.10000644000175000017500000001205300000000000015433 0ustar00lazkalazka00000000000000.\" 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, WXXX, 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. \fBFRAMES\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 \-p\fP,\fB \-\-picture\fB= Set the attached picture (APIC). Everything except the filename can be omitted in which case default values will be used. .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 has the format ; 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\&. The description can be omitted in which case it defaults to an empty string. .sp The WXXX frame has the same format as TXXX but since URLs usually contain a ":" you have provide a description or enable escaping (\-e): \fB\-\-WXXX "desc:http://foo.bar"\fP or \fB\-e \-\-WXXX "http\e\e://foo.bar"\fP .sp The USLT frame has the format . The language and description may be omitted, in which case the language defaults to English, and the description to an empty string. .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. . ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/man/moggsplit.10000644000175000017500000000323400000000000016335 0ustar00lazkalazka00000000000000.\" 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. . ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/man/mutagen-inspect.10000644000175000017500000000215500000000000017434 0ustar00lazkalazka00000000000000.\" 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. . ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/man/mutagen-pony.10000644000175000017500000000214000000000000016746 0ustar00lazkalazka00000000000000.\" 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. . ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1596184724.8191056 mutagen-1.45.1/mutagen/0000775000175000017500000000000000000000000015133 5ustar00lazkalazka00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594483675.0 mutagen-1.45.1/mutagen/__init__.py0000664000175000017500000000203700000000000017246 0ustar00lazkalazka00000000000000# -*- 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 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. """Mutagen aims to be an all purpose multimedia tagging library. :: import mutagen.[format] metadata = mutagen.[format].Open(filename) ``metadata`` acts like a dictionary of tags in the file. Tags are generally a list of string-like values, but may have additional methods available depending on tag or format. They may also be entirely different objects for certain keys, again depending on format. """ from mutagen._util import MutagenError from mutagen._file import FileType, StreamInfo, File from mutagen._tags import Tags, Metadata, PaddingInfo version = (1, 45, 1) """Version tuple.""" version_string = ".".join(map(str, version)) """Version string.""" MutagenError FileType StreamInfo File Tags Metadata PaddingInfo ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/mutagen/_constants.py0000644000175000017500000000707300000000000017665 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # # 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. """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.""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/_file.py0000664000175000017500000002124600000000000016570 0ustar00lazkalazka00000000000000# -*- 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 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. import warnings from mutagen._util import DictMixin, loadfile class FileType(DictMixin): """FileType(filething, **kwargs) Args: filething (filething): A filename or a file-like object Subclasses might take further options via keyword arguments. An abstract object wrapping tags and audio stream information. 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. Attributes: info (`StreamInfo`): contains length, bitrate, sample rate tags (`Tags`): metadata tags, if any, otherwise `None` """ __module__ = "mutagen" info = None tags = None filename = None _mimes = ["application/octet-stream"] def __init__(self, *args, **kwargs): if not args and not kwargs: warnings.warn("FileType constructor requires a filename", DeprecationWarning) else: self.load(*args, **kwargs) @loadfile() def load(self, filething, *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() @loadfile(writable=True) def delete(self, filething=None): """delete(filething=None) Remove tags from a file. In cases where the tagging format is independent of the file type (for example `mutagen.id3.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: return self.tags.delete(filething) @loadfile(writable=True) def save(self, filething=None, **kwargs): """save(filething=None, **kwargs) Save metadata tags. Raises: MutagenError: if saving wasn't possible """ if self.tags is not None: return self.tags.save(filething, **kwargs) def pprint(self): """ Returns: text: 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 (:class:`mutagen.text`)""" 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): """Returns a score for how likely the file can be parsed by this type. Args: filename (fspath): a file path fileobj (fileobj): a file object open in rb mode. Position is undefined header (bytes): data of undefined length, starts with the start of the file. Returns: int: negative if definitely not a matching type, otherwise a score, the bigger the more certain that the file can be loaded. """ 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): """ Returns: text: Print stream information """ raise NotImplementedError @loadfile(method=False) def File(filething, options=None, easy=False): """File(filething, 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. Args: filething (filething) options: Sequence of :class:`FileType` implementations, defaults to all included ones. easy (bool): If the easy wrappers should be returned if available. For example :class:`EasyMP3 ` instead of :class:`MP3 `. Returns: FileType: A FileType instance for the detected type or `None` in case the type couldn't be determined. Raises: MutagenError: in case the detected type fails to load the file. """ 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 from mutagen.ac3 import AC3 from mutagen.smf import SMF from mutagen.tak import TAK from mutagen.dsf import DSF from mutagen.dsdiff import DSDIFF from mutagen.wave import WAVE options = [MP3, TrueAudio, OggTheora, OggSpeex, OggVorbis, OggFLAC, FLAC, AIFF, APEv2File, MP4, ID3FileType, WavPack, Musepack, MonkeysAudio, OptimFROG, ASF, OggOpus, AAC, AC3, SMF, TAK, DSF, DSDIFF, WAVE] if not options: return None fileobj = filething.fileobj try: header = fileobj.read(128) except IOError: header = b"" # Sort by name after score. Otherwise import order affects # Kind sort order, which affects treatment of things with # equals scores. results = [(Kind.score(filething.name, fileobj, header), Kind.__name__) for Kind in options] results = list(zip(results, options)) results.sort() (score, name), Kind = results[-1] if score > 0: try: fileobj.seek(0, 0) except IOError: pass return Kind(fileobj, filename=filething.filename) else: return None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/_iff.py0000664000175000017500000002753100000000000016420 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2014 Evan Purkhiser # 2014 Ben Ockmore # 2017 Borewit # 2019-2020 Philipp Wolfer # # 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. """Base classes for various IFF based formats (e.g. AIFF or RIFF).""" import sys from mutagen.id3 import ID3 from mutagen.id3._util import ID3NoHeaderError, error as ID3Error from mutagen._util import ( MutagenError, convert_error, delete_bytes, insert_bytes, loadfile, reraise, resize_bytes, ) class error(MutagenError): pass class InvalidChunk(error): pass class EmptyChunk(InvalidChunk): pass def is_valid_chunk_id(id): """ is_valid_chunk_id(FOURCC) Arguments: id (FOURCC) Returns: true if valid; otherwise false Check if argument id is valid FOURCC type. """ assert isinstance(id, str), \ 'id is of type %s, must be str: %r' % (type(id), id) return ((0 < len(id) <= 4) and (min(id) >= ' ') and (max(id) <= '~')) # Assert FOURCC formatted valid def assert_valid_chunk_id(id): if not is_valid_chunk_id(id): raise ValueError("IFF chunk ID must be four ASCII characters.") class IffChunk(object): """Generic representation of a single IFF chunk. IFF chunks always consist of an ID followed by the chunk size. The exact format varies between different IFF based formats, e.g. AIFF uses big-endian while RIFF uses little-endian. """ # Chunk headers are usually 8 bytes long (4 for ID and 4 for the size) HEADER_SIZE = 8 @classmethod def parse_header(cls, header): """Read ID and data_size from the given header. Must be implemented in subclasses.""" raise error("Not implemented") def write_new_header(self, id_, size): """Write the chunk header with id_ and size to the file. Must be implemented in subclasses. The data must be written to the current position in self._fileobj.""" raise error("Not implemented") def write_size(self): """Write self.data_size to the file. Must be implemented in subclasses. The data must be written to the current position in self._fileobj.""" raise error("Not implemented") @classmethod def get_class(cls, id): """Returns the class for a new chunk for a given ID. Can be overridden in subclasses to implement specific chunk types.""" return cls @classmethod def parse(cls, fileobj, parent_chunk=None): header = fileobj.read(cls.HEADER_SIZE) if len(header) < cls.HEADER_SIZE: raise EmptyChunk('Header size < %i' % cls.HEADER_SIZE) id, data_size = cls.parse_header(header) try: id = id.decode('ascii').rstrip() except UnicodeDecodeError as e: raise InvalidChunk(e) if not is_valid_chunk_id(id): raise InvalidChunk('Invalid chunk ID %r' % id) return cls.get_class(id)(fileobj, id, data_size, parent_chunk) def __init__(self, fileobj, id, data_size, parent_chunk): self._fileobj = fileobj self.id = id self.data_size = data_size self.parent_chunk = parent_chunk self.data_offset = fileobj.tell() self.offset = self.data_offset - self.HEADER_SIZE self._calculate_size() def __repr__(self): return ("<%s id=%s, offset=%i, size=%i, data_offset=%i, data_size=%i>" % (type(self).__name__, self.id, self.offset, self.size, self.data_offset, self.data_size)) 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) # Write the padding bytes padding = self.padding() if padding: self._fileobj.seek(self.data_offset + self.data_size) self._fileobj.write(b'\x00' * padding) 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._remove_subchunk(self) self._fileobj.flush() def _update_size(self, size_diff, changed_subchunk=None): """Update the size of the chunk""" old_size = self.size self.data_size += size_diff self._fileobj.seek(self.offset + 4) self.write_size() self._calculate_size() if self.parent_chunk is not None: self.parent_chunk._update_size(self.size - old_size, self) if changed_subchunk: self._update_sibling_offsets( changed_subchunk, old_size - self.size) def _calculate_size(self): self.size = self.HEADER_SIZE + self.data_size + self.padding() assert self.size % 2 == 0 def resize(self, new_data_size): """Resize the file and update the chunk sizes""" padding = new_data_size % 2 resize_bytes(self._fileobj, self.data_size + self.padding(), new_data_size + padding, self.data_offset) size_diff = new_data_size - self.data_size self._update_size(size_diff) self._fileobj.flush() def padding(self): """Returns the number of padding bytes (0 or 1). IFF chunks are required to be a even number in total length. If data_size is odd a padding byte will be added at the end. """ return self.data_size % 2 class IffContainerChunkMixin(): """A IFF chunk containing other chunks. A container chunk can have an additional name as the first 4 bytes of the chunk data followed by an arbitrary number of subchunks. The root chunk of the file is always a container chunk (e.g. the AIFF chunk or the FORM chunk for RIFF) but there can be other types of container chunks (e.g. the LIST chunks used in RIFF). """ def parse_next_subchunk(self): """""" raise error("Not implemented") def init_container(self, name_size=4): # Lists can store an additional name identifier before the subchunks self.__name_size = name_size if self.data_size < name_size: raise InvalidChunk( 'Container chunk data size < %i' % name_size) # Read the container name if name_size > 0: try: self.name = self._fileobj.read(name_size).decode('ascii') except UnicodeDecodeError as e: raise error(e) else: self.name = None # Load all IFF subchunks self.__subchunks = [] def subchunks(self): """Returns a list of all subchunks. The list is lazily loaded on first access. """ if not self.__subchunks: next_offset = self.data_offset + self.__name_size while next_offset < self.offset + self.size: self._fileobj.seek(next_offset) try: chunk = self.parse_next_subchunk() except EmptyChunk: break except InvalidChunk: break self.__subchunks.append(chunk) # Calculate the location of the next chunk next_offset = chunk.offset + chunk.size return self.__subchunks def insert_chunk(self, id_, data=None): """Insert a new chunk at the end of the container chunk""" if not is_valid_chunk_id(id_): raise KeyError("Invalid IFF key.") next_offset = self.offset + self.size size = self.HEADER_SIZE data_size = 0 if data: data_size = len(data) padding = data_size % 2 size += data_size + padding insert_bytes(self._fileobj, size, next_offset) self._fileobj.seek(next_offset) self.write_new_header(id_.ljust(4).encode('ascii'), data_size) self._fileobj.seek(next_offset) chunk = self.parse_next_subchunk() self._update_size(chunk.size) if data: chunk.write(data) self.subchunks().append(chunk) self._fileobj.flush() return chunk def __contains__(self, id_): """Check if this chunk contains a specific subchunk.""" assert_valid_chunk_id(id_) try: self[id_] return True except KeyError: return False def __getitem__(self, id_): """Get a subchunk by ID.""" assert_valid_chunk_id(id_) found_chunk = None for chunk in self.subchunks(): if chunk.id == id_: found_chunk = chunk break else: raise KeyError("No %r chunk found" % id_) return found_chunk def __delitem__(self, id_): """Remove a chunk from the IFF file""" assert_valid_chunk_id(id_) self[id_].delete() def _remove_subchunk(self, chunk): assert chunk in self.__subchunks self._update_size(-chunk.size, chunk) self.__subchunks.remove(chunk) def _update_sibling_offsets(self, changed_subchunk, size_diff): """Update the offsets of subchunks after `changed_subchunk`. """ index = self.__subchunks.index(changed_subchunk) sibling_chunks = self.__subchunks[index + 1:len(self.__subchunks)] for sibling in sibling_chunks: sibling.offset -= size_diff sibling.data_offset -= size_diff class IffFile: """Representation of a IFF file""" def __init__(self, chunk_cls, fileobj): fileobj.seek(0) self.root = chunk_cls.parse(fileobj) def __contains__(self, id_): """Check if the IFF file contains a specific chunk""" return id_ in self.root def __getitem__(self, id_): """Get a chunk from the IFF file""" return self.root[id_] def __delitem__(self, id_): """Remove a chunk from the IFF file""" self.delete_chunk(id_) def delete_chunk(self, id_): """Remove a chunk from the IFF file""" del self.root[id_] def insert_chunk(self, id_, data=None): """Insert a new chunk at the end of the IFF file""" return self.root.insert_chunk(id_, data) class IffID3(ID3): """A generic IFF file with ID3v2 tags""" def _load_file(self, fileobj): raise error("Not implemented") def _pre_load_header(self, fileobj): try: fileobj.seek(self._load_file(fileobj)['ID3'].data_offset) except (InvalidChunk, KeyError): raise ID3NoHeaderError("No ID3 chunk") @convert_error(IOError, error) @loadfile(writable=True) def save(self, filething=None, v2_version=4, v23_sep='/', padding=None): """Save ID3v2 data to the IFF file""" fileobj = filething.fileobj iff_file = self._load_file(fileobj) if 'ID3' not in iff_file: iff_file.insert_chunk('ID3') chunk = iff_file['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]) chunk.resize(len(data)) chunk.write(data) @convert_error(IOError, error) @loadfile(writable=True) def delete(self, filething=None): """Completely removes the ID3 chunk from the IFF file""" try: iff_file = self._load_file(filething.fileobj) del iff_file['ID3'] except KeyError: pass self.clear() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/_riff.py0000664000175000017500000000353700000000000016602 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2017 Borewit # Copyright (C) 2019-2020 Philipp Wolfer # # 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. """Resource Interchange File Format (RIFF).""" import struct from struct import pack from mutagen._iff import ( IffChunk, IffContainerChunkMixin, IffFile, InvalidChunk, ) class RiffChunk(IffChunk): """Generic RIFF chunk""" @classmethod def parse_header(cls, header): return struct.unpack('<4sI', header) @classmethod def get_class(cls, id): if id in (u'LIST', u'RIFF'): return RiffListChunk else: return cls def write_new_header(self, id_, size): self._fileobj.write(pack('<4sI', id_, size)) def write_size(self): self._fileobj.write(pack(' None: ... _fsnative = Union[fsnative, _base] if sys.platform == "win32": _bytes_default_encoding = str else: _bytes_default_encoding = Optional[str] def path2fsn(path: _pathlike) -> _fsnative: ... def fsn2text(path: _fsnative, strict: bool=False) -> Text: ... def text2fsn(text: Text) -> _fsnative: ... def fsn2bytes(path: _fsnative, encoding: _bytes_default_encoding="utf-8") -> bytes: ... def bytes2fsn(data: bytes, encoding: _bytes_default_encoding="utf-8") -> _fsnative: ... def uri2fsn(uri: _uri) -> _fsnative: ... def fsn2uri(path: _fsnative) -> Text: ... def fsn2norm(path: _fsnative) -> _fsnative: ... sep: _fsnative pathsep: _fsnative curdir: _fsnative pardir: _fsnative altsep: _fsnative extsep: _fsnative devnull: _fsnative defpath: _fsnative def getcwd() -> _fsnative: ... def getenv(key: _pathlike, value: Optional[_fsnative]=None) -> Optional[_fsnative]: ... def putenv(key: _pathlike, value: _pathlike): ... def unsetenv(key: _pathlike) -> None: ... def supports_ansi_escape_codes(fd: int) -> bool: ... def expandvars(path: _pathlike) -> _fsnative: ... def expanduser(path: _pathlike) -> _fsnative: ... environ: Dict[_fsnative,_fsnative] argv: List[_fsnative] def gettempdir() -> _fsnative: pass def mkstemp(suffix: Optional[_pathlike]=None, prefix: Optional[_pathlike]=None, dir: Optional[_pathlike]=None, text: bool=False) -> Tuple[int, _fsnative]: ... def mkdtemp(suffix: Optional[_pathlike]=None, prefix: Optional[_pathlike]=None, dir: Optional[_pathlike]=None) -> _fsnative: ... version_string: str version: Tuple[int, int, int] print_ = print def input_(prompt: Any=None) -> _fsnative: ... ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/_senf/_argv.py0000664000175000017500000000653100000000000017702 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2016 Christoph Reiter # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import sys import ctypes try: from collections import abc except ImportError: import collections as abc # type: ignore from functools import total_ordering from ._compat import PY2, string_types from ._fsnative import is_win, _fsn2legacy, path2fsn from . import _winapi as winapi def _get_win_argv(): """Returns a unicode argv under Windows and standard sys.argv otherwise Returns: List[`fsnative`] """ assert is_win argc = ctypes.c_int() try: argv = winapi.CommandLineToArgvW( winapi.GetCommandLineW(), ctypes.byref(argc)) except WindowsError: return [] if not argv: return [] res = argv[max(0, argc.value - len(sys.argv)):argc.value] winapi.LocalFree(argv) return res @total_ordering class Argv(abc.MutableSequence): """List[`fsnative`]: Like `sys.argv` but contains unicode keys and values under Windows + Python 2. Any changes made will be forwarded to `sys.argv`. """ def __init__(self): if PY2 and is_win: self._argv = _get_win_argv() else: self._argv = sys.argv def __getitem__(self, index): return self._argv[index] def __setitem__(self, index, value): if isinstance(value, string_types): value = path2fsn(value) self._argv[index] = value if sys.argv is not self._argv: try: if isinstance(value, string_types): sys.argv[index] = _fsn2legacy(value) else: sys.argv[index] = [_fsn2legacy(path2fsn(v)) for v in value] except IndexError: pass def __delitem__(self, index): del self._argv[index] try: del sys.argv[index] except IndexError: pass def __eq__(self, other): return self._argv == other def __lt__(self, other): return self._argv < other def __len__(self): return len(self._argv) def __repr__(self): return repr(self._argv) def insert(self, index, value): value = path2fsn(value) self._argv.insert(index, value) if sys.argv is not self._argv: sys.argv.insert(index, _fsn2legacy(value)) argv = Argv() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573386527.0 mutagen-1.45.1/mutagen/_senf/_compat.py0000644000175000017500000000347400000000000020227 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2016 Christoph Reiter # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import sys PY2 = sys.version_info[0] == 2 PY3 = not PY2 if PY2: from urlparse import urlparse, urlunparse urlparse, urlunparse from urllib import quote, unquote quote, unquote from StringIO import StringIO BytesIO = StringIO from io import StringIO as TextIO TextIO string_types = (str, unicode) text_type = unicode iteritems = lambda d: d.iteritems() elif PY3: from urllib.parse import urlparse, quote, unquote, urlunparse urlparse, quote, unquote, urlunparse from io import StringIO StringIO = StringIO TextIO = StringIO from io import BytesIO BytesIO = BytesIO string_types = (str,) text_type = str iteritems = lambda d: iter(d.items()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/_senf/_environ.py0000664000175000017500000001530700000000000020424 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2016 Christoph Reiter # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import os import ctypes try: from collections import abc except ImportError: import collections as abc # type: ignore from ._compat import text_type, PY2 from ._fsnative import path2fsn, is_win, _fsn2legacy, fsnative from . import _winapi as winapi def get_windows_env_var(key): """Get an env var. Raises: WindowsError """ if not isinstance(key, text_type): raise TypeError("%r not of type %r" % (key, text_type)) buf = ctypes.create_unicode_buffer(32767) stored = winapi.GetEnvironmentVariableW(key, buf, 32767) if stored == 0: raise ctypes.WinError() return buf[:stored] def set_windows_env_var(key, value): """Set an env var. Raises: WindowsError """ if not isinstance(key, text_type): raise TypeError("%r not of type %r" % (key, text_type)) if not isinstance(value, text_type): raise TypeError("%r not of type %r" % (value, text_type)) status = winapi.SetEnvironmentVariableW(key, value) if status == 0: raise ctypes.WinError() def del_windows_env_var(key): """Delete an env var. Raises: WindowsError """ if not isinstance(key, text_type): raise TypeError("%r not of type %r" % (key, text_type)) status = winapi.SetEnvironmentVariableW(key, None) if status == 0: raise ctypes.WinError() def read_windows_environ(): """Returns a unicode dict of the Windows environment. Raises: WindowsEnvironError """ res = winapi.GetEnvironmentStringsW() if not res: raise ctypes.WinError() res = ctypes.cast(res, ctypes.POINTER(ctypes.c_wchar)) done = [] current = u"" i = 0 while 1: c = res[i] i += 1 if c == u"\x00": if not current: break done.append(current) current = u"" continue current += c dict_ = {} for entry in done: try: key, value = entry.split(u"=", 1) except ValueError: continue key = _norm_key(key) dict_[key] = value status = winapi.FreeEnvironmentStringsW(res) if status == 0: raise ctypes.WinError() return dict_ def _norm_key(key): assert isinstance(key, fsnative) if is_win: key = key.upper() return key class Environ(abc.MutableMapping): """Dict[`fsnative`, `fsnative`]: Like `os.environ` but contains unicode keys and values under Windows + Python 2. Any changes made will be forwarded to `os.environ`. """ def __init__(self): if is_win and PY2: try: env = read_windows_environ() except WindowsError: env = {} else: env = os.environ self._env = env def __getitem__(self, key): key = _norm_key(path2fsn(key)) return self._env[key] def __setitem__(self, key, value): key = _norm_key(path2fsn(key)) value = path2fsn(value) if is_win and PY2: # this calls putenv, so do it first and replace later try: os.environ[_fsn2legacy(key)] = _fsn2legacy(value) except OSError: raise ValueError try: set_windows_env_var(key, value) except WindowsError: # py3+win fails for invalid keys. try to do the same raise ValueError try: self._env[key] = value except OSError: raise ValueError def __delitem__(self, key): key = _norm_key(path2fsn(key)) if is_win and PY2: try: del_windows_env_var(key) except WindowsError: pass try: del os.environ[_fsn2legacy(key)] except KeyError: pass del self._env[key] def __iter__(self): return iter(self._env) def __len__(self): return len(self._env) def __repr__(self): return repr(self._env) def copy(self): return self._env.copy() environ = Environ() def getenv(key, value=None): """Like `os.getenv` but returns unicode under Windows + Python 2 Args: key (pathlike): The env var to get value (object): The value to return if the env var does not exist Returns: `fsnative` or `object`: The env var or the passed value if it doesn't exist """ key = path2fsn(key) if is_win and PY2: return environ.get(key, value) return os.getenv(key, value) def unsetenv(key): """Like `os.unsetenv` but takes unicode under Windows + Python 2 Args: key (pathlike): The env var to unset """ key = path2fsn(key) if is_win: # python 3 has no unsetenv under Windows -> use our ctypes one as well try: del_windows_env_var(key) except WindowsError: pass else: os.unsetenv(key) def putenv(key, value): """Like `os.putenv` but takes unicode under Windows + Python 2 Args: key (pathlike): The env var to get value (pathlike): The value to set Raises: ValueError """ key = path2fsn(key) value = path2fsn(value) if is_win and PY2: try: set_windows_env_var(key, value) except WindowsError: # py3 + win fails here raise ValueError else: try: os.putenv(key, value) except OSError: # win + py3 raise here for invalid keys which is probably a bug. # ValueError seems better raise ValueError ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/_senf/_fsnative.py0000664000175000017500000004455000000000000020565 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2016 Christoph Reiter # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import os import sys import ctypes import codecs from . import _winapi as winapi from ._compat import text_type, PY3, PY2, urlparse, quote, unquote, urlunparse is_win = os.name == "nt" is_unix = not is_win is_darwin = sys.platform == "darwin" _surrogatepass = "strict" if PY2 else "surrogatepass" def _normalize_codec(codec, _cache={}): """Raises LookupError""" try: return _cache[codec] except KeyError: _cache[codec] = codecs.lookup(codec).name return _cache[codec] def _swap_bytes(data): """swaps bytes for 16 bit, leaves remaining trailing bytes alone""" a, b = data[1::2], data[::2] data = bytearray().join(bytearray(x) for x in zip(a, b)) if len(b) > len(a): data += b[-1:] return bytes(data) def _decode_surrogatepass(data, codec): """Like data.decode(codec, 'surrogatepass') but makes utf-16-le/be work on Python 2. https://bugs.python.org/issue27971 Raises UnicodeDecodeError, LookupError """ try: return data.decode(codec, _surrogatepass) except UnicodeDecodeError: if PY2: if _normalize_codec(codec) == "utf-16-be": data = _swap_bytes(data) codec = "utf-16-le" if _normalize_codec(codec) == "utf-16-le": buffer_ = ctypes.create_string_buffer(data + b"\x00\x00") value = ctypes.wstring_at(buffer_, len(data) // 2) if value.encode("utf-16-le", _surrogatepass) != data: raise return value else: raise else: raise def _merge_surrogates(text): """Returns a copy of the text with all surrogate pairs merged""" return _decode_surrogatepass( text.encode("utf-16-le", _surrogatepass), "utf-16-le") def fsn2norm(path): """ Args: path (fsnative): The path to normalize Returns: `fsnative` Normalizes an fsnative path. The same underlying path can have multiple representations as fsnative (due to surrogate pairs and variable length encodings). When concatenating fsnative the result might be different than concatenating the serialized form and then deserializing it. This returns the normalized form i.e. the form which os.listdir() would return. This is useful when you alter fsnative but require that the same underlying path always maps to the same fsnative value. All functions like :func:`bytes2fsn`, :func:`fsnative`, :func:`text2fsn` and :func:`path2fsn` always return a normalized path, independent of their input. """ native = _fsn2native(path) if is_win: return _merge_surrogates(native) elif PY3: return bytes2fsn(native, None) else: return path def _fsn2legacy(path): """Takes a fsnative path and returns a path that can be put into os.environ or sys.argv. Might result in a mangled path on Python2 + Windows. Can't fail. Args: path (fsnative) Returns: str """ if PY2 and is_win: return path.encode(_encoding, "replace") return path def _fsnative(text): if not isinstance(text, text_type): raise TypeError("%r needs to be a text type (%r)" % (text, text_type)) if is_unix: # First we go to bytes so we can be sure we have a valid source. # Theoretically we should fail here in case we have a non-unicode # encoding. But this would make everything complicated and there is # no good way to handle a failure from the user side. Instead # fall back to utf-8 which is the most likely the right choice in # a mis-configured environment encoding = _encoding try: path = text.encode(encoding, _surrogatepass) except UnicodeEncodeError: path = text.encode("utf-8", _surrogatepass) if b"\x00" in path: path = path.replace(b"\x00", fsn2bytes(_fsnative(u"\uFFFD"), None)) if PY3: return path.decode(_encoding, "surrogateescape") return path else: if u"\x00" in text: text = text.replace(u"\x00", u"\uFFFD") text = fsn2norm(text) return text def _create_fsnative(type_): # a bit of magic to make fsnative(u"foo") and isinstance(path, fsnative) # work class meta(type): def __instancecheck__(self, instance): return _typecheck_fsnative(instance) def __subclasscheck__(self, subclass): return issubclass(subclass, type_) class impl(object): """fsnative(text=u"") Args: text (text): The text to convert to a path Returns: fsnative: The new path. Raises: TypeError: In case something other then `text` has been passed This type is a virtual base class for the real path type. Instantiating it returns an instance of the real path type and it overrides instance and subclass checks so that `isinstance` and `issubclass` checks work: :: isinstance(fsnative(u"foo"), fsnative) == True issubclass(type(fsnative(u"foo")), fsnative) == True The real returned type is: - **Python 2 + Windows:** :obj:`python:unicode`, with ``surrogates``, without ``null`` - **Python 2 + Unix:** :obj:`python:str`, without ``null`` - **Python 3 + Windows:** :obj:`python3:str`, with ``surrogates``, without ``null`` - **Python 3 + Unix:** :obj:`python3:str`, with ``surrogates``, without ``null``, without code points not encodable with the locale encoding Constructing a `fsnative` can't fail. Passing a `fsnative` to :func:`open` will never lead to `ValueError` or `TypeError`. Any operation on `fsnative` can also use the `str` type, as long as the `str` only contains ASCII and no NULL. """ def __new__(cls, text=u""): return _fsnative(text) new_type = meta("fsnative", (object,), dict(impl.__dict__)) new_type.__module__ = "senf" return new_type fsnative_type = text_type if is_win or PY3 else bytes fsnative = _create_fsnative(fsnative_type) def _typecheck_fsnative(path): """ Args: path (object) Returns: bool: if path is a fsnative """ if not isinstance(path, fsnative_type): return False if PY3 or is_win: if u"\x00" in path: return False if is_unix: try: path.encode(_encoding, "surrogateescape") except UnicodeEncodeError: return False elif b"\x00" in path: return False return True def _fsn2native(path): """ Args: path (fsnative) Returns: `text` on Windows, `bytes` on Unix Raises: TypeError: in case the type is wrong or the ´str` on Py3 + Unix can't be converted to `bytes` This helper allows to validate the type and content of a path. To reduce overhead the encoded value for Py3 + Unix is returned so it can be reused. """ if not isinstance(path, fsnative_type): raise TypeError("path needs to be %s, not %s" % ( fsnative_type.__name__, type(path).__name__)) if is_unix: if PY3: try: path = path.encode(_encoding, "surrogateescape") except UnicodeEncodeError: # This look more like ValueError, but raising only one error # makes things simpler... also one could say str + surrogates # is its own type raise TypeError( "path contained Unicode code points not valid in" "the current path encoding. To create a valid " "path from Unicode use text2fsn()") if b"\x00" in path: raise TypeError("fsnative can't contain nulls") else: if u"\x00" in path: raise TypeError("fsnative can't contain nulls") return path def _get_encoding(): """The encoding used for paths, argv, environ, stdout and stdin""" encoding = sys.getfilesystemencoding() if encoding is None: if is_darwin: encoding = "utf-8" elif is_win: encoding = "mbcs" else: encoding = "ascii" encoding = _normalize_codec(encoding) return encoding _encoding = _get_encoding() def path2fsn(path): """ Args: path (pathlike): The path to convert Returns: `fsnative` Raises: TypeError: In case the type can't be converted to a `fsnative` ValueError: In case conversion fails Returns a `fsnative` path for a `pathlike`. """ # allow mbcs str on py2+win and bytes on py3 if PY2: if is_win: if isinstance(path, bytes): path = path.decode(_encoding) else: if isinstance(path, text_type): path = path.encode(_encoding) if "\x00" in path: raise ValueError("embedded null") else: path = getattr(os, "fspath", lambda x: x)(path) if isinstance(path, bytes): if b"\x00" in path: raise ValueError("embedded null") path = path.decode(_encoding, "surrogateescape") elif is_unix and isinstance(path, str): # make sure we can encode it and this is not just some random # unicode string data = path.encode(_encoding, "surrogateescape") if b"\x00" in data: raise ValueError("embedded null") path = fsn2norm(path) else: if u"\x00" in path: raise ValueError("embedded null") path = fsn2norm(path) if not isinstance(path, fsnative_type): raise TypeError("path needs to be %s", fsnative_type.__name__) return path def fsn2text(path, strict=False): """ Args: path (fsnative): The path to convert strict (bool): Fail in case the conversion is not reversible Returns: `text` Raises: TypeError: In case no `fsnative` has been passed ValueError: In case ``strict`` was True and the conversion failed Converts a `fsnative` path to `text`. Can be used to pass a path to some unicode API, like for example a GUI toolkit. If ``strict`` is True the conversion will fail in case it is not reversible. This can be useful for converting program arguments that are supposed to be text and erroring out in case they are not. Encoding with a Unicode encoding will always succeed with the result. """ path = _fsn2native(path) errors = "strict" if strict else "replace" if is_win: return path.encode("utf-16-le", _surrogatepass).decode("utf-16-le", errors) else: return path.decode(_encoding, errors) def text2fsn(text): """ Args: text (text): The text to convert Returns: `fsnative` Raises: TypeError: In case no `text` has been passed Takes `text` and converts it to a `fsnative`. This operation is not reversible and can't fail. """ return fsnative(text) def fsn2bytes(path, encoding="utf-8"): """ Args: path (fsnative): The path to convert encoding (`str`): encoding used for Windows Returns: `bytes` Raises: TypeError: If no `fsnative` path is passed ValueError: If encoding fails or the encoding is invalid Converts a `fsnative` path to `bytes`. The passed *encoding* is only used on platforms where paths are not associated with an encoding (Windows for example). For Windows paths, lone surrogates will be encoded like normal code points and surrogate pairs will be merged before encoding. In case of ``utf-8`` or ``utf-16-le`` this is equal to the `WTF-8 and WTF-16 encoding `__. """ path = _fsn2native(path) if is_win: if encoding is None: raise ValueError("invalid encoding %r" % encoding) if PY2: try: return path.encode(encoding) except LookupError: raise ValueError("invalid encoding %r" % encoding) else: try: return path.encode(encoding) except LookupError: raise ValueError("invalid encoding %r" % encoding) except UnicodeEncodeError: # Fallback implementation for text including surrogates # merge surrogate codepoints if _normalize_codec(encoding).startswith("utf-16"): # fast path, utf-16 merges anyway return path.encode(encoding, _surrogatepass) return _merge_surrogates(path).encode(encoding, _surrogatepass) else: return path def bytes2fsn(data, encoding="utf-8"): """ Args: data (bytes): The data to convert encoding (`str`): encoding used for Windows Returns: `fsnative` Raises: TypeError: If no `bytes` path is passed ValueError: If decoding fails or the encoding is invalid Turns `bytes` to a `fsnative` path. The passed *encoding* is only used on platforms where paths are not associated with an encoding (Windows for example). For Windows paths ``WTF-8`` is accepted if ``utf-8`` is used and ``WTF-16`` accepted if ``utf-16-le`` is used. """ if not isinstance(data, bytes): raise TypeError("data needs to be bytes") if is_win: if encoding is None: raise ValueError("invalid encoding %r" % encoding) try: path = _decode_surrogatepass(data, encoding) except LookupError: raise ValueError("invalid encoding %r" % encoding) if u"\x00" in path: raise ValueError("contains nulls") return path else: if b"\x00" in data: raise ValueError("contains nulls") if PY2: return data else: return data.decode(_encoding, "surrogateescape") def uri2fsn(uri): """ Args: uri (`text` or :obj:`python:str`): A file URI Returns: `fsnative` Raises: TypeError: In case an invalid type is passed ValueError: In case the URI isn't a valid file URI Takes a file URI and returns a `fsnative` path """ if PY2: if isinstance(uri, text_type): uri = uri.encode("utf-8") if not isinstance(uri, bytes): raise TypeError("uri needs to be ascii str or unicode") else: if not isinstance(uri, str): raise TypeError("uri needs to be str") parsed = urlparse(uri) scheme = parsed.scheme netloc = parsed.netloc path = parsed.path if scheme != "file": raise ValueError("Not a file URI: %r" % uri) if not path: raise ValueError("Invalid file URI: %r" % uri) uri = urlunparse(parsed)[7:] if is_win: try: drive, rest = uri.split(":", 1) except ValueError: path = "" rest = uri.replace("/", "\\") else: path = drive[-1] + ":" rest = rest.replace("/", "\\") if PY2: path += unquote(rest) else: path += unquote(rest, encoding="utf-8", errors="surrogatepass") if netloc: path = "\\\\" + path if PY2: path = path.decode("utf-8") if u"\x00" in path: raise ValueError("embedded null") return path else: if PY2: path = unquote(uri) else: path = unquote(uri, encoding=_encoding, errors="surrogateescape") if "\x00" in path: raise ValueError("embedded null") return path def fsn2uri(path): """ Args: path (fsnative): The path to convert to an URI Returns: `text`: An ASCII only URI Raises: TypeError: If no `fsnative` was passed ValueError: If the path can't be converted Takes a `fsnative` path and returns a file URI. On Windows non-ASCII characters will be encoded using utf-8 and then percent encoded. """ path = _fsn2native(path) def _quote_path(path): # RFC 2396 path = quote(path, "/:@&=+$,") if PY2: path = path.decode("ascii") return path if is_win: buf = ctypes.create_unicode_buffer(winapi.INTERNET_MAX_URL_LENGTH) length = winapi.DWORD(winapi.INTERNET_MAX_URL_LENGTH) flags = 0 try: winapi.UrlCreateFromPathW(path, buf, ctypes.byref(length), flags) except WindowsError as e: raise ValueError(e) uri = buf[:length.value] # https://bitbucket.org/pypy/pypy/issues/3133 uri = _merge_surrogates(uri) # For some reason UrlCreateFromPathW escapes some chars outside of # ASCII and some not. Unquote and re-quote with utf-8. if PY3: # latin-1 maps code points directly to bytes, which is what we want uri = unquote(uri, "latin-1") else: # Python 2 does what we want by default uri = unquote(uri) return _quote_path(uri.encode("utf-8", _surrogatepass)) else: return u"file://" + _quote_path(path) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573386527.0 mutagen-1.45.1/mutagen/_senf/_print.py0000644000175000017500000003011600000000000020071 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2016 Christoph Reiter # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import sys import os import ctypes import re from ._fsnative import _encoding, is_win, is_unix, _surrogatepass, bytes2fsn from ._compat import text_type, PY2, PY3 from ._winansi import AnsiState, ansi_split from . import _winapi as winapi def print_(*objects, **kwargs): """print_(*objects, sep=None, end=None, file=None, flush=False) Args: objects (object): zero or more objects to print sep (str): Object separator to use, defaults to ``" "`` end (str): Trailing string to use, defaults to ``"\\n"``. If end is ``"\\n"`` then `os.linesep` is used. file (object): A file-like object, defaults to `sys.stdout` flush (bool): If the file stream should be flushed Raises: EnvironmentError Like print(), but: * Supports printing filenames under Unix + Python 3 and Windows + Python 2 * Emulates ANSI escape sequence support under Windows * Never fails due to encoding/decoding errors. Tries hard to get everything on screen as is, but will fall back to "?" if all fails. This does not conflict with ``colorama``, but will not use it on Windows. """ sep = kwargs.get("sep") sep = sep if sep is not None else " " end = kwargs.get("end") end = end if end is not None else "\n" file = kwargs.get("file") file = file if file is not None else sys.stdout flush = bool(kwargs.get("flush", False)) if is_win: _print_windows(objects, sep, end, file, flush) else: _print_unix(objects, sep, end, file, flush) def _print_unix(objects, sep, end, file, flush): """A print_() implementation which writes bytes""" encoding = _encoding if isinstance(sep, text_type): sep = sep.encode(encoding, "replace") if not isinstance(sep, bytes): raise TypeError if isinstance(end, text_type): end = end.encode(encoding, "replace") if not isinstance(end, bytes): raise TypeError if end == b"\n": end = os.linesep if PY3: end = end.encode("ascii") parts = [] for obj in objects: if not isinstance(obj, text_type) and not isinstance(obj, bytes): obj = text_type(obj) if isinstance(obj, text_type): if PY2: obj = obj.encode(encoding, "replace") else: try: obj = obj.encode(encoding, "surrogateescape") except UnicodeEncodeError: obj = obj.encode(encoding, "replace") assert isinstance(obj, bytes) parts.append(obj) data = sep.join(parts) + end assert isinstance(data, bytes) file = getattr(file, "buffer", file) try: file.write(data) except TypeError: if PY3: # For StringIO, first try with surrogates surr_data = data.decode(encoding, "surrogateescape") try: file.write(surr_data) except (TypeError, ValueError): file.write(data.decode(encoding, "replace")) else: # for file like objects with don't support bytes file.write(data.decode(encoding, "replace")) if flush: file.flush() ansi_state = AnsiState() def _print_windows(objects, sep, end, file, flush): """The windows implementation of print_()""" h = winapi.INVALID_HANDLE_VALUE try: fileno = file.fileno() except (EnvironmentError, AttributeError): pass else: if fileno == 1: h = winapi.GetStdHandle(winapi.STD_OUTPUT_HANDLE) elif fileno == 2: h = winapi.GetStdHandle(winapi.STD_ERROR_HANDLE) encoding = _encoding parts = [] for obj in objects: if isinstance(obj, bytes): obj = obj.decode(encoding, "replace") if not isinstance(obj, text_type): obj = text_type(obj) parts.append(obj) if isinstance(sep, bytes): sep = sep.decode(encoding, "replace") if not isinstance(sep, text_type): raise TypeError if isinstance(end, bytes): end = end.decode(encoding, "replace") if not isinstance(end, text_type): raise TypeError if end == u"\n": end = os.linesep text = sep.join(parts) + end assert isinstance(text, text_type) is_console = True if h == winapi.INVALID_HANDLE_VALUE: is_console = False else: # get the default value info = winapi.CONSOLE_SCREEN_BUFFER_INFO() if not winapi.GetConsoleScreenBufferInfo(h, ctypes.byref(info)): is_console = False if is_console: # make sure we flush before we apply any console attributes file.flush() # try to force a utf-8 code page, use the output CP if that fails cp = winapi.GetConsoleOutputCP() try: encoding = "utf-8" if winapi.SetConsoleOutputCP(65001) == 0: encoding = None for is_ansi, part in ansi_split(text): if is_ansi: ansi_state.apply(h, part) else: if encoding is not None: data = part.encode(encoding, _surrogatepass) else: data = _encode_codepage(cp, part) os.write(fileno, data) finally: # reset the code page to what we had before winapi.SetConsoleOutputCP(cp) else: # try writing bytes first, so in case of Python 2 StringIO we get # the same type on all platforms try: file.write(text.encode("utf-8", _surrogatepass)) except (TypeError, ValueError): file.write(text) if flush: file.flush() def _readline_windows(): """Raises OSError""" try: fileno = sys.stdin.fileno() except (EnvironmentError, AttributeError): fileno = -1 # In case stdin is replaced, read from that if fileno != 0: return _readline_windows_fallback() h = winapi.GetStdHandle(winapi.STD_INPUT_HANDLE) if h == winapi.INVALID_HANDLE_VALUE: return _readline_windows_fallback() buf_size = 1024 buf = ctypes.create_string_buffer(buf_size * ctypes.sizeof(winapi.WCHAR)) read = winapi.DWORD() text = u"" while True: if winapi.ReadConsoleW( h, buf, buf_size, ctypes.byref(read), None) == 0: if not text: return _readline_windows_fallback() raise ctypes.WinError() data = buf[:read.value * ctypes.sizeof(winapi.WCHAR)] text += data.decode("utf-16-le", _surrogatepass) if text.endswith(u"\r\n"): return text[:-2] def _decode_codepage(codepage, data): """ Args: codepage (int) data (bytes) Returns: `text` Decodes data using the given codepage. If some data can't be decoded using the codepage it will not fail. """ assert isinstance(data, bytes) if not data: return u"" # get the required buffer length first length = winapi.MultiByteToWideChar(codepage, 0, data, len(data), None, 0) if length == 0: raise ctypes.WinError() # now decode buf = ctypes.create_unicode_buffer(length) length = winapi.MultiByteToWideChar( codepage, 0, data, len(data), buf, length) if length == 0: raise ctypes.WinError() return buf[:] def _encode_codepage(codepage, text): """ Args: codepage (int) text (text) Returns: `bytes` Encode text using the given code page. Will not fail if a char can't be encoded using that codepage. """ assert isinstance(text, text_type) if not text: return b"" size = (len(text.encode("utf-16-le", _surrogatepass)) // ctypes.sizeof(winapi.WCHAR)) # get the required buffer size length = winapi.WideCharToMultiByte( codepage, 0, text, size, None, 0, None, None) if length == 0: raise ctypes.WinError() # decode to the buffer buf = ctypes.create_string_buffer(length) length = winapi.WideCharToMultiByte( codepage, 0, text, size, buf, length, None, None) if length == 0: raise ctypes.WinError() return buf[:length] def _readline_windows_fallback(): # In case reading from the console failed (maybe we get piped data) # we assume the input was generated according to the output encoding. # Got any better ideas? assert is_win cp = winapi.GetConsoleOutputCP() data = getattr(sys.stdin, "buffer", sys.stdin).readline().rstrip(b"\r\n") return _decode_codepage(cp, data) def _readline_default(): assert is_unix data = getattr(sys.stdin, "buffer", sys.stdin).readline().rstrip(b"\r\n") if PY3: return data.decode(_encoding, "surrogateescape") else: return data def _readline(): if is_win: return _readline_windows() else: return _readline_default() def input_(prompt=None): """ Args: prompt (object): Prints the passed object to stdout without adding a trailing newline Returns: `fsnative` Raises: EnvironmentError Like :func:`python3:input` but returns a `fsnative` and allows printing filenames as prompt to stdout. Use :func:`fsn2text` on the result if you just want to deal with text. """ if prompt is not None: print_(prompt, end="") return _readline() def _get_file_name_for_handle(handle): """(Windows only) Returns a file name for a file handle. Args: handle (winapi.HANDLE) Returns: `text` or `None` if no file name could be retrieved. """ assert is_win assert handle != winapi.INVALID_HANDLE_VALUE size = winapi.FILE_NAME_INFO.FileName.offset + \ winapi.MAX_PATH * ctypes.sizeof(winapi.WCHAR) buf = ctypes.create_string_buffer(size) if winapi.GetFileInformationByHandleEx is None: # Windows XP return None status = winapi.GetFileInformationByHandleEx( handle, winapi.FileNameInfo, buf, size) if status == 0: return None name_info = ctypes.cast( buf, ctypes.POINTER(winapi.FILE_NAME_INFO)).contents offset = winapi.FILE_NAME_INFO.FileName.offset data = buf[offset:offset + name_info.FileNameLength] return bytes2fsn(data, "utf-16-le") def supports_ansi_escape_codes(fd): """Returns whether the output device is capable of interpreting ANSI escape codes when :func:`print_` is used. Args: fd (int): file descriptor (e.g. ``sys.stdout.fileno()``) Returns: `bool` """ if os.isatty(fd): return True if not is_win: return False # Check for cygwin/msys terminal handle = winapi._get_osfhandle(fd) if handle == winapi.INVALID_HANDLE_VALUE: return False if winapi.GetFileType(handle) != winapi.FILE_TYPE_PIPE: return False file_name = _get_file_name_for_handle(handle) match = re.match( "^\\\\(cygwin|msys)-[a-z0-9]+-pty[0-9]+-(from|to)-master$", file_name) return match is not None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1583513766.0 mutagen-1.45.1/mutagen/_senf/_stdlib.py0000644000175000017500000001056200000000000020221 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2016 Christoph Reiter # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import re import os from ._fsnative import path2fsn, fsnative, is_win from ._compat import PY2 from ._environ import environ sep = path2fsn(os.sep) pathsep = path2fsn(os.pathsep) curdir = path2fsn(os.curdir) pardir = path2fsn(os.pardir) altsep = path2fsn(os.altsep) if os.altsep is not None else None extsep = path2fsn(os.extsep) devnull = path2fsn(os.devnull) defpath = path2fsn(os.defpath) def getcwd(): """Like `os.getcwd` but returns a `fsnative` path Returns: `fsnative` """ if is_win and PY2: return os.getcwdu() return os.getcwd() def _get_userdir(user=None): """Returns the user dir or None""" if user is not None and not isinstance(user, fsnative): raise TypeError if is_win: if "HOME" in environ: path = environ["HOME"] elif "USERPROFILE" in environ: path = environ["USERPROFILE"] elif "HOMEPATH" in environ and "HOMEDRIVE" in environ: path = os.path.join(environ["HOMEDRIVE"], environ["HOMEPATH"]) else: return if user is None: return path else: return os.path.join(os.path.dirname(path), user) else: import pwd if user is None: if "HOME" in environ: return environ["HOME"] else: try: return path2fsn(pwd.getpwuid(os.getuid()).pw_dir) except KeyError: return else: try: return path2fsn(pwd.getpwnam(user).pw_dir) except KeyError: return def expanduser(path): """ Args: path (pathlike): A path to expand Returns: `fsnative` Like :func:`python:os.path.expanduser` but supports unicode home directories under Windows + Python 2 and always returns a `fsnative`. """ path = path2fsn(path) if path == "~": return _get_userdir() elif path.startswith("~" + sep) or ( altsep is not None and path.startswith("~" + altsep)): userdir = _get_userdir() if userdir is None: return path return userdir + path[1:] elif path.startswith("~"): sep_index = path.find(sep) if altsep is not None: alt_index = path.find(altsep) if alt_index != -1 and alt_index < sep_index: sep_index = alt_index if sep_index == -1: user = path[1:] rest = "" else: user = path[1:sep_index] rest = path[sep_index:] userdir = _get_userdir(user) if userdir is not None: return userdir + rest else: return path else: return path def expandvars(path): """ Args: path (pathlike): A path to expand Returns: `fsnative` Like :func:`python:os.path.expandvars` but supports unicode under Windows + Python 2 and always returns a `fsnative`. """ path = path2fsn(path) def repl_func(match): return environ.get(match.group(1), match.group(0)) path = re.compile(r"\$(\w+)", flags=re.UNICODE).sub(repl_func, path) if os.name == "nt": path = re.sub(r"%([^%]+)%", repl_func, path) return re.sub(r"\$\{([^\}]+)\}", repl_func, path) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573386527.0 mutagen-1.45.1/mutagen/_senf/_temp.py0000644000175000017500000000632200000000000017704 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2016 Christoph Reiter # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import tempfile from ._fsnative import path2fsn, fsnative def gettempdir(): """ Returns: `fsnative` Like :func:`python3:tempfile.gettempdir`, but always returns a `fsnative` path """ # FIXME: I don't want to reimplement all that logic, reading env vars etc. # At least for the default it works. return path2fsn(tempfile.gettempdir()) def gettempprefix(): """ Returns: `fsnative` Like :func:`python3:tempfile.gettempprefix`, but always returns a `fsnative` path """ return path2fsn(tempfile.gettempprefix()) def mkstemp(suffix=None, prefix=None, dir=None, text=False): """ Args: suffix (`pathlike` or `None`): suffix or `None` to use the default prefix (`pathlike` or `None`): prefix or `None` to use the default dir (`pathlike` or `None`): temp dir or `None` to use the default text (bool): if the file should be opened in text mode Returns: Tuple[`int`, `fsnative`]: A tuple containing the file descriptor and the file path Raises: EnvironmentError Like :func:`python3:tempfile.mkstemp` but always returns a `fsnative` path. """ suffix = fsnative() if suffix is None else path2fsn(suffix) prefix = gettempprefix() if prefix is None else path2fsn(prefix) dir = gettempdir() if dir is None else path2fsn(dir) return tempfile.mkstemp(suffix, prefix, dir, text) def mkdtemp(suffix=None, prefix=None, dir=None): """ Args: suffix (`pathlike` or `None`): suffix or `None` to use the default prefix (`pathlike` or `None`): prefix or `None` to use the default dir (`pathlike` or `None`): temp dir or `None` to use the default Returns: `fsnative`: A path to a directory Raises: EnvironmentError Like :func:`python3:tempfile.mkstemp` but always returns a `fsnative` path. """ suffix = fsnative() if suffix is None else path2fsn(suffix) prefix = gettempprefix() if prefix is None else path2fsn(prefix) dir = gettempdir() if dir is None else path2fsn(dir) return tempfile.mkdtemp(suffix, prefix, dir) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1573386527.0 mutagen-1.45.1/mutagen/_senf/_winansi.py0000644000175000017500000002454700000000000020420 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2016 Christoph Reiter # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import ctypes import re import atexit from . import _winapi as winapi def ansi_parse(code): """Returns command, (args)""" return code[-1:], tuple([int(v or "0") for v in code[2:-1].split(";")]) def ansi_split(text, _re=re.compile(u"(\x1b\\[(\\d*;?)*\\S)")): """Yields (is_ansi, text)""" for part in _re.split(text): if part: yield (bool(_re.match(part)), part) class AnsiCommand(object): TEXT = "m" MOVE_UP = "A" MOVE_DOWN = "B" MOVE_FORWARD = "C" MOVE_BACKWARD = "D" SET_POS = "H" SET_POS_ALT = "f" SAVE_POS = "s" RESTORE_POS = "u" class TextAction(object): RESET_ALL = 0 SET_BOLD = 1 SET_DIM = 2 SET_ITALIC = 3 SET_UNDERLINE = 4 SET_BLINK = 5 SET_BLINK_FAST = 6 SET_REVERSE = 7 SET_HIDDEN = 8 RESET_BOLD = 21 RESET_DIM = 22 RESET_ITALIC = 23 RESET_UNDERLINE = 24 RESET_BLINK = 25 RESET_BLINK_FAST = 26 RESET_REVERSE = 27 RESET_HIDDEN = 28 FG_BLACK = 30 FG_RED = 31 FG_GREEN = 32 FG_YELLOW = 33 FG_BLUE = 34 FG_MAGENTA = 35 FG_CYAN = 36 FG_WHITE = 37 FG_DEFAULT = 39 FG_LIGHT_BLACK = 90 FG_LIGHT_RED = 91 FG_LIGHT_GREEN = 92 FG_LIGHT_YELLOW = 93 FG_LIGHT_BLUE = 94 FG_LIGHT_MAGENTA = 95 FG_LIGHT_CYAN = 96 FG_LIGHT_WHITE = 97 BG_BLACK = 40 BG_RED = 41 BG_GREEN = 42 BG_YELLOW = 43 BG_BLUE = 44 BG_MAGENTA = 45 BG_CYAN = 46 BG_WHITE = 47 BG_DEFAULT = 49 BG_LIGHT_BLACK = 100 BG_LIGHT_RED = 101 BG_LIGHT_GREEN = 102 BG_LIGHT_YELLOW = 103 BG_LIGHT_BLUE = 104 BG_LIGHT_MAGENTA = 105 BG_LIGHT_CYAN = 106 BG_LIGHT_WHITE = 107 class AnsiState(object): def __init__(self): self.default_attrs = None self.bold = False self.bg_light = False self.fg_light = False self.saved_pos = (0, 0) def do_text_action(self, attrs, action): # In case the external state has changed, apply it it to ours. # Mostly the first time this is called. if attrs & winapi.FOREGROUND_INTENSITY and not self.fg_light \ and not self.bold: self.fg_light = True if attrs & winapi.BACKGROUND_INTENSITY and not self.bg_light: self.bg_light = True dark_fg = { TextAction.FG_BLACK: 0, TextAction.FG_RED: winapi.FOREGROUND_RED, TextAction.FG_GREEN: winapi.FOREGROUND_GREEN, TextAction.FG_YELLOW: winapi.FOREGROUND_GREEN | winapi.FOREGROUND_RED, TextAction.FG_BLUE: winapi.FOREGROUND_BLUE, TextAction.FG_MAGENTA: winapi.FOREGROUND_BLUE | winapi.FOREGROUND_RED, TextAction.FG_CYAN: winapi.FOREGROUND_BLUE | winapi.FOREGROUND_GREEN, TextAction.FG_WHITE: winapi.FOREGROUND_BLUE | winapi.FOREGROUND_GREEN | winapi.FOREGROUND_RED, } dark_bg = { TextAction.BG_BLACK: 0, TextAction.BG_RED: winapi.BACKGROUND_RED, TextAction.BG_GREEN: winapi.BACKGROUND_GREEN, TextAction.BG_YELLOW: winapi.BACKGROUND_GREEN | winapi.BACKGROUND_RED, TextAction.BG_BLUE: winapi.BACKGROUND_BLUE, TextAction.BG_MAGENTA: winapi.BACKGROUND_BLUE | winapi.BACKGROUND_RED, TextAction.BG_CYAN: winapi.BACKGROUND_BLUE | winapi.BACKGROUND_GREEN, TextAction.BG_WHITE: winapi.BACKGROUND_BLUE | winapi.BACKGROUND_GREEN | winapi.BACKGROUND_RED, } light_fg = { TextAction.FG_LIGHT_BLACK: 0, TextAction.FG_LIGHT_RED: winapi.FOREGROUND_RED, TextAction.FG_LIGHT_GREEN: winapi.FOREGROUND_GREEN, TextAction.FG_LIGHT_YELLOW: winapi.FOREGROUND_GREEN | winapi.FOREGROUND_RED, TextAction.FG_LIGHT_BLUE: winapi.FOREGROUND_BLUE, TextAction.FG_LIGHT_MAGENTA: winapi.FOREGROUND_BLUE | winapi.FOREGROUND_RED, TextAction.FG_LIGHT_CYAN: winapi.FOREGROUND_BLUE | winapi.FOREGROUND_GREEN, TextAction.FG_LIGHT_WHITE: winapi.FOREGROUND_BLUE | winapi.FOREGROUND_GREEN | winapi.FOREGROUND_RED, } light_bg = { TextAction.BG_LIGHT_BLACK: 0, TextAction.BG_LIGHT_RED: winapi.BACKGROUND_RED, TextAction.BG_LIGHT_GREEN: winapi.BACKGROUND_GREEN, TextAction.BG_LIGHT_YELLOW: winapi.BACKGROUND_GREEN | winapi.BACKGROUND_RED, TextAction.BG_LIGHT_BLUE: winapi.BACKGROUND_BLUE, TextAction.BG_LIGHT_MAGENTA: winapi.BACKGROUND_BLUE | winapi.BACKGROUND_RED, TextAction.BG_LIGHT_CYAN: winapi.BACKGROUND_BLUE | winapi.BACKGROUND_GREEN, TextAction.BG_LIGHT_WHITE: winapi.BACKGROUND_BLUE | winapi.BACKGROUND_GREEN | winapi.BACKGROUND_RED, } if action == TextAction.RESET_ALL: attrs = self.default_attrs self.bold = self.fg_light = self.bg_light = False elif action == TextAction.SET_BOLD: self.bold = True elif action == TextAction.RESET_BOLD: self.bold = False elif action == TextAction.SET_DIM: self.bold = False elif action == TextAction.SET_REVERSE: attrs |= winapi.COMMON_LVB_REVERSE_VIDEO elif action == TextAction.RESET_REVERSE: attrs &= ~winapi.COMMON_LVB_REVERSE_VIDEO elif action == TextAction.SET_UNDERLINE: attrs |= winapi.COMMON_LVB_UNDERSCORE elif action == TextAction.RESET_UNDERLINE: attrs &= ~winapi.COMMON_LVB_UNDERSCORE elif action == TextAction.FG_DEFAULT: attrs = (attrs & ~0xF) | (self.default_attrs & 0xF) self.fg_light = False elif action == TextAction.BG_DEFAULT: attrs = (attrs & ~0xF0) | (self.default_attrs & 0xF0) self.bg_light = False elif action in dark_fg: attrs = (attrs & ~0xF) | dark_fg[action] self.fg_light = False elif action in dark_bg: attrs = (attrs & ~0xF0) | dark_bg[action] self.bg_light = False elif action in light_fg: attrs = (attrs & ~0xF) | light_fg[action] self.fg_light = True elif action in light_bg: attrs = (attrs & ~0xF0) | light_bg[action] self.bg_light = True if self.fg_light or self.bold: attrs |= winapi.FOREGROUND_INTENSITY else: attrs &= ~winapi.FOREGROUND_INTENSITY if self.bg_light: attrs |= winapi.BACKGROUND_INTENSITY else: attrs &= ~winapi.BACKGROUND_INTENSITY return attrs def apply(self, handle, code): buffer_info = winapi.CONSOLE_SCREEN_BUFFER_INFO() if not winapi.GetConsoleScreenBufferInfo(handle, ctypes.byref(buffer_info)): return attrs = buffer_info.wAttributes # We take the first attrs we see as default if self.default_attrs is None: self.default_attrs = attrs # Make sure that like with linux terminals the program doesn't # affect the prompt after it exits atexit.register( winapi.SetConsoleTextAttribute, handle, self.default_attrs) cmd, args = ansi_parse(code) if cmd == AnsiCommand.TEXT: for action in args: attrs = self.do_text_action(attrs, action) winapi.SetConsoleTextAttribute(handle, attrs) elif cmd in (AnsiCommand.MOVE_UP, AnsiCommand.MOVE_DOWN, AnsiCommand.MOVE_FORWARD, AnsiCommand.MOVE_BACKWARD): coord = buffer_info.dwCursorPosition x, y = coord.X, coord.Y amount = max(args[0], 1) if cmd == AnsiCommand.MOVE_UP: y -= amount elif cmd == AnsiCommand.MOVE_DOWN: y += amount elif cmd == AnsiCommand.MOVE_FORWARD: x += amount elif cmd == AnsiCommand.MOVE_BACKWARD: x -= amount x = max(x, 0) y = max(y, 0) winapi.SetConsoleCursorPosition(handle, winapi.COORD(x, y)) elif cmd in (AnsiCommand.SET_POS, AnsiCommand.SET_POS_ALT): args = list(args) while len(args) < 2: args.append(0) x, y = args[:2] win_rect = buffer_info.srWindow x += win_rect.Left - 1 y += win_rect.Top - 1 x = max(x, 0) y = max(y, 0) winapi.SetConsoleCursorPosition(handle, winapi.COORD(x, y)) elif cmd == AnsiCommand.SAVE_POS: win_rect = buffer_info.srWindow coord = buffer_info.dwCursorPosition x, y = coord.X, coord.Y x -= win_rect.Left y -= win_rect.Top self.saved_pos = (x, y) elif cmd == AnsiCommand.RESTORE_POS: win_rect = buffer_info.srWindow x, y = self.saved_pos x += win_rect.Left y += win_rect.Top winapi.SetConsoleCursorPosition(handle, winapi.COORD(x, y)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/_senf/_winapi.py0000664000175000017500000001605000000000000020227 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2016 Christoph Reiter # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import sys import ctypes if sys.platform == 'win32': from ctypes import WinDLL, CDLL, wintypes shell32 = WinDLL("shell32") kernel32 = WinDLL("kernel32") shlwapi = WinDLL("shlwapi") msvcrt = CDLL("msvcrt") GetCommandLineW = kernel32.GetCommandLineW GetCommandLineW.argtypes = [] GetCommandLineW.restype = wintypes.LPCWSTR CommandLineToArgvW = shell32.CommandLineToArgvW CommandLineToArgvW.argtypes = [ wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_int)] CommandLineToArgvW.restype = ctypes.POINTER(wintypes.LPWSTR) LocalFree = kernel32.LocalFree LocalFree.argtypes = [wintypes.HLOCAL] LocalFree.restype = wintypes.HLOCAL # https://msdn.microsoft.com/en-us/library/windows/desktop/aa383751.aspx LPCTSTR = ctypes.c_wchar_p LPWSTR = wintypes.LPWSTR LPCWSTR = ctypes.c_wchar_p LPTSTR = LPWSTR PCWSTR = ctypes.c_wchar_p PCTSTR = PCWSTR PWSTR = ctypes.c_wchar_p PTSTR = PWSTR LPVOID = wintypes.LPVOID WCHAR = wintypes.WCHAR LPSTR = ctypes.c_char_p BOOL = wintypes.BOOL LPBOOL = ctypes.POINTER(BOOL) UINT = wintypes.UINT WORD = wintypes.WORD DWORD = wintypes.DWORD SHORT = wintypes.SHORT HANDLE = wintypes.HANDLE ULONG = wintypes.ULONG LPCSTR = wintypes.LPCSTR STD_INPUT_HANDLE = DWORD(-10) STD_OUTPUT_HANDLE = DWORD(-11) STD_ERROR_HANDLE = DWORD(-12) INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value INTERNET_MAX_SCHEME_LENGTH = 32 INTERNET_MAX_PATH_LENGTH = 2048 INTERNET_MAX_URL_LENGTH = ( INTERNET_MAX_SCHEME_LENGTH + len("://") + INTERNET_MAX_PATH_LENGTH) FOREGROUND_BLUE = 0x0001 FOREGROUND_GREEN = 0x0002 FOREGROUND_RED = 0x0004 FOREGROUND_INTENSITY = 0x0008 BACKGROUND_BLUE = 0x0010 BACKGROUND_GREEN = 0x0020 BACKGROUND_RED = 0x0040 BACKGROUND_INTENSITY = 0x0080 COMMON_LVB_REVERSE_VIDEO = 0x4000 COMMON_LVB_UNDERSCORE = 0x8000 UrlCreateFromPathW = shlwapi.UrlCreateFromPathW UrlCreateFromPathW.argtypes = [ PCTSTR, PTSTR, ctypes.POINTER(DWORD), DWORD] UrlCreateFromPathW.restype = ctypes.HRESULT SetEnvironmentVariableW = kernel32.SetEnvironmentVariableW SetEnvironmentVariableW.argtypes = [LPCTSTR, LPCTSTR] SetEnvironmentVariableW.restype = wintypes.BOOL GetEnvironmentVariableW = kernel32.GetEnvironmentVariableW GetEnvironmentVariableW.argtypes = [LPCTSTR, LPTSTR, DWORD] GetEnvironmentVariableW.restype = DWORD GetEnvironmentStringsW = kernel32.GetEnvironmentStringsW GetEnvironmentStringsW.argtypes = [] GetEnvironmentStringsW.restype = ctypes.c_void_p FreeEnvironmentStringsW = kernel32.FreeEnvironmentStringsW FreeEnvironmentStringsW.argtypes = [ctypes.c_void_p] FreeEnvironmentStringsW.restype = ctypes.c_bool GetStdHandle = kernel32.GetStdHandle GetStdHandle.argtypes = [DWORD] GetStdHandle.restype = HANDLE class COORD(ctypes.Structure): _fields_ = [ ("X", SHORT), ("Y", SHORT), ] class SMALL_RECT(ctypes.Structure): _fields_ = [ ("Left", SHORT), ("Top", SHORT), ("Right", SHORT), ("Bottom", SHORT), ] class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure): _fields_ = [ ("dwSize", COORD), ("dwCursorPosition", COORD), ("wAttributes", WORD), ("srWindow", SMALL_RECT), ("dwMaximumWindowSize", COORD), ] GetConsoleScreenBufferInfo = kernel32.GetConsoleScreenBufferInfo GetConsoleScreenBufferInfo.argtypes = [ HANDLE, ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)] GetConsoleScreenBufferInfo.restype = BOOL GetConsoleOutputCP = kernel32.GetConsoleOutputCP GetConsoleOutputCP.argtypes = [] GetConsoleOutputCP.restype = UINT SetConsoleOutputCP = kernel32.SetConsoleOutputCP SetConsoleOutputCP.argtypes = [UINT] SetConsoleOutputCP.restype = BOOL GetConsoleCP = kernel32.GetConsoleCP GetConsoleCP.argtypes = [] GetConsoleCP.restype = UINT SetConsoleCP = kernel32.SetConsoleCP SetConsoleCP.argtypes = [UINT] SetConsoleCP.restype = BOOL SetConsoleTextAttribute = kernel32.SetConsoleTextAttribute SetConsoleTextAttribute.argtypes = [HANDLE, WORD] SetConsoleTextAttribute.restype = BOOL SetConsoleCursorPosition = kernel32.SetConsoleCursorPosition SetConsoleCursorPosition.argtypes = [HANDLE, COORD] SetConsoleCursorPosition.restype = BOOL ReadConsoleW = kernel32.ReadConsoleW ReadConsoleW.argtypes = [ HANDLE, LPVOID, DWORD, ctypes.POINTER(DWORD), LPVOID] ReadConsoleW.restype = BOOL MultiByteToWideChar = kernel32.MultiByteToWideChar MultiByteToWideChar.argtypes = [ UINT, DWORD, LPCSTR, ctypes.c_int, LPWSTR, ctypes.c_int] MultiByteToWideChar.restype = ctypes.c_int WideCharToMultiByte = kernel32.WideCharToMultiByte WideCharToMultiByte.argtypes = [ UINT, DWORD, LPCWSTR, ctypes.c_int, LPSTR, ctypes.c_int, LPCSTR, LPBOOL] WideCharToMultiByte.restype = ctypes.c_int MoveFileW = kernel32.MoveFileW MoveFileW.argtypes = [LPCTSTR, LPCTSTR] MoveFileW.restype = BOOL GetFileInformationByHandleEx = None if hasattr(kernel32, "GetFileInformationByHandleEx"): GetFileInformationByHandleEx = kernel32.GetFileInformationByHandleEx GetFileInformationByHandleEx.argtypes = [ HANDLE, ctypes.c_int, ctypes.c_void_p, DWORD] GetFileInformationByHandleEx.restype = BOOL else: # Windows XP pass MAX_PATH = 260 FileNameInfo = 2 class FILE_NAME_INFO(ctypes.Structure): _fields_ = [ ("FileNameLength", DWORD), ("FileName", WCHAR), ] _get_osfhandle = msvcrt._get_osfhandle _get_osfhandle.argtypes = [ctypes.c_int] _get_osfhandle.restype = HANDLE GetFileType = kernel32.GetFileType GetFileType.argtypes = [HANDLE] GetFileType.restype = DWORD FILE_TYPE_PIPE = 0x0003 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/_senf/py.typed0000664000175000017500000000000000000000000017712 0ustar00lazkalazka00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/mutagen/_tags.py0000644000175000017500000000766400000000000016615 0ustar00lazkalazka00000000000000# -*- 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 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. from ._util import loadfile class PaddingInfo(object): """PaddingInfo() 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. Attributes: padding (`int`): The amount of padding left after saving in bytes (can be negative if more data needs to be added as padding is available) size (`int`): 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. Returns: int: Amount of padding after saving """ 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 Tags(object): """`Tags` is the base class for many of the tag objects in Mutagen. In many cases it has a dict like interface. """ __module__ = "mutagen" def pprint(self): """ Returns: text: tag information """ raise NotImplementedError class Metadata(Tags): """Metadata(filething=None, **kwargs) Args: filething (filething): a filename or a file-like object or `None` to create an empty instance (like ``ID3()``) Like :class:`Tags` but for standalone tagging formats that are not solely managed by a container format. Provides methods to load, save and delete tags. """ __module__ = "mutagen" def __init__(self, *args, **kwargs): if args or kwargs: self.load(*args, **kwargs) @loadfile() def load(self, filething, **kwargs): raise NotImplementedError @loadfile(writable=False) def save(self, filething=None, **kwargs): """save(filething=None, **kwargs) Save changes to a file. Args: filething (filething): or `None` Raises: MutagenError: if saving wasn't possible """ raise NotImplementedError @loadfile(writable=False) def delete(self, filething=None): """delete(filething=None) Remove tags from a file. In most cases this means any traces of the tag will be removed from the file. Args: filething (filething): or `None` Raises: MutagenError: if deleting wasn't possible """ raise NotImplementedError ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1596184724.8191056 mutagen-1.45.1/mutagen/_tools/0000775000175000017500000000000000000000000016432 5ustar00lazkalazka00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/mutagen/_tools/__init__.py0000644000175000017500000000046400000000000020545 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2016 Christoph Reiter # # 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/_tools/_util.py0000664000175000017500000000510500000000000020121 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. import os import signal import contextlib import optparse from mutagen._senf import print_ from mutagen._util import 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, str): 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...") class OptionParser(optparse.OptionParser): """OptionParser subclass which supports printing Unicode under Windows""" def print_help(self, file=None): print_(self.format_help(), file=file) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/_tools/mid3cp.py0000664000175000017500000001016400000000000020165 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2014 Marcus Sundman # # 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. """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._senf import print_, argv from ._util import SignalHandler, OptionParser 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, merge, 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 Exception as err: print_(str(err), file=sys.stderr) return 1 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 merge: try: target = mutagen.id3.ID3(dst, translate=False) except mutagen.id3.ID3NoHeaderError: # no need to merge pass except Exception as err: print_(str(err), file=sys.stderr) return 1 else: for frame in id3.values(): target.add(frame) id3 = target # 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 Exception as err: print_(u"Error saving", dst, u":\n%s" % str(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=[]) parser.add_option("--merge", action="store_true", help="Copy over frames instead of the whole ID3 tag", default=False) (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.merge, options.write_v1, excluded_tags, options.verbose) def entry_point(): _sig.init() return main(argv) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/_tools/mid3iconv.py0000664000175000017500000001216600000000000020705 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2006 Emfox Zhou # # 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. """ ID3iconv is a Java based ID3 encoding convertor, here's the Python version. """ import sys import locale import mutagen import mutagen.id3 from mutagen._senf import argv, print_, fsnative from ._util import SignalHandler, 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_(str(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] = fsnative(u"--force-v1") elif arg == "-removev1": argv[i] = fsnative(u"--remove-v1") (options, args) = parser.parse_args(argv[1:]) if args: update(options, args) else: parser.print_help() def entry_point(): _sig.init() return main(argv) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/_tools/mid3v2.py0000664000175000017500000004415600000000000020122 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """Pretend to be /usr/bin/id3v2 from id3lib, sort of.""" import sys import codecs import mimetypes import warnings from optparse import SUPPRESS_HELP import mutagen import mutagen.id3 from mutagen.id3 import Encoding, PictureType from mutagen._senf import fsnative, print_, argv, fsn2text, fsn2bytes, \ bytes2fsn from ._util import split_escape, SignalHandler, OptionParser 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_(str(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_(str(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 isinstance(arg, fsnative) text = fsn2text(arg, strict=True) return text.encode("ascii").decode("ascii") def value_from_fsnative(arg, escape): """Takes an item from argv and returns a str value without surrogate escapes or raises ValueError. """ assert isinstance(arg, fsnative) if escape: bytes_ = fsn2bytes(arg) # With py3.7 this has started to warn for invalid escapes, but we # don't control the input so ignore it. with warnings.catch_warnings(): warnings.simplefilter("ignore") bytes_ = codecs.escape_decode(bytes_)[0] arg = bytes2fsn(bytes_) text = fsn2text(arg, strict=True) return text def error(*args): print_(*args, file=sys.stderr) raise SystemExit(1) def get_frame_encoding(frame_id, value): if frame_id == "APIC": # See https://github.com/beetbox/beets/issues/899#issuecomment-62437773 return Encoding.UTF16 if value else Encoding.LATIN1 else: return Encoding.UTF8 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_(str(err), file=sys.stderr) assert isinstance(frame, str) # strip "--" frame = frame[2:] try: value = value_from_fsnative(value, escape) except ValueError as err: error(u"%s: %s" % (frame, str(err))) assert isinstance(value, str) 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 == "APIC": for value in vlist: values = string_split(value, ":") # FIXME: doesn't support filenames with an invalid # encoding since we have already decoded at that point fn = values[0] if len(values) >= 2: desc = values[1] else: desc = u"cover" if len(values) >= 3: try: picture_type = int(values[2]) except ValueError: error(u"Invalid picture type: %r" % values[1]) else: picture_type = PictureType.COVER_FRONT if len(values) >= 4: mime = values[3] else: mime = mimetypes.guess_type(fn)[0] or "image/jpeg" if len(values) >= 5: error("APIC: Invalid format") encoding = get_frame_encoding(frame, desc) try: with open(fn, "rb") as h: data = h.read() except IOError as e: error(str(e)) frame = mutagen.id3.APIC(encoding=encoding, mime=mime, desc=desc, type=picture_type, data=data) 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 == "USLT": 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.USLT( encoding=3, text=value, lang=lang, desc=desc) id3.add(frame) elif frame == "UFID": for value in vlist: values = string_split(value, ":") if len(values) != 2: error(u"Invalid value: %r" % values) owner = values[0] data = values[1].encode("utf-8") frame = mutagen.id3.UFID(owner=owner, data=data) 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 frame == "WXXX": 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.WXXX( encoding=3, url=value, desc=desc) id3.add(frame) elif issubclass(mutagen.id3.Frames[frame], mutagen.id3.UrlFrame): frame = mutagen.id3.Frames[frame]( encoding=3, url=vlist[-1]) 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_(str(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_(str(err), file=sys.stderr) raise SystemExit(1) else: for frame in id3.values(): print_(str(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( "-p", "--picture", metavar='"FILENAME":"DESCRIPTION":"IMAGE-TYPE":"MIME-TYPE"', action="callback", help="Set the picture", type="string", callback=lambda *args: args[3].edits.append((fsnative(u"--APIC"), 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 key, frame in mutagen.id3.Frames.items(): if (issubclass(frame, mutagen.id3.TextFrame) or issubclass(frame, mutagen.id3.UrlFrame) or issubclass(frame, mutagen.id3.POPM) or frame in (mutagen.id3.APIC, mutagen.id3.UFID, mutagen.id3.USLT)): parser.add_option( "--" + key, 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() def entry_point(): _sig.init() return main(argv) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/mutagen/_tools/moggsplit.py0000644000175000017500000000502400000000000021010 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """Split a multiplex/chained Ogg file into its component parts.""" import os import mutagen.ogg from mutagen._senf import argv from ._util import SignalHandler, 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] with open(filename, "rb") as fileobj: 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() def entry_point(): _sig.init() return main(argv) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/_tools/mutagen_inspect.py0000664000175000017500000000226700000000000022200 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """Full tag list for any given file.""" from mutagen._senf import print_, argv from ._util import SignalHandler, OptionParser _sig = SignalHandler() 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_(str(err)) print_(u"") def entry_point(): _sig.init() return main(argv) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/mutagen/_tools/mutagen_pony.py0000644000175000017500000000640400000000000021513 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2005 Joe Wreschnig, Michael Urman # # 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. import os import sys import traceback from mutagen._senf import print_, argv from ._util import SignalHandler 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) def entry_point(): SignalHandler().init() return main(argv) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1595060151.0 mutagen-1.45.1/mutagen/_util.py0000664000175000017500000007146000000000000016631 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """Utility classes for Mutagen. You should not rely on the interfaces here being stable. They are intended for internal use in Mutagen only. """ import sys import struct import codecs import errno import decimal from io import BytesIO from collections import namedtuple from contextlib import contextmanager from functools import wraps from fnmatch import fnmatchcase _DEFAULT_BUFFER_SIZE = 2 ** 18 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) def reraise(tp, value, tb): raise tp(value).with_traceback(tb) def bchr(x): return bytes([x]) def iterbytes(b): return (bytes([v]) for v in b) def intround(value): """Given a float returns a rounded int. Should give the same result on both Py2/3 """ return int(decimal.Decimal.from_float( value).to_integral_value(decimal.ROUND_HALF_EVEN)) def is_fileobj(fileobj): """Returns: bool: if an argument passed ot mutagen should be treated as a file object """ return not (isinstance(fileobj, (str, bytes)) or hasattr(fileobj, "__fspath__")) def verify_fileobj(fileobj, writable=False): """Verifies that the passed fileobj is a file like object which we can use. Args: writable (bool): verify that the file object is writable as well Raises: ValueError: In case the object is not a file object that is readable (or writable if required) or is not opened in bytes mode. """ try: data = fileobj.read(0) except Exception: if not hasattr(fileobj, "read"): raise ValueError("%r not a valid file object" % fileobj) raise ValueError("Can't read from file object %r" % fileobj) if not isinstance(data, bytes): raise ValueError( "file object %r not opened in binary mode" % fileobj) if writable: try: fileobj.write(b"") except Exception: if not hasattr(fileobj, "write"): raise ValueError("%r not a valid file object" % fileobj) raise ValueError("Can't write to file object %r" % fileobj) def verify_filename(filename): """Checks of the passed in filename has the correct type. Raises: ValueError: if not a filename """ if is_fileobj(filename): raise ValueError("%r not a filename" % filename) def fileobj_name(fileobj): """ Returns: text: A potential filename for a file object. Always a valid path type, but might be empty or non-existent. """ value = getattr(fileobj, "name", u"") if not isinstance(value, (str, bytes)): value = str(value) return value def loadfile(method=True, writable=False, create=False): """A decorator for functions taking a `filething` as a first argument. Passes a FileThing instance as the first argument to the wrapped function. Args: method (bool): If the wrapped functions is a method writable (bool): If a filename is passed opens the file readwrite, if passed a file object verifies that it is writable. create (bool): If passed a filename that does not exist will create a new empty file. """ def convert_file_args(args, kwargs): filething = args[0] if args else None filename = kwargs.pop("filename", None) fileobj = kwargs.pop("fileobj", None) return filething, filename, fileobj, args[1:], kwargs def wrap(func): @wraps(func) def wrapper(self, *args, **kwargs): filething, filename, fileobj, args, kwargs = \ convert_file_args(args, kwargs) with _openfile(self, filething, filename, fileobj, writable, create) as h: return func(self, h, *args, **kwargs) @wraps(func) def wrapper_func(*args, **kwargs): filething, filename, fileobj, args, kwargs = \ convert_file_args(args, kwargs) with _openfile(None, filething, filename, fileobj, writable, create) as h: return func(h, *args, **kwargs) return wrapper if method else wrapper_func return wrap def convert_error(exc_src, exc_dest): """A decorator for reraising exceptions with a different type. Mostly useful for IOError. Args: exc_src (type): The source exception type exc_dest (type): The target exception type. """ def wrap(func): @wraps(func) def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except exc_dest: raise except exc_src as err: reraise(exc_dest, err, sys.exc_info()[2]) return wrapper return wrap FileThing = namedtuple("FileThing", ["fileobj", "filename", "name"]) """filename is None if the source is not a filename. name is a filename which can be used for file type detection """ @contextmanager def _openfile(instance, filething, filename, fileobj, writable, create): """yields a FileThing Args: filething: Either a file name, a file object or None filename: Either a file name or None fileobj: Either a file object or None writable (bool): if the file should be opened create (bool): if the file should be created if it doesn't exist. implies writable Raises: MutagenError: In case opening the file failed TypeError: in case neither a file name or a file object is passed """ assert not create or writable # to allow stacked context managers, just pass the result through if isinstance(filething, FileThing): filename = filething.filename fileobj = filething.fileobj filething = None if filething is not None: if is_fileobj(filething): fileobj = filething elif hasattr(filething, "__fspath__"): filename = filething.__fspath__() if not isinstance(filename, (bytes, str)): raise TypeError("expected __fspath__() to return a filename") else: filename = filething if instance is not None: # XXX: take "not writable" as loading the file.. if not writable: instance.filename = filename elif filename is None: filename = getattr(instance, "filename", None) if fileobj is not None: verify_fileobj(fileobj, writable=writable) yield FileThing(fileobj, filename, filename or fileobj_name(fileobj)) elif filename is not None: verify_filename(filename) inmemory_fileobj = False try: fileobj = open(filename, "rb+" if writable else "rb") except IOError as e: if writable and e.errno == errno.EOPNOTSUPP: # Some file systems (gvfs over fuse) don't support opening # files read/write. To make things still work read the whole # file into an in-memory file like object and write it back # later. # https://github.com/quodlibet/mutagen/issues/300 try: with open(filename, "rb") as fileobj: fileobj = BytesIO(fileobj.read()) except IOError as e2: raise MutagenError(e2) inmemory_fileobj = True elif create and e.errno == errno.ENOENT: assert writable try: fileobj = open(filename, "wb+") except IOError as e2: raise MutagenError(e2) else: raise MutagenError(e) with fileobj as fileobj: yield FileThing(fileobj, filename, filename) if inmemory_fileobj: assert writable data = fileobj.getvalue() try: with open(filename, "wb") as fileobj: fileobj.write(data) except IOError as e: raise MutagenError(e) else: raise TypeError("Missing filename or fileobj argument") class MutagenError(Exception): """Base class for all custom exceptions in mutagen .. versionadded:: 1.25 """ __module__ = "mutagen" def total_ordering(cls): """Adds all possible ordering methods to a class. Needs a working __eq__ and __lt__ and will supply the rest. """ 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__. """ 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): """A decorator for creating an int enum class. Makes the values a subclass of the type and implements repr/str. The new class will be a subclass of int. Args: cls (type): The class to convert to an enum Returns: type: A new class :: @enum class Foo(object): FOO = 1 BAR = 2 """ 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 d.items(): if key.upper() == key and isinstance(value, int): 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 def flags(cls): """A decorator for creating an int flags class. Makes the values a subclass of the type and implements repr/str. The new class will be a subclass of int. Args: cls (type): The class to convert to an flags Returns: type: A new class :: @flags class Foo(object): FOO = 1 BAR = 2 """ 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 d.items(): if key.upper() == key and isinstance(value, int): value_instance = new_type(value) setattr(new_type, key, value_instance) map_[value] = key def str_(self): value = int(self) matches = [] for k, v in map_.items(): if value & k: matches.append("%s.%s" % (type(self).__name__, v)) value &= ~k if value != 0 or not matches: matches.append(str(value)) return " | ".join(matches) def repr_(self): return "<%s: %d>" % (str(self), 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 __contains__ = __has_key def values(self): return [self[k] for k in self.keys()] def items(self): return list(zip(self.keys(), self.values())) 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) if unsigned: max_ = 2 ** (s.size * 8) - 1 min_ = 0 else: max_ = 2 ** (s.size * 8 - 1) - 1 min_ = - 2 ** (s.size * 8 - 1) funcs["%s%s_min" % (prefix, name)] = min_ funcs["%s%s_max" % (prefix, name)] = max_ funcs["%sint%s_min" % (prefix, bits)] = min_ funcs["%sint%s_max" % (prefix, bits)] = max_ 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 funcs.items(): 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) """ error = struct.error bitswap = b''.join( bchr(sum(((val >> i) & 1) << (7 - i) for i in range(8))) for val in range(256)) test_bit = staticmethod(lambda value, n: bool((value >> n) & 1)) _fill_cdata(cdata) def get_size(fileobj): """Returns the size of the file. The position when passed in will be preserved if no error occurs. Args: fileobj (fileobj) Returns: int: The size of the file Raises: IOError """ old_pos = fileobj.tell() try: fileobj.seek(0, 2) return fileobj.tell() finally: fileobj.seek(old_pos, 0) def read_full(fileobj, size): """Like fileobj.read but raises IOError if not all requested data is returned. If you want to distinguish IOError and the EOS case, better handle the error yourself instead of using this. Args: fileobj (fileobj) size (int): amount of bytes to read Raises: IOError: In case read fails or not enough data is read """ if size < 0: raise ValueError("size must not be negative") data = fileobj.read(size) if len(data) != size: raise IOError return data def seek_end(fileobj, offset): """Like fileobj.seek(-offset, 2), but will not try to go beyond the start Needed since file objects from BytesIO will not raise IOError and file objects from open() will raise IOError if going to a negative offset. To make things easier for custom implementations, instead of allowing both behaviors, we just don't do it. Args: fileobj (fileobj) offset (int): how many bytes away from the end backwards to seek to Raises: IOError """ if offset < 0: raise ValueError if get_size(fileobj) < offset: fileobj.seek(0, 0) else: fileobj.seek(-offset, 2) def resize_file(fobj, diff, BUFFER_SIZE=_DEFAULT_BUFFER_SIZE): """Resize a file by `diff`. New space will be filled with zeros. Args: fobj (fileobj) diff (int): amount of size to change Raises: IOError """ fobj.seek(0, 2) filesize = fobj.tell() if diff < 0: if filesize + diff < 0: raise ValueError # truncate flushes internally fobj.truncate(filesize + diff) elif diff > 0: try: while diff: addsize = min(BUFFER_SIZE, diff) fobj.write(b"\x00" * addsize) diff -= addsize fobj.flush() except IOError as e: if e.errno == errno.ENOSPC: # To reduce the chance of corrupt files in case of missing # space try to revert the file expansion back. Of course # in reality every in-file-write can also fail due to COW etc. # Note: IOError gets also raised in flush() due to buffering fobj.truncate(filesize) raise def move_bytes(fobj, dest, src, count, BUFFER_SIZE=_DEFAULT_BUFFER_SIZE): """Moves data around using read()/write(). Args: fileobj (fileobj) dest (int): The destination offset src (int): The source offset count (int) The amount of data to move Raises: IOError: In case an operation on the fileobj fails ValueError: In case invalid parameters were given """ if dest < 0 or src < 0 or count < 0: raise ValueError fobj.seek(0, 2) filesize = fobj.tell() if max(dest, src) + count > filesize: raise ValueError("area outside of file") if src > dest: moved = 0 while count - moved: this_move = min(BUFFER_SIZE, count - moved) fobj.seek(src + moved) buf = fobj.read(this_move) fobj.seek(dest + moved) fobj.write(buf) moved += this_move fobj.flush() else: while count: this_move = min(BUFFER_SIZE, count) fobj.seek(src + count - this_move) buf = fobj.read(this_move) fobj.seek(count + dest - this_move) fobj.write(buf) count -= this_move fobj.flush() def insert_bytes(fobj, size, offset, BUFFER_SIZE=_DEFAULT_BUFFER_SIZE): """Insert size bytes of empty space starting at offset. fobj must be an open file object, open rb+ or equivalent. Args: fobj (fileobj) size (int): The amount of space to insert offset (int): The offset at which to insert the space Raises: IOError """ if size < 0 or offset < 0: raise ValueError fobj.seek(0, 2) filesize = fobj.tell() movesize = filesize - offset if movesize < 0: raise ValueError resize_file(fobj, size, BUFFER_SIZE) move_bytes(fobj, offset + size, offset, movesize, BUFFER_SIZE) def delete_bytes(fobj, size, offset, BUFFER_SIZE=_DEFAULT_BUFFER_SIZE): """Delete size bytes of empty space starting at offset. fobj must be an open file object, open rb+ or equivalent. Args: fobj (fileobj) size (int): The amount of space to delete offset (int): The start of the space to delete Raises: IOError """ if size < 0 or offset < 0: raise ValueError fobj.seek(0, 2) filesize = fobj.tell() movesize = filesize - offset - size if movesize < 0: raise ValueError move_bytes(fobj, offset, offset + size, movesize, BUFFER_SIZE) resize_file(fobj, -size, BUFFER_SIZE) 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. Args: fobj (fileobj) old_size (int): The area starting at offset new_size (int): The new size of the area offset (int): The start of the area Raises: IOError """ 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. Args: d (dict): A dict with filename patterns as keys key (str): A key potentially matching any of the keys default (object): The object to return if no pattern matched the passed in key Returns: object: The dict value where the dict key matched the passed in key. Or default if there was no match. """ if key in d and "[" not in key: return d[key] else: for pattern, value in d.items(): if fnmatchcase(key, pattern): return value return default def encode_endian(text, encoding, errors="strict", le=True): """Like text.encode(encoding) but always returns little endian/big endian BOMs instead of the system one. Args: text (text) encoding (str) errors (str) le (boolean): if little endian Returns: bytes Raises: UnicodeEncodeError LookupError """ encoding = codecs.lookup(encoding).name if encoding == "utf-16": if le: return codecs.BOM_UTF16_LE + text.encode("utf-16-le", errors) else: return codecs.BOM_UTF16_BE + text.encode("utf-16-be", errors) elif encoding == "utf-32": if le: return codecs.BOM_UTF32_LE + text.encode("utf-32-le", errors) else: return codecs.BOM_UTF32_BE + text.encode("utf-32-be", errors) else: return text.encode(encoding, errors) def decode_terminated(data, encoding, strict=True): """Returns the decoded data until the first NULL terminator and all data after it. Args: data (bytes): data to decode encoding (str): The codec to use strict (bool): If True will raise ValueError in case no NULL is found but the available data decoded successfully. Returns: Tuple[`text`, `bytes`]: A tuple containing the decoded text and the remaining data after the found NULL termination. Raises: UnicodeError: In case the data can't be decoded. LookupError:In case the encoding is not found. ValueError: In case the data isn't null terminated (even if it is encoded correctly) 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 range(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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/_vorbis.py0000664000175000017500000002243000000000000017151 0ustar00lazkalazka00000000000000# -*- 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 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. """Read and write Vorbis comment data. Vorbis comments are freeform key/value pairs; keys are case-insensitive ASCII and values are Unicode strings. A key may have multiple values. The specification is at http://www.xiph.org/vorbis/doc/v-comment.html. """ import sys from io import BytesIO import mutagen from mutagen._util import DictMixin, cdata, MutagenError, reraise 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 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(MutagenError): pass class VorbisUnsetFrameError(error): pass class VorbisEncodingError(error): pass class VComment(mutagen.Tags, 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 (text): 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. Arguments: errors (str): 'strict', 'replace', or 'ignore'. This affects Unicode decoding and how other malformed content is interpreted. framing (bool): 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 range(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: 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, str): raise ValueError("vendor needs to be str") for key, value in self: try: if not is_valid_key(key): raise ValueError("%r is not a valid key" % key) except TypeError: raise ValueError("%r is not a valid key" % key) if not isinstance(value, str): err = "%r needs to be str for key %r" % (value, key) raise ValueError(err) 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. Arguments: framing (bool): 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, str): 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. """ 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.""" 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. """ 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 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()]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/aac.py0000664000175000017500000002676500000000000016251 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """ * 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, loadfile, \ convert_error, endswith from mutagen.id3._util import BitPaddedInt _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 range(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): """AACInfo() AAC stream information. The length of the stream is just a guess and might not be correct. Attributes: channels (`int`): number of audio channels length (`float`): file length in seconds, as a float sample_rate (`int`): audio sampling rate in Hz bitrate (`int`): audio bitrate, in bits per second """ channels = 0 length = 0 sample_rate = 0 bitrate = 0 @convert_error(IOError, AACError) def __init__(self, fileobj): """Raises AACError""" # skip id3v2 header start_offset = 0 header = fileobj.read(10) 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 range(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 range(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 range(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 = 0.0 if s.frequency != 0: 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): """AAC(filething) Arguments: filething (filething) Load ADTS or ADIF streams containing AAC. Tagging is not supported. Use the ID3/APEv2 classes directly instead. Attributes: info (`AACInfo`) """ _mimes = ["audio/x-aac"] @loadfile() def load(self, filething): self.info = AACInfo(filething.fileobj) 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"] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/ac3.py0000664000175000017500000002432300000000000016157 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2019 Philipp Wolfer # # 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. """Pure AC3 file information. """ __all__ = ["AC3", "Open"] from mutagen import StreamInfo from mutagen._file import FileType from mutagen._util import ( BitReader, BitReaderError, MutagenError, convert_error, enum, loadfile, endswith, ) @enum class ChannelMode(object): DUALMONO = 0 MONO = 1 STEREO = 2 C3F = 3 C2F1R = 4 C3F1R = 5 C2F2R = 6 C3F2R = 7 AC3_CHANNELS = { ChannelMode.DUALMONO: 2, ChannelMode.MONO: 1, ChannelMode.STEREO: 2, ChannelMode.C3F: 3, ChannelMode.C2F1R: 3, ChannelMode.C3F1R: 4, ChannelMode.C2F2R: 4, ChannelMode.C3F2R: 5 } AC3_HEADER_SIZE = 7 AC3_SAMPLE_RATES = [48000, 44100, 32000] AC3_BITRATES = [ 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640 ] @enum class EAC3FrameType(object): INDEPENDENT = 0 DEPENDENT = 1 AC3_CONVERT = 2 RESERVED = 3 EAC3_BLOCKS = [1, 2, 3, 6] class AC3Error(MutagenError): pass class AC3Info(StreamInfo): """AC3 stream information. The length of the stream is just a guess and might not be correct. Attributes: channels (`int`): number of audio channels length (`float`): file length in seconds, as a float sample_rate (`int`): audio sampling rate in Hz bitrate (`int`): audio bitrate, in bits per second codec (`str`): ac-3 or ec-3 (Enhanced AC-3) """ channels = 0 length = 0 sample_rate = 0 bitrate = 0 codec = 'ac-3' @convert_error(IOError, AC3Error) def __init__(self, fileobj): """Raises AC3Error""" header = bytearray(fileobj.read(6)) if len(header) < 6: raise AC3Error("not enough data") if not header.startswith(b"\x0b\x77"): raise AC3Error("not a AC3 file") bitstream_id = header[5] >> 3 if bitstream_id > 16: raise AC3Error("invalid bitstream_id %i" % bitstream_id) fileobj.seek(2) self._read_header(fileobj, bitstream_id) def _read_header(self, fileobj, bitstream_id): bitreader = BitReader(fileobj) try: # This is partially based on code from # https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/ac3_parser.c if bitstream_id <= 10: # Normal AC-3 self._read_header_normal(bitreader, bitstream_id) else: # Enhanced AC-3 self._read_header_enhanced(bitreader) except BitReaderError as e: raise AC3Error(e) self.length = self._guess_length(fileobj) def _read_header_normal(self, bitreader, bitstream_id): r = bitreader r.skip(16) # 16 bit CRC sr_code = r.bits(2) if sr_code == 3: raise AC3Error("invalid sample rate code %i" % sr_code) frame_size_code = r.bits(6) if frame_size_code > 37: raise AC3Error("invalid frame size code %i" % frame_size_code) r.skip(5) # bitstream ID, already read r.skip(3) # bitstream mode, not needed channel_mode = ChannelMode(r.bits(3)) r.skip(2) # dolby surround mode or surround mix level lfe_on = r.bits(1) sr_shift = max(bitstream_id, 8) - 8 try: self.sample_rate = AC3_SAMPLE_RATES[sr_code] >> sr_shift self.bitrate = (AC3_BITRATES[frame_size_code >> 1] * 1000 ) >> sr_shift except KeyError as e: raise AC3Error(e) self.channels = self._get_channels(channel_mode, lfe_on) self._skip_unused_header_bits_normal(r, channel_mode) def _read_header_enhanced(self, bitreader): r = bitreader self.codec = "ec-3" frame_type = r.bits(2) if frame_type == EAC3FrameType.RESERVED: raise AC3Error("invalid frame type %i" % frame_type) r.skip(3) # substream ID, not needed frame_size = (r.bits(11) + 1) << 1 if frame_size < AC3_HEADER_SIZE: raise AC3Error("invalid frame size %i" % frame_size) sr_code = r.bits(2) try: if sr_code == 3: sr_code2 = r.bits(2) if sr_code2 == 3: raise AC3Error("invalid sample rate code %i" % sr_code2) numblocks_code = 3 self.sample_rate = AC3_SAMPLE_RATES[sr_code2] // 2 else: numblocks_code = r.bits(2) self.sample_rate = AC3_SAMPLE_RATES[sr_code] channel_mode = ChannelMode(r.bits(3)) lfe_on = r.bits(1) self.bitrate = 8 * frame_size * self.sample_rate // ( EAC3_BLOCKS[numblocks_code] * 256) except KeyError as e: raise AC3Error(e) r.skip(5) # bitstream ID, already read self.channels = self._get_channels(channel_mode, lfe_on) self._skip_unused_header_bits_enhanced( r, frame_type, channel_mode, sr_code, numblocks_code) @staticmethod def _skip_unused_header_bits_normal(bitreader, channel_mode): r = bitreader r.skip(5) # Dialogue Normalization if r.bits(1): # Compression Gain Word Exists r.skip(8) # Compression Gain Word if r.bits(1): # Language Code Exists r.skip(8) # Language Code if r.bits(1): # Audio Production Information Exists # Mixing Level, 5 Bits # Room Type, 2 Bits r.skip(7) if channel_mode == ChannelMode.DUALMONO: r.skip(5) # Dialogue Normalization, ch2 if r.bits(1): # Compression Gain Word Exists, ch2 r.skip(8) # Compression Gain Word, ch2 if r.bits(1): # Language Code Exists, ch2 r.skip(8) # Language Code, ch2 if r.bits(1): # Audio Production Information Exists, ch2 # Mixing Level, ch2, 5 Bits # Room Type, ch2, 2 Bits r.skip(7) # Copyright Bit, 1 Bit # Original Bit Stream, 1 Bit r.skip(2) timecod1e = r.bits(1) # Time Code First Halve Exists timecod2e = r.bits(1) # Time Code Second Halve Exists if timecod1e: r.skip(14) # Time Code First Half if timecod2e: r.skip(14) # Time Code Second Half if r.bits(1): # Additional Bit Stream Information Exists addbsil = r.bit(6) # Additional Bit Stream Information Length r.skip((addbsil + 1) * 8) @staticmethod def _skip_unused_header_bits_enhanced(bitreader, frame_type, channel_mode, sr_code, numblocks_code): r = bitreader r.skip(5) # Dialogue Normalization if r.bits(1): # Compression Gain Word Exists r.skip(8) # Compression Gain Word if channel_mode == ChannelMode.DUALMONO: r.skip(5) # Dialogue Normalization, ch2 if r.bits(1): # Compression Gain Word Exists, ch2 r.skip(8) # Compression Gain Word, ch2 if frame_type == EAC3FrameType.DEPENDENT: if r.bits(1): # chanmap exists r.skip(16) # chanmap if r.bits(1): # mixmdate, 1 Bit # FIXME: Handle channel dependent fields return if r.bits(1): # Informational Metadata Exists # bsmod, 3 Bits # Copyright Bit, 1 Bit # Original Bit Stream, 1 Bit r.skip(5) if channel_mode == ChannelMode.STEREO: # dsurmod. 2 Bits # dheadphonmod, 2 Bits r.skip(4) elif channel_mode >= ChannelMode.C2F2R: r.skip(2) # dsurexmod if r.bits(1): # Audio Production Information Exists # Mixing Level, 5 Bits # Room Type, 2 Bits # adconvtyp, 1 Bit r.skip(8) if channel_mode == ChannelMode.DUALMONO: if r.bits(1): # Audio Production Information Exists, ch2 # Mixing Level, ch2, 5 Bits # Room Type, ch2, 2 Bits # adconvtyp, ch2, 1 Bit r.skip(8) if sr_code < 3: # if not half sample rate r.skip(1) # sourcefscod if frame_type == EAC3FrameType.INDEPENDENT and numblocks_code == 3: r.skip(1) # convsync if frame_type == EAC3FrameType.AC3_CONVERT: if numblocks_code != 3: if r.bits(1): # blkid r.skip(6) # frmsizecod if r.bits(1): # Additional Bit Stream Information Exists addbsil = r.bit(6) # Additional Bit Stream Information Length r.skip((addbsil + 1) * 8) @staticmethod def _get_channels(channel_mode, lfe_on): try: return AC3_CHANNELS[channel_mode] + lfe_on except KeyError as e: raise AC3Error(e) def _guess_length(self, fileobj): # use bitrate + data size to guess length if self.bitrate == 0: return start = fileobj.tell() fileobj.seek(0, 2) length = fileobj.tell() - start return 8.0 * length / self.bitrate def pprint(self): return u"%s, %d Hz, %.2f seconds, %d channel(s), %d bps" % ( self.codec, self.sample_rate, self.length, self.channels, self.bitrate) class AC3(FileType): """AC3(filething) Arguments: filething (filething) Load AC3 or EAC3 files. Tagging is not supported. Use the ID3/APEv2 classes directly instead. Attributes: info (`AC3Info`) """ _mimes = ["audio/ac3"] @loadfile() def load(self, filething): self.info = AC3Info(filething.fileobj) def add_tags(self): raise AC3Error("doesn't support tags") @staticmethod def score(filename, fileobj, header): return header.startswith(b"\x0b\x77") * 2 \ + (endswith(filename, ".ac3") or endswith(filename, ".eac3")) Open = AC3 error = AC3Error ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/aiff.py0000664000175000017500000001437200000000000016421 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2014 Evan Purkhiser # 2014 Ben Ockmore # 2019-2020 Philipp Wolfer # # 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. """AIFF audio stream information and tags.""" import struct from struct import pack from mutagen import StreamInfo, FileType from mutagen.id3._util import ID3NoHeaderError, error as ID3Error from mutagen._iff import ( IffChunk, IffContainerChunkMixin, IffFile, IffID3, InvalidChunk, error as IffError, ) from mutagen._util import ( convert_error, loadfile, endswith, ) __all__ = ["AIFF", "Open", "delete"] class error(IffError): pass # based on stdlib's aifc _HUGE_VAL = 1.79769313486231e+308 def read_float(data): """Raises OverflowError""" assert len(data) == 10 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: raise OverflowError("inf and nan not supported") else: expon = expon - 16383 # this can raise OverflowError too f = (himant * 0x100000000 + lomant) * pow(2.0, expon - 63) return sign * f class AIFFChunk(IffChunk): """Representation of a single IFF chunk""" @classmethod def parse_header(cls, header): return struct.unpack('>4sI', header) @classmethod def get_class(cls, id): if id == 'FORM': return AIFFFormChunk else: return cls def write_new_header(self, id_, size): self._fileobj.write(pack('>4sI', id_, size)) def write_size(self): self._fileobj.write(pack('>I', self.data_size)) class AIFFFormChunk(AIFFChunk, IffContainerChunkMixin): """The AIFF root chunk.""" def parse_next_subchunk(self): return AIFFChunk.parse(self._fileobj, self) def __init__(self, fileobj, id, data_size, parent_chunk): if id != u'FORM': raise InvalidChunk('Expected FORM chunk, got %s' % id) AIFFChunk.__init__(self, fileobj, id, data_size, parent_chunk) self.init_container() class AIFFFile(IffFile): """Representation of a AIFF file""" def __init__(self, fileobj): # AIFF Files always start with the FORM chunk which contains a 4 byte # ID before the start of other chunks super().__init__(AIFFChunk, fileobj) if self.root.id != u'FORM': raise InvalidChunk("Root chunk must be a FORM chunk, got %s" % self.root.id) def __contains__(self, id_): if id_ == 'FORM': # For backwards compatibility return True return super().__contains__(id_) def __getitem__(self, id_): if id_ == 'FORM': # For backwards compatibility return self.root return super().__getitem__(id_) class AIFFInfo(StreamInfo): """AIFFInfo() AIFF audio stream information. Information is parsed from the COMM chunk of the AIFF file Attributes: length (`float`): audio length, in seconds bitrate (`int`): audio bitrate, in bits per second channels (`int`): The number of audio channels sample_rate (`int`): audio sample rate, in Hz bits_per_sample (`int`): The audio sample size """ length = 0 bitrate = 0 channels = 0 sample_rate = 0 @convert_error(IOError, error) def __init__(self, fileobj): """Raises error""" iff = AIFFFile(fileobj) try: common_chunk = iff[u'COMM'] except KeyError as e: raise error(str(e)) data = common_chunk.read() if len(data) < 18: raise error info = struct.unpack('>hLh10s', data[:18]) channels, frame_count, sample_size, sample_rate = info try: self.sample_rate = int(read_float(sample_rate)) except OverflowError: raise error("Invalid sample rate") if self.sample_rate < 0: raise error("Invalid sample rate") if self.sample_rate != 0: self.length = frame_count / float(self.sample_rate) self.bits_per_sample = sample_size self.sample_size = sample_size # For backward compatibility self.channels = channels self.bitrate = channels * sample_size * 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(IffID3): """A AIFF file with ID3v2 tags""" def _load_file(self, fileobj): return AIFFFile(fileobj) @convert_error(IOError, error) @loadfile(method=False, writable=True) def delete(filething): """Completely removes the ID3 chunk from the AIFF file""" try: del AIFFFile(filething.fileobj)[u'ID3'] except KeyError: pass class AIFF(FileType): """AIFF(filething) An AIFF audio file. Arguments: filething (filething) Attributes: tags (`mutagen.id3.ID3`) info (`AIFFInfo`) """ _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") @convert_error(IOError, error) @loadfile() def load(self, filething, **kwargs): """Load stream and tag information from a file.""" fileobj = filething.fileobj try: self.tags = _IFFID3(fileobj, **kwargs) except ID3NoHeaderError: self.tags = None except ID3Error as e: raise error(e) else: self.tags.filename = self.filename fileobj.seek(0, 0) self.info = AIFFInfo(fileobj) Open = AIFF ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/apev2.py0000664000175000017500000005044500000000000016532 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """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 io import BytesIO from collections.abc import MutableSequence from mutagen import Metadata, FileType, StreamInfo from mutagen._util import DictMixin, cdata, delete_bytes, total_ordering, \ MutagenError, loadfile, convert_error, seek_end, get_size, reraise def is_valid_apev2_key(key): if not isinstance(key, str): raise TypeError("APEv2 key must be str") # 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 = range(3) HAS_HEADER = 1 << 31 HAS_NO_FOOTER = 1 << 30 IS_HEADER = 1 << 29 class error(MutagenError): pass class APENoHeaderError(error): pass class APEUnsupportedVersionError(error): pass class APEBadItemError(error): 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): """Raises IOError and apev2.error""" 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: if get_size(fileobj) < 128: raise IOError 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): """Raises IOError and apev2.error""" fileobj.seek(self.metadata + 8) data = fileobj.read(16) if len(data) != 16: raise error self.version = data[:4] self.size = cdata.uint32_le(data[4:8]) self.items = cdata.uint32_le(data[8:12]) self.flags = cdata.uint32_le(data[12:]) 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): """APEv2(filething=None) 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) @convert_error(IOError, error) @loadfile() def load(self, filething): """Load tags from a filename. Raises apev2.error """ data = _APEv2Data(filething.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): """Raises IOError and APEBadItemError""" fileobj = BytesIO(tag) for i in range(count): tag_data = fileobj.read(8) # someone writes wrong item counts if not tag_data: break if len(tag_data) != 8: raise error size = cdata.uint32_le(tag_data[:4]) flags = cdata.uint32_le(tag_data[4:8]) # 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) if not key: raise APEBadItemError while key[-1:] != b'\x00' and value: value = fileobj.read(1) if not value: raise APEBadItemError key += value if key[-1:] == b"\x00": key = key[:-1] try: key = key.decode("ascii") except UnicodeError as err: reraise(APEBadItemError, err, sys.exc_info()[2]) value = fileobj.read(size) if len(value) != size: raise APEBadItemError 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) 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) 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 not isinstance(value, _APEValue): # let's guess at the content if we're not already a value... if isinstance(value, str): # unicode? we've got to be text. value = APEValue(value, TEXT) elif isinstance(value, list): items = [] for v in value: if not isinstance(v, str): raise TypeError("item in list not str") items.append(v) # list? text. value = APEValue(u"\0".join(items), TEXT) else: value = APEValue(value, BINARY) super(APEv2, self).__setitem__(key, value) @convert_error(IOError, error) @loadfile(writable=True, create=True) def save(self, filething=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. """ fileobj = filething.fileobj 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=lambda tag: (len(tag), tag)) 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) @convert_error(IOError, error) @loadfile(writable=True) def delete(self, filething=None): """Remove tags from a file.""" fileobj = filething.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 @convert_error(IOError, error) @loadfile(method=False, writable=True) def delete(filething): """delete(filething) Arguments: filething (filething) Raises: mutagen.MutagenError Remove tags from a file. """ try: t = APEv2(filething) except APENoHeaderError: return filething.fileobj.seek(0) t.delete(filething) 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) @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, str): raise TypeError("value not str") 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, str): raise TypeError("value not str") values = list(self) values[index] = value self.value = u"\0".join(values) def insert(self, index, value): if not isinstance(value, str): raise TypeError("value not str") 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) @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): """APEv2File(filething) Arguments: filething (filething) Attributes: tags (`APEv2`) """ class _Info(StreamInfo): length = 0 bitrate = 0 def __init__(self, fileobj): pass @staticmethod def pprint(): return u"Unknown format with APEv2 tag." @loadfile() def load(self, filething): fileobj = filething.fileobj self.info = self._Info(fileobj) try: fileobj.seek(0, 0) except IOError as e: raise error(e) try: self.tags = APEv2(fileobj) 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: seek_end(fileobj, 160) footer = fileobj.read() except IOError: return -1 return ((b"APETAGEX" in footer) - header.startswith(b"ID3")) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1596184724.8191056 mutagen-1.45.1/mutagen/asf/0000775000175000017500000000000000000000000015704 5ustar00lazkalazka00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/asf/__init__.py0000664000175000017500000002320000000000000020012 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """Read and write ASF (Window Media Audio) files.""" __all__ = ["ASF", "Open"] from mutagen import FileType, Tags, StreamInfo from mutagen._util import resize_bytes, DictMixin, loadfile, convert_error from ._util import error, ASFError, ASFHeaderError from ._objects import HeaderObject, MetadataLibraryObject, MetadataObject, \ ExtendedContentDescriptionObject, HeaderExtensionObject, \ ContentDescriptionObject from ._attrs import ASFGUIDAttribute, ASFWordAttribute, ASFQWordAttribute, \ ASFDWordAttribute, ASFBoolAttribute, ASFByteArrayAttribute, \ ASFUnicodeAttribute, ASFBaseAttribute, ASFValue # flake8 error, ASFError, ASFHeaderError, ASFValue class ASFInfo(StreamInfo): """ASFInfo() ASF stream information. Attributes: length (`float`): "Length in seconds sample_rate (`int`): Sample rate in Hz bitrate (`int`): Bitrate in bps channels (`int`): Number of channels codec_type (`mutagen.text`): Name of the codec type of the first audio stream or an empty string if unknown. Example: ``Windows Media Audio 9 Standard`` codec_name (`mutagen.text`): Name and maybe version of the codec used. Example: ``Windows Media Audio 9.1`` codec_description (`mutagen.text`): Further information on the codec used. Example: ``64 kbps, 48 kHz, stereo 2-pass CBR`` """ length = 0.0 sample_rate = 0 bitrate = 0 channels = 0 codec_type = u"" codec_name = u"" codec_description = u"" def __init__(self): self.length = 0.0 self.sample_rate = 0 self.bitrate = 0 self.channels = 0 self.codec_type = u"" self.codec_name = u"" self.codec_description = u"" def pprint(self): """Returns: text: a stream information text summary """ s = u"ASF (%s) %d bps, %s Hz, %d channels, %.2f seconds" % ( self.codec_type or self.codec_name or u"???", self.bitrate, self.sample_rate, self.channels, self.length) return s class ASFTags(list, DictMixin, Tags): """ASFTags() Dictionary containing ASF attributes. """ def __getitem__(self, key): """A list of values for the key. This is a copy, so comment['title'].append('a title') will not work. """ if isinstance(key, slice): return list.__getitem__(self, key) values = [value for (k, value) in self if k == key] if not values: raise KeyError(key) else: return values def __delitem__(self, key): """Delete all values associated with the key.""" if isinstance(key, slice): return list.__delitem__(self, key) to_delete = [x for x in self if x[0] == key] if not to_delete: raise KeyError(key) else: for k in to_delete: self.remove(k) def __contains__(self, key): """Return true if the key has any values.""" for k, value in self: if k == key: return True else: return False def __setitem__(self, key, values): """Set a key's value or values. Setting a value overwrites all old ones. The value may be a list of Unicode or UTF-8 strings, or a single Unicode or UTF-8 string. """ if isinstance(key, slice): return list.__setitem__(self, key, values) if not isinstance(values, list): values = [values] to_append = [] for value in values: if not isinstance(value, ASFBaseAttribute): if isinstance(value, str): value = ASFUnicodeAttribute(value) elif isinstance(value, bytes): value = ASFByteArrayAttribute(value) elif isinstance(value, bool): value = ASFBoolAttribute(value) elif isinstance(value, int): value = ASFDWordAttribute(value) else: raise TypeError("Invalid type %r" % type(value)) to_append.append((key, value)) try: del(self[key]) except KeyError: pass self.extend(to_append) def keys(self): """Return a sequence of all keys in the comment.""" return self and set(next(zip(*self))) def as_dict(self): """Return a copy of the comment data in a real dict.""" d = {} for key, value in self: d.setdefault(key, []).append(value) return d def pprint(self): """Returns a string containing all key, value pairs. :rtype: text """ return "\n".join("%s=%s" % (k, v) for k, v in self) UNICODE = ASFUnicodeAttribute.TYPE """Unicode string type""" BYTEARRAY = ASFByteArrayAttribute.TYPE """Byte array type""" BOOL = ASFBoolAttribute.TYPE """Bool type""" DWORD = ASFDWordAttribute.TYPE """"DWord type (uint32)""" QWORD = ASFQWordAttribute.TYPE """QWord type (uint64)""" WORD = ASFWordAttribute.TYPE """Word type (uint16)""" GUID = ASFGUIDAttribute.TYPE """GUID type""" class ASF(FileType): """ASF(filething) An ASF file, probably containing WMA or WMV. Arguments: filething (filething) Attributes: info (`ASFInfo`) tags (`ASFTags`) """ _mimes = ["audio/x-ms-wma", "audio/x-ms-wmv", "video/x-ms-asf", "audio/x-wma", "video/x-wmv"] info = None tags = None @convert_error(IOError, error) @loadfile() def load(self, filething): """load(filething) Args: filething (filething) Raises: mutagen.MutagenError """ fileobj = filething.fileobj self.info = ASFInfo() self.tags = ASFTags() self._tags = {} self._header = HeaderObject.parse_full(self, fileobj) for guid in [ContentDescriptionObject.GUID, ExtendedContentDescriptionObject.GUID, MetadataObject.GUID, MetadataLibraryObject.GUID]: self.tags.extend(self._tags.pop(guid, [])) assert not self._tags @convert_error(IOError, error) @loadfile(writable=True) def save(self, filething=None, padding=None): """save(filething=None, padding=None) Save tag changes back to the loaded file. Args: filething (filething) padding (:obj:`mutagen.PaddingFunction`) Raises: mutagen.MutagenError """ # Move attributes to the right objects self.to_content_description = {} self.to_extended_content_description = {} self.to_metadata = {} self.to_metadata_library = [] for name, value in self.tags: library_only = (value.data_size() > 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()) fileobj = filething.fileobj # Render to file 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 @loadfile(writable=True) def delete(self, filething=None): """delete(filething=None) Args: filething (filething) Raises: mutagen.MutagenError """ self.tags.clear() self.save(filething, padding=lambda x: 0) @staticmethod def score(filename, fileobj, header): return header.startswith(HeaderObject.GUID) * 2 Open = ASF ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/asf/_attrs.py0000664000175000017500000002302500000000000017554 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. import sys import struct from mutagen._util import total_ordering, reraise 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 is not None: 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("" % ( 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() remaining_header, num_objects = cls.parse_size(fileobj) remaining_header -= 30 for i in range(num_objects): obj_header_size = 24 if remaining_header < obj_header_size: raise ASFHeaderError("invalid header size") data = fileobj.read(obj_header_size) if len(data) != obj_header_size: raise ASFHeaderError("truncated") remaining_header -= obj_header_size guid, size = struct.unpack("<16sQ", data) obj = BaseObject._get_object(guid) payload_size = size - obj_header_size if remaining_header < payload_size: raise ASFHeaderError("invalid object size") remaining_header -= payload_size try: data = fileobj.read(payload_size) except (OverflowError, MemoryError): # read doesn't take 64bit values raise ASFHeaderError("invalid header size") if len(data) != payload_size: raise ASFHeaderError("truncated") try: obj.parse(asf, data) except struct.error: raise ASFHeaderError("truncated") 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: texts.append(data[pos:end].decode("utf-16-le").strip(u"\x00")) else: texts.append(None) pos = end for key, value in zip(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 str(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("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", } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/dsdiff.py0000664000175000017500000001746500000000000016761 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2020 Philipp Wolfer # # 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. """DSDIFF audio stream information and tags.""" import struct from mutagen import StreamInfo from mutagen._file import FileType from mutagen._iff import ( IffChunk, IffContainerChunkMixin, IffID3, IffFile, InvalidChunk, error as IffError, ) from mutagen.id3._util import ID3NoHeaderError, error as ID3Error from mutagen._util import ( convert_error, loadfile, endswith, ) __all__ = ["DSDIFF", "Open", "delete"] class error(IffError): pass # See # https://dsd-guide.com/sites/default/files/white-papers/DSDIFF_1.5_Spec.pdf class DSDIFFChunk(IffChunk): """Representation of a single DSDIFF chunk""" HEADER_SIZE = 12 @classmethod def parse_header(cls, header): return struct.unpack('>4sQ', header) @classmethod def get_class(cls, id): if id in DSDIFFListChunk.LIST_CHUNK_IDS: return DSDIFFListChunk elif id == 'DST': return DSTChunk else: return cls def write_new_header(self, id_, size): self._fileobj.write(struct.pack('>4sQ', id_, size)) def write_size(self): self._fileobj.write(struct.pack('>Q', self.data_size)) class DSDIFFListChunk(DSDIFFChunk, IffContainerChunkMixin): """A DSDIFF chunk containing other chunks. """ LIST_CHUNK_IDS = ['FRM8', 'PROP'] def parse_next_subchunk(self): return DSDIFFChunk.parse(self._fileobj, self) def __init__(self, fileobj, id, data_size, parent_chunk): if id not in self.LIST_CHUNK_IDS: raise InvalidChunk('Not a list chunk: %s' % id) DSDIFFChunk.__init__(self, fileobj, id, data_size, parent_chunk) self.init_container() class DSTChunk(DSDIFFChunk, IffContainerChunkMixin): """A DSDIFF chunk containing other chunks. """ def parse_next_subchunk(self): return DSDIFFChunk.parse(self._fileobj, self) def __init__(self, fileobj, id, data_size, parent_chunk): if id != 'DST': raise InvalidChunk('Not a DST chunk: %s' % id) DSDIFFChunk.__init__(self, fileobj, id, data_size, parent_chunk) self.init_container(name_size=0) class DSDIFFFile(IffFile): """Representation of a DSDIFF file""" def __init__(self, fileobj): super().__init__(DSDIFFChunk, fileobj) if self.root.id != u'FRM8': raise InvalidChunk("Root chunk must be a FRM8 chunk, got %r" % self.root) class DSDIFFInfo(StreamInfo): """DSDIFF stream information. Attributes: channels (`int`): number of audio channels length (`float`): file length in seconds, as a float sample_rate (`int`): audio sampling rate in Hz bits_per_sample (`int`): audio sample size (for DSD this is always 1) bitrate (`int`): audio bitrate, in bits per second compression (`str`): DSD (uncompressed) or DST """ channels = 0 length = 0 sample_rate = 0 bits_per_sample = 1 bitrate = 0 compression = None @convert_error(IOError, error) def __init__(self, fileobj): """Raises error""" iff = DSDIFFFile(fileobj) try: prop_chunk = iff['PROP'] except KeyError as e: raise error(str(e)) if prop_chunk.name == 'SND ': for chunk in prop_chunk.subchunks(): if chunk.id == 'FS' and chunk.data_size == 4: data = chunk.read() if len(data) < 4: raise InvalidChunk("Not enough data in FS chunk") self.sample_rate, = struct.unpack('>L', data[:4]) elif chunk.id == 'CHNL' and chunk.data_size >= 2: data = chunk.read() if len(data) < 2: raise InvalidChunk("Not enough data in CHNL chunk") self.channels, = struct.unpack('>H', data[:2]) elif chunk.id == 'CMPR' and chunk.data_size >= 4: data = chunk.read() if len(data) < 4: raise InvalidChunk("Not enough data in CMPR chunk") compression_id, = struct.unpack('>4s', data[:4]) self.compression = compression_id.decode('ascii').rstrip() if self.sample_rate < 0: raise error("Invalid sample rate") if self.compression == 'DSD': # not compressed try: dsd_chunk = iff['DSD'] except KeyError as e: raise error(str(e)) # DSD data has one bit per sample. Eight samples of a channel # are clustered together for a channel byte. For multiple channels # the channel bytes are interleaved (in the order specified in the # CHNL chunk). See DSDIFF spec chapter 3.3. sample_count = dsd_chunk.data_size * 8 / (self.channels or 1) if self.sample_rate != 0: self.length = sample_count / float(self.sample_rate) self.bitrate = (self.channels * self.bits_per_sample * self.sample_rate) elif self.compression == 'DST': try: dst_frame = iff['DST'] dst_frame_info = dst_frame['FRTE'] except KeyError as e: raise error(str(e)) if dst_frame_info.data_size >= 6: data = dst_frame_info.read() if len(data) < 6: raise InvalidChunk("Not enough data in FRTE chunk") frame_count, frame_rate = struct.unpack('>LH', data[:6]) if frame_rate: self.length = frame_count / frame_rate if frame_count: dst_data_size = dst_frame.data_size - dst_frame_info.size avg_frame_size = dst_data_size / frame_count self.bitrate = avg_frame_size * 8 * frame_rate def pprint(self): return u"%d channel DSDIFF (%s) @ %d bps, %s Hz, %.2f seconds" % ( self.channels, self.compression, self.bitrate, self.sample_rate, self.length) class _DSDIFFID3(IffID3): """A DSDIFF file with ID3v2 tags""" def _load_file(self, fileobj): return DSDIFFFile(fileobj) @convert_error(IOError, error) @loadfile(method=False, writable=True) def delete(filething): """Completely removes the ID3 chunk from the DSDIFF file""" try: del DSDIFFFile(filething.fileobj)[u'ID3'] except KeyError: pass class DSDIFF(FileType): """DSDIFF(filething) An DSDIFF audio file. For tagging ID3v2 data is added to a chunk with the ID "ID3 ". Arguments: filething (filething) Attributes: tags (`mutagen.id3.ID3`) info (`DSDIFFInfo`) """ _mimes = ["audio/x-dff"] @convert_error(IOError, error) @loadfile() def load(self, filething, **kwargs): fileobj = filething.fileobj try: self.tags = _DSDIFFID3(fileobj, **kwargs) except ID3NoHeaderError: self.tags = None except ID3Error as e: raise error(e) else: self.tags.filename = self.filename fileobj.seek(0, 0) self.info = DSDIFFInfo(fileobj) def add_tags(self): """Add empty ID3 tags to the file.""" if self.tags is None: self.tags = _DSDIFFID3() else: raise error("an ID3 tag already exists") @staticmethod def score(filename, fileobj, header): return header.startswith(b"FRM8") * 2 + endswith(filename, ".dff") Open = DSDIFF ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/dsf.py0000664000175000017500000002340200000000000016262 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2017 Boris Pruessmann # # 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. """Read and write DSF audio stream information and tags.""" import sys import struct from io import BytesIO from mutagen import FileType, StreamInfo from mutagen._util import cdata, MutagenError, loadfile, \ convert_error, reraise, endswith from mutagen.id3 import ID3 from mutagen.id3._util import ID3NoHeaderError, error as ID3Error __all__ = ["DSF", "Open", "delete"] class error(MutagenError): pass class DSFChunk(object): """A generic chunk of a DSFFile.""" chunk_offset = 0 chunk_header = " " chunk_size = -1 def __init__(self, fileobj, create=False): self.fileobj = fileobj if not create: self.chunk_offset = fileobj.tell() self.load() def load(self): raise NotImplementedError def write(self): raise NotImplementedError class DSDChunk(DSFChunk): """Represents the first chunk of a DSF file""" CHUNK_SIZE = 28 total_size = 0 offset_metdata_chunk = 0 def __init__(self, fileobj, create=False): super(DSDChunk, self).__init__(fileobj, create) if create: self.chunk_header = b"DSD " self.chunk_size = DSDChunk.CHUNK_SIZE def load(self): data = self.fileobj.read(DSDChunk.CHUNK_SIZE) if len(data) != DSDChunk.CHUNK_SIZE: raise error("DSF chunk truncated") self.chunk_header = data[0:4] if self.chunk_header != b"DSD ": raise error("DSF dsd header not found") self.chunk_size = cdata.ulonglong_le(data[4:12]) if self.chunk_size != DSDChunk.CHUNK_SIZE: raise error("DSF dsd header size mismatch") self.total_size = cdata.ulonglong_le(data[12:20]) self.offset_metdata_chunk = cdata.ulonglong_le(data[20:28]) def write(self): f = BytesIO() f.write(self.chunk_header) f.write(struct.pack(" u'\x7f': enc = 3 break id3.add(mutagen.id3.TXXX(encoding=enc, text=value, desc=desc)) 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)) @loadfile(writable=True, create=True) def save(self, filething=None, v1=1, v2_version=4, v23_sep='/', padding=None): """save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None) Save changes to a file. See :meth:`mutagen.id3.ID3.save` for more info. """ if v2_version == 3: # EasyID3 only works with v2.4 frames, so update_to_v23() would # break things. We have to save a shallow copy of all tags # and restore it after saving. Due to CHAP/CTOC copying has # to be done recursively implemented in ID3Tags. backup = self.__id3._copy() try: self.__id3.update_to_v23() self.__id3.save( filething, v1=v1, v2_version=v2_version, v23_sep=v23_sep, padding=padding) finally: self.__id3._restore(backup) else: self.__id3.save(filething, v1=v1, v2_version=v2_version, v23_sep=v23_sep, padding=padding) 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)) @property def size(self): return self.__id3.size def __getitem__(self, key): func = dict_match(self.Get, key.lower(), 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): if isinstance(value, str): value = [value] func = dict_match(self.Set, key.lower(), 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): func = dict_match(self.Delete, key.lower(), 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 { "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": "albumartist", "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", }.items(): 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 { 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"PERFORMER": "performer", 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", }.items(): EasyID3.RegisterTXXXKey(key, desc) class EasyID3FileType(ID3FileType): """EasyID3FileType(filething=None) Like ID3FileType, but uses EasyID3 for tags. Arguments: filething (filething) Attributes: tags (`EasyID3`) """ ID3 = EasyID3 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/easymp4.py0000664000175000017500000002061100000000000017067 0ustar00lazkalazka00000000000000# -*- 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 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. from mutagen import Tags from mutagen._util import DictMixin, dict_match from mutagen.mp4 import MP4, MP4Tags, error, delete __all__ = ["EasyMP4Tags", "EasyMP4", "delete", "error"] class EasyMP4KeyError(error, KeyError, ValueError): pass class EasyMP4Tags(DictMixin, Tags): """EasyMP4Tags() A file with MPEG-4 iTunes metadata. Like Vorbis comments, EasyMP4Tags keys are case-insensitive ASCII strings, and values are a list of Unicode strings (and these lists are always of length 0 or 1). If you need access to the full MP4 metadata feature set, you should use MP4, not EasyMP4. """ Set = {} Get = {} Delete = {} List = {} def __init__(self, *args, **kwargs): self.__mp4 = MP4Tags(*args, **kwargs) self.load = self.__mp4.load self.save = self.__mp4.save self.delete = self.__mp4.delete filename = property(lambda s: s.__mp4.filename, lambda s, fn: setattr(s.__mp4, 'filename', fn)) @property def _padding(self): return self.__mp4._padding @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(str, 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(str(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, str): raise TypeError("%r not str" % v) 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 isinstance(value, str): 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): """EasyMP4(filelike) Like :class:`MP4 `, but uses :class:`EasyMP4Tags` for tags. Attributes: info (`mutagen.mp4.MP4Info`) tags (`EasyMP4Tags`) """ MP4Tags = EasyMP4Tags Get = EasyMP4Tags.Get Set = EasyMP4Tags.Set Delete = EasyMP4Tags.Delete List = EasyMP4Tags.List RegisterTextKey = EasyMP4Tags.RegisterTextKey RegisterKey = EasyMP4Tags.RegisterKey ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/flac.py0000664000175000017500000007573700000000000016435 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """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 io import BytesIO from ._vorbis import VCommentDict import mutagen from mutagen._util import resize_bytes, MutagenError, get_size, loadfile, \ convert_error, bchr, endswith from mutagen._tags import PaddingInfo from mutagen.id3._util import BitPaddedInt from functools import reduce class error(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", "flush", "truncate"]: 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 (`bytes`): 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 = BytesIO(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): """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 (`int`): minimum audio block size max_blocksize (`int`): maximum audio block size sample_rate (`int`): audio sample rate in Hz channels (`int`): audio channels (1 for mono, 2 for stereo) bits_per_sample (`int`): bits per sample total_samples (`int`): total samples in file length (`float`): audio length in seconds bitrate (`int`): bitrate in bits per second, as an int """ code = 0 bitrate = 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 Exception: 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 = BytesIO() 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(bchr(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(bchr(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): """SeekPoint() 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 (`int`): sample number of first sample in the target frame byte_offset (`int`): offset from first frame to target frame num_samples (`int`): 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)) def __getnewargs__(self): return self.first_sample, self.byte_offset, self.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 = BytesIO() 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): """VCFLACDict() 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): """CueSheetTrackIndex(index_number, index_offset) 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 (`int`): index point number index_offset (`int`): 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): """CueSheetTrack() 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 (`int`): track number start_offset (`int`): track offset in samples from start of FLAC stream isrc (`mutagen.text`): ISRC code, exactly 12 characters type (`int`): 0 for audio, 1 for digital data pre_emphasis (`bool`): true if the track is recorded with pre-emphasis indexes (list[CueSheetTrackIndex]): 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): """CueSheet() 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 (`mutagen.text`): media catalog number in ASCII, up to 128 characters lead_in_samples (`int`): number of lead-in samples compact_disc (`bool`): true if the cuesheet corresponds to a compact disc tracks (list[CueSheetTrack]): list of CueSheetTrack objects lead_out (`CueSheetTrack` or `None`): 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 range(num_tracks): track = data.read(self.__CUESHEET_TRACK_SIZE) start_offset, track_number, isrc_padded, flags, num_indexes = \ struct.unpack(self.__CUESHEET_TRACK_FORMAT, track) isrc = isrc_padded.rstrip(b'\0') type_ = (flags & 0x80) >> 7 pre_emphasis = bool(flags & 0x40) val = CueSheetTrack( track_number, start_offset, isrc, type_, pre_emphasis) for j in range(num_indexes): index = data.read(self.__CUESHEET_TRACKINDEX_SIZE) index_offset, index_number = struct.unpack( self.__CUESHEET_TRACKINDEX_FORMAT, index) val.indexes.append( CueSheetTrackIndex(index_number, index_offset)) self.tracks.append(val) def write(self): f = BytesIO() 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): """Picture() Read and write FLAC embed pictures. .. currentmodule:: mutagen Attributes: type (`id3.PictureType`): picture type (same as types for ID3 APIC frames) mime (`text`): MIME type of the picture desc (`text`): picture's description width (`int`): width in pixels height (`int`): height in pixels depth (`int`): color depth in bits-per-pixel colors (`int`): number of colors for indexed palettes (like GIF), 0 for non-indexed data (`bytes`): 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 = BytesIO() 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): """Padding() 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. Attributes: length (`int`): length """ 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): """FLAC(filething) A FLAC audio file. Args: filething (filething) Attributes: cuesheet (`CueSheet`): if any or `None` seektable (`SeekTable`): if any or `None` pictures (list[Picture]): list of embedded pictures info (`StreamInfo`) tags (`mutagen._vorbis.VCommentDict`) """ _mimes = ["audio/flac", "audio/x-flac", "application/x-flac"] info = None tags = None 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. # https://github.com/quodlibet/mutagen/issues/52 # ..same for the Picture block: # https://github.com/quodlibet/mutagen/issues/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: # https://github.com/quodlibet/mutagen/issues/377 # Something writes multiple and metaflac doesn't care pass 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 @convert_error(IOError, error) @loadfile(writable=True) def delete(self, filething=None): """Remove Vorbis comments from a file. If no filename is given, the one most recently loaded is used. """ if self.tags is not None: temp_blocks = [ b for b in self.metadata_blocks if b.code != VCFLACDict.code] self._save(filething, temp_blocks, False, padding=lambda x: 0) self.metadata_blocks[:] = [ b for b in self.metadata_blocks if b.code != VCFLACDict.code or b is self.tags] self.tags.clear() vc = property(lambda s: s.tags, doc="Alias for tags; don't use this.") @convert_error(IOError, error) @loadfile() def load(self, filething): """Load file information from a filename.""" fileobj = filething.fileobj self.metadata_blocks = [] self.tags = None self.cuesheet = None self.seektable = None fileobj = StrictFileObject(fileobj) self.__check_header(fileobj, filething.name) while self.__read_metadata_block(fileobj): pass try: self.metadata_blocks[0].length except (AttributeError, IndexError): raise FLACNoHeaderError("Stream info block not found") if self.info.length: start = fileobj.tell() fileobj.seek(0, 2) self.info.bitrate = int( float(fileobj.tell() - start) * 8 / self.info.length) else: self.info.bitrate = 0 @property def info(self): return self.metadata_blocks[0] def add_picture(self, picture): """Add a new picture to the file. Args: picture (Picture) """ 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): return [b for b in self.metadata_blocks if b.code == Picture.code] @convert_error(IOError, error) @loadfile(writable=True) def save(self, filething=None, deleteid3=False, padding=None): """Save metadata blocks to a file. Args: filething (filething) deleteid3 (bool): delete id3 tags while at it padding (:obj:`mutagen.PaddingFunction`) If no filename is given, the one most recently loaded is used. """ self._save(filething, self.metadata_blocks, deleteid3, padding) def _save(self, filething, metadata_blocks, deleteid3, padding): f = StrictFileObject(filething.fileobj) header = self.__check_header(f, filething.name) 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( metadata_blocks, available, content_size, padding) data_size = len(data) resize_bytes(filething.fileobj, 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, name): """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" % name) return size Open = FLAC @convert_error(IOError, error) @loadfile(method=False, writable=True) def delete(filething): """Remove tags from a file. Args: filething (filething) Raises: mutagen.MutagenError """ f = FLAC(filething) filething.fileobj.seek(0) f.delete(filething) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1596184724.8191056 mutagen-1.45.1/mutagen/id3/0000775000175000017500000000000000000000000015612 5ustar00lazkalazka00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/id3/__init__.py0000664000175000017500000001101100000000000017715 0ustar00lazkalazka00000000000000# -*- 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 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. """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. """ from ._file import ID3, ID3FileType, delete, ID3v1SaveOptions from ._specs import Encoding, PictureType, CTOCFlags, ID3TimeStamp from ._frames import Frames, Frames_2_2, Frame, TextFrame, UrlFrame, \ UrlFrameU, TimeStampTextFrame, BinaryFrame, NumericPartTextFrame, \ NumericTextFrame, PairedTextFrame from ._util import ID3NoHeaderError, error, ID3UnsupportedVersionError from ._id3v1 import ParseID3v1, MakeID3v1 from ._tags import ID3Tags from ._frames import (AENC, APIC, ASPI, BUF, CHAP, CNT, COM, COMM, COMR, CRA, CRM, CTOC, ENCR, EQU2, ETC, ETCO, GEO, GEOB, GP1, GRID, GRP1, IPL, IPLS, LINK, LNK, MCDI, MCI, MLL, MLLT, MVI, MVIN, MVN, MVNM, OWNE, PCNT, PCST, PIC, POP, POPM, POSS, PRIV, RBUF, REV, RVA, RVA2, RVAD, RVRB, SEEK, SIGN, SLT, STC, SYLT, SYTC, TAL, TALB, TBP, TBPM, TCAT, TCM, TCMP, TCO, TCOM, TCON, TCOP, TCP, TCR, TDA, TDAT, TDEN, TDES, TDLY, TDOR, TDRC, TDRL, TDTG, TDY, TEN, TENC, TEXT, TFLT, TFT, TGID, TIM, TIME, TIPL, TIT1, TIT2, TIT3, TKE, TKEY, TKWD, TLA, TLAN, TLE, TLEN, TMCL, TMED, TMOO, TMT, TOA, TOAL, TOF, TOFN, TOL, TOLY, TOPE, TOR, TORY, TOT, TOWN, TP1, TP2, TP3, TP4, TPA, TPB, TPE1, TPE2, TPE3, TPE4, TPOS, TPRO, TPUB, TRC, TRCK, TRD, TRDA, TRK, TRSN, TRSO, TS2, TSA, TSC, TSI, TSIZ, TSO2, TSOA, TSOC, TSOP, TSOT, TSP, TSRC, TSS, TSSE, TSST, TST, TT1, TT2, TT3, TXT, TXX, TXXX, TYE, TYER, UFI, UFID, ULT, USER, USLT, WAF, WAR, WAS, WCM, WCOM, WCOP, WCP, WFED, WOAF, WOAR, WOAS, WORS, WPAY, WPB, WPUB, WXX, WXXX) # deprecated from ._util import ID3EncryptionUnsupportedError, ID3JunkFrameError, \ ID3BadUnsynchData, ID3BadCompressedData, ID3TagError, ID3Warning, \ BitPaddedInt as _BitPaddedIntForPicard # support open(filename) as interface Open = ID3 # flake8 ID3, ID3FileType, delete, ID3v1SaveOptions, Encoding, PictureType, CTOCFlags, ID3TimeStamp, Frames, Frames_2_2, Frame, TextFrame, UrlFrame, UrlFrameU, TimeStampTextFrame, BinaryFrame, NumericPartTextFrame, NumericTextFrame, PairedTextFrame, ID3NoHeaderError, error, ID3UnsupportedVersionError, ParseID3v1, MakeID3v1, ID3Tags, ID3EncryptionUnsupportedError, ID3JunkFrameError, ID3BadUnsynchData, ID3BadCompressedData, ID3TagError, ID3Warning AENC, APIC, ASPI, BUF, CHAP, CNT, COM, COMM, COMR, CRA, CRM, CTOC, ENCR, EQU2, ETC, ETCO, GEO, GEOB, GP1, GRID, GRP1, IPL, IPLS, LINK, LNK, MCDI, MCI, MLL, MLLT, MVI, MVIN, MVN, MVNM, OWNE, PCNT, PCST, PIC, POP, POPM, POSS, PRIV, RBUF, REV, RVA, RVA2, RVAD, RVRB, SEEK, SIGN, SLT, STC, SYLT, SYTC, TAL, TALB, TBP, TBPM, TCAT, TCM, TCMP, TCO, TCOM, TCON, TCOP, TCP, TCR, TDA, TDAT, TDEN, TDES, TDLY, TDOR, TDRC, TDRL, TDTG, TDY, TEN, TENC, TEXT, TFLT, TFT, TGID, TIM, TIME, TIPL, TIT1, TIT2, TIT3, TKE, TKEY, TKWD, TLA, TLAN, TLE, TLEN, TMCL, TMED, TMOO, TMT, TOA, TOAL, TOF, TOFN, TOL, TOLY, TOPE, TOR, TORY, TOT, TOWN, TP1, TP2, TP3, TP4, TPA, TPB, TPE1, TPE2, TPE3, TPE4, TPOS, TPRO, TPUB, TRC, TRCK, TRD, TRDA, TRK, TRSN, TRSO, TS2, TSA, TSC, TSI, TSIZ, TSO2, TSOA, TSOC, TSOP, TSOT, TSP, TSRC, TSS, TSSE, TSST, TST, TT1, TT2, TT3, TXT, TXX, TXXX, TYE, TYER, UFI, UFID, ULT, USER, USLT, WAF, WAR, WAS, WCM, WCOM, WCOP, WCP, WFED, WOAF, WOAR, WOAS, WORS, WPAY, WPB, WPUB, WXX, WXXX # Workaround for http://tickets.musicbrainz.org/browse/PICARD-833 class _DummySpecForPicard(object): write = None EncodedTextSpec = MultiSpec = _DummySpecForPicard BitPaddedInt = _BitPaddedIntForPicard __all__ = ['ID3', 'ID3FileType', 'Frames', 'Open', 'delete'] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1592725327.0 mutagen-1.45.1/mutagen/id3/_file.py0000664000175000017500000003004100000000000017240 0ustar00lazkalazka00000000000000# -*- 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 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. import struct import mutagen from mutagen._util import insert_bytes, delete_bytes, enum, \ loadfile, convert_error, read_full from mutagen._tags import PaddingInfo from ._util import error, ID3NoHeaderError, ID3UnsupportedVersionError, \ BitPaddedInt from ._tags import ID3Tags, ID3Header, ID3SaveConfig from ._id3v1 import MakeID3v1, find_id3v1 @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""" class ID3(ID3Tags, mutagen.Metadata): """ID3(filething=None) A file with an ID3v2 tag. If any arguments are given, the :meth:`load` is called with them. If no arguments are given then an empty `ID3` object is created. :: ID3("foo.mp3") # same as t = ID3() t.load("foo.mp3") Arguments: filething (filething): or `None` Attributes: version (tuple[int]): ID3 tag version as a tuple unknown_frames (list[bytes]): raw frame data of any unknown frames found size (int): the total size of the ID3 tag, including the header """ __module__ = "mutagen.id3" PEDANTIC = True """`bool`: .. deprecated:: 1.28 Doesn't have any effect """ filename = None def __init__(self, *args, **kwargs): self._header = None self._version = (2, 4, 0) super(ID3, self).__init__(*args, **kwargs) @property def version(self): 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 @convert_error(IOError, error) @loadfile() def load(self, filething, known_frames=None, translate=True, v2_version=4, load_v1=True): """Load tags from a filename. Args: filename (filething): filename or file object to load tag data from known_frames (Dict[`mutagen.text`, `Frame`]): dict mapping frame IDs to Frame objects translate (bool): 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 (int): if update_to_v23 or update_to_v24 get called (3 or 4) load_v1 (bool): Load tags from ID3v1 header if present. If both ID3v1 and ID3v2 headers are present, combine the tags from the two, with ID3v2 having precedence. .. versionadded:: 1.42 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) """ fileobj = filething.fileobj if v2_version not in (3, 4): raise ValueError("Only 3 and 4 possible for v2_version") self.unknown_frames = [] self._header = None self._padding = 0 self._pre_load_header(fileobj) try: self._header = ID3Header(fileobj) except (ID3NoHeaderError, ID3UnsupportedVersionError): if not load_v1: raise frames, offset = find_id3v1(fileobj, v2_version, known_frames) if frames is None: raise self.version = ID3Header._V11 for v in frames.values(): if len(self.getall(v.HashKey)) == 0: self.add(v) else: # XXX: attach to the header object so we have it in spec parsing.. if known_frames is not None: self._header._known_frames = known_frames data = read_full(fileobj, self.size - 10) remaining_data = self._read(self._header, data) self._padding = len(remaining_data) if load_v1: v1v2_ver = 4 if self.version[1] == 4 else 3 frames, offset = find_id3v1(fileobj, v1v2_ver, known_frames) if frames: for v in frames.values(): if len(self.getall(v.HashKey)) == 0: self.add(v) if translate: if v2_version == 3: self.update_to_v23() else: self.update_to_v24() def _prepare_data(self, fileobj, start, available, v2_version, v23_sep, pad_func): if v2_version not in (3, 4): raise ValueError("Only 3 or 4 allowed for v2_version") config = ID3SaveConfig(v2_version, v23_sep) framedata = self._write(config) needed = 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 = struct.pack( '>3sBBB4s', b'ID3', v2_version, 0, 0, new_framesize) data = header + framedata assert new_size >= len(data) data += (new_size - len(data)) * b'\x00' assert new_size == len(data) return data @convert_error(IOError, error) @loadfile(writable=True, create=True) def save(self, filething=None, v1=1, v2_version=4, v23_sep='/', padding=None): """save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None) Save changes to a file. Args: filething (filething): 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 (text): 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 (:obj:`mutagen.PaddingFunction`) Raises: mutagen.MutagenError 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. """ f = filething.fileobj try: header = ID3Header(filething.fileobj) 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) 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() @loadfile(writable=True) def delete(self, filething=None, delete_v1=True, delete_v2=True): """delete(filething=None, delete_v1=True, delete_v2=True) Remove tags from a file. Args: filething (filething): A filename or `None` to use the one used when loading. delete_v1 (bool): delete any ID3v1 tag delete_v2 (bool): delete any ID3v2 tag If no filename is given, the one most recently loaded is used. """ delete(filething, delete_v1, delete_v2) self.clear() @convert_error(IOError, error) @loadfile(method=False, writable=True) def delete(filething, delete_v1=True, delete_v2=True): """Remove tags from a file. Args: delete_v1 (bool): delete any ID3v1 tag delete_v2 (bool): delete any ID3v2 tag Raises: mutagen.MutagenError: In case deleting failed """ f = filething.fileobj 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 = struct.unpack('>3sBBB4s', idata) except struct.error: pass else: insize = BitPaddedInt(insize) if id3 == b'ID3' and insize >= 0: delete_bytes(f, insize + 10, 0) class ID3FileType(mutagen.FileType): """ID3FileType(filething, ID3=None, **kwargs) An unknown type of file with ID3 tags. Args: filething (filething): A filename or file-like object ID3 (ID3): An ID3 subclass to use for tags. Raises: mutagen.MutagenError: In case loading the file failed 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. """ __module__ = "mutagen.id3" ID3 = ID3 class _Info(mutagen.StreamInfo): length = 0 def __init__(self, fileobj, offset): pass @staticmethod def pprint(): return u"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. Args: ID3 (ID3): An ID3 subclass to use or `None` to use the one that used when loading. A custom tag reader may be used in instead of the default `ID3` object, e.g. an `mutagen.easyid3.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") @loadfile() def load(self, filething, ID3=None, **kwargs): # see __init__ for docs fileobj = filething.fileobj 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 try: self.tags = ID3(fileobj, **kwargs) except ID3NoHeaderError: self.tags = None if self.tags is not None: try: offset = self.tags.size except AttributeError: offset = None else: offset = None self.info = self._Info(fileobj, offset) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/id3/_frames.py0000664000175000017500000013667300000000000017620 0ustar00lazkalazka00000000000000# -*- 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 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. import zlib from struct import unpack from ._util import ID3JunkFrameError, ID3EncryptionUnsupportedError, unsynch, \ ID3SaveConfig, error from ._specs import BinaryDataSpec, StringSpec, Latin1TextSpec, \ EncodedTextSpec, ByteSpec, EncodingSpec, ASPIIndexSpec, SizedIntegerSpec, \ IntegerSpec, Encoding, VolumeAdjustmentsSpec, VolumePeakSpec, \ VolumeAdjustmentSpec, ChannelSpec, MultiSpec, SynchronizedTextSpec, \ KeyEventSpec, TimeStampSpec, EncodedNumericPartTextSpec, \ EncodedNumericTextSpec, SpecError, PictureTypeSpec, ID3FramesSpec, \ Latin1TextListSpec, CTOCFlagsSpec, FrameIDSpec, RVASpec 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 = [] _optionalspec = [] 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 zip(self._framespec, args): setattr(self, checker.name, val) for checker in self._framespec[len(args):]: setattr(self, checker.name, kwargs.get(checker.name, checker.default)) for spec in self._optionalspec: if spec.name in kwargs: setattr(self, spec.name, kwargs[spec.name]) else: break def __setattr__(self, name, value): for checker in self._framespec: if checker.name == name: self._setattr(name, checker.validate(self, value)) return for checker in self._optionalspec: if checker.name == name: self._setattr(name, checker.validate(self, value)) return super(Frame, self).__setattr__(name, value) def _setattr(self, name, value): self.__dict__[name] = value 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: other._setattr(checker.name, getattr(self, checker.name)) # 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): other._setattr(checker.name, getattr(self, checker.name)) def _merge_frame(self, other): # default impl, use the new tag over the old one return other def _upgrade_frame(self): """Returns either this instance or a new instance if this is a v2.2 frame and an upgrade to a v2.3/4 equivalent is viable. If this is a v2.2 instance and there is no upgrade path, returns None. """ # turn 2.2 into 2.3/2.4 tags if len(type(self).__name__) == 3: base = type(self).__base__ if base is Frame: return return base(self) else: return self 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) for checker in self._optionalspec: name = checker.name if hasattr(self, 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))) 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)) def _readData(self, id3, data): """Raises ID3JunkFrameError; Returns leftover data""" for reader in self._framespec: if len(data) or reader.handle_nodata: try: value, data = reader.read(id3, self, data) except SpecError as e: raise ID3JunkFrameError(e) else: raise ID3JunkFrameError("no data left") self._setattr(reader.name, value) for reader in self._optionalspec: if len(data) or reader.handle_nodata: try: value, data = reader.read(id3, self, data) except SpecError as e: raise ID3JunkFrameError(e) else: break self._setattr(reader.name, value) return data def _writeData(self, config=None): """Raises error""" if config is None: config = ID3SaveConfig() if config.v2_version == 3: frame = self._get_v23_frame(sep=config.v23_separator) else: frame = self data = [] for writer in self._framespec: try: data.append( writer.write(config, frame, getattr(frame, writer.name))) except SpecError as e: raise error(e) for writer in self._optionalspec: try: data.append( writer.write(config, frame, getattr(frame, writer.name))) except AttributeError: break except SpecError as e: raise error(e) 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, header, 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 header.version >= header._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 header.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://github.com/quodlibet/mutagen/issues/210 # https://github.com/quodlibet/mutagen/issues/223 pass if tflags & Frame.FLAG24_ENCRYPT: raise ID3EncryptionUnsupportedError if tflags & Frame.FLAG24_COMPRESS: try: data = zlib.decompress(data) except zlib.error: # 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 header.version >= header._V23: if tflags & Frame.FLAG23_COMPRESS: if len(data) < 4: raise ID3JunkFrameError('frame too small: %r' % data) 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(header, data) return frame def __hash__(self): raise TypeError("Frame objects are unhashable") class CHAP(Frame): """Chapter""" _framespec = [ Latin1TextSpec("element_id"), SizedIntegerSpec("start_time", 4, default=0), SizedIntegerSpec("end_time", 4, default=0), SizedIntegerSpec("start_offset", 4, default=0xffffffff), SizedIntegerSpec("end_offset", 4, default=0xffffffff), ID3FramesSpec("sub_frames"), ] @property def HashKey(self): return '%s:%s' % (self.FrameID, self.element_id) def __eq__(self, other): if not isinstance(other, CHAP): return False self_frames = self.sub_frames or {} other_frames = other.sub_frames or {} if sorted(self_frames.values()) != sorted(other_frames.values()): return False return self.element_id == other.element_id and \ self.start_time == other.start_time and \ self.end_time == other.end_time and \ self.start_offset == other.start_offset and \ self.end_offset == other.end_offset __hash__ = Frame.__hash__ def _pprint(self): frame_pprint = u"" for frame in self.sub_frames.values(): for line in frame.pprint().splitlines(): frame_pprint += "\n" + " " * 4 + line return u"%s time=%d..%d offset=%d..%d%s" % ( self.element_id, self.start_time, self.end_time, self.start_offset, self.end_offset, frame_pprint) class CTOC(Frame): """Table of contents""" _framespec = [ Latin1TextSpec("element_id"), CTOCFlagsSpec("flags", default=0), Latin1TextListSpec("child_element_ids"), ID3FramesSpec("sub_frames"), ] @property def HashKey(self): return '%s:%s' % (self.FrameID, self.element_id) __hash__ = Frame.__hash__ def __eq__(self, other): if not isinstance(other, CTOC): return False self_frames = self.sub_frames or {} other_frames = other.sub_frames or {} if sorted(self_frames.values()) != sorted(other_frames.values()): return False return self.element_id == other.element_id and \ self.flags == other.flags and \ self.child_element_ids == other.child_element_ids def _pprint(self): frame_pprint = u"" if getattr(self, "sub_frames", None): frame_pprint += "\n" + "\n".join( [" " * 4 + f.pprint() for f in self.sub_frames.values()]) return u"%s flags=%d child_element_ids=%s%s" % ( self.element_id, int(self.flags), u",".join(self.child_element_ids), frame_pprint) 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', default=Encoding.UTF16), MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000', default=[]), ] def __bytes__(self): return str(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, str): return str(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 _merge_frame(self, other): # merge in new values for val in other[:]: if val not in self: self.append(val) self.encoding = max(self.encoding, other.encoding) return self 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', default=Encoding.UTF16), MultiSpec('text', EncodedNumericTextSpec('text'), sep=u'\u0000', default=[]), ] 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', default=Encoding.UTF16), MultiSpec('text', EncodedNumericPartTextSpec('text'), sep=u'\u0000', default=[]), ] def __pos__(self): return int(self.text[0].split("/")[0]) class TimeStampTextFrame(TextFrame): """A list of time stamps. The 'text' attribute in this frame is a list of ID3TimeStamp objects, not a list of strings. """ _framespec = [ EncodingSpec('encoding', default=Encoding.UTF16), MultiSpec('text', TimeStampSpec('stamp'), sep=u',', default=[]), ] def __bytes__(self): return str(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]) 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 = str(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, str): 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 TKWD(TextFrame): "iTunes Podcast Keywords" class TCAT(TextFrame): "iTunes Podcast Category" class MVNM(TextFrame): "iTunes Movement Name" class MVN(MVNM): "iTunes Movement Name" class MVIN(NumericPartTextFrame): "iTunes Movement Number/Count" class MVI(MVIN): "iTunes Movement Number/Count" class GRP1(TextFrame): "iTunes Grouping" class GP1(GRP1): "iTunes Grouping" 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', default=[]), ] @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', default=Encoding.UTF16), 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', default=Encoding.UTF16), MultiSpec('people', EncodedTextSpec('involvement'), EncodedTextSpec('person'), default=[]) ] 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", default=1), KeyEventSpec("events", default=[]), ] 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', size=2, default=0), SizedIntegerSpec('bytes', size=3, default=0), SizedIntegerSpec('milliseconds', size=3, default=0), ByteSpec('bits_for_bytes', default=0), ByteSpec('bits_for_milliseconds', default=0), 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", default=1), BinaryDataSpec("data"), ] def __eq__(self, other): return self.data == other __hash__ = Frame.__hash__ class USLT(Frame): """Unsynchronised lyrics/text transcription. Lyrics have a three letter ISO language code ('lang'), a description ('desc'), and a block of plain text ('text'). """ _framespec = [ EncodingSpec('encoding', default=Encoding.UTF16), StringSpec('lang', length=3, default=u"XXX"), 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__ def _pprint(self): return "%s=%s=%s" % (self.desc, self.lang, self.text) class SYLT(Frame): """Synchronised lyrics/text.""" _framespec = [ EncodingSpec('encoding'), StringSpec('lang', length=3, default=u"XXX"), ByteSpec('format', default=1), ByteSpec('type', default=0), EncodedTextSpec('desc'), SynchronizedTextSpec('text'), ] @property def HashKey(self): return '%s:%s:%s' % (self.FrameID, self.desc, self.lang) def _pprint(self): return str(self) def __eq__(self, other): return str(self) == other __hash__ = Frame.__hash__ def __str__(self): unit = 'fr' if self.format == 1 else 'ms' return u"\n".join("[{0}{1}]: {2}".format(time, unit, text) for (text, time) in self.text) def __bytes__(self): return str(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', length=3, default="XXX"), EncodedTextSpec('desc'), MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000', default=[]), ] @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', default=1), VolumeAdjustmentSpec('gain', default=1), VolumePeakSpec('peak', default=1), ] _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", default=0), Latin1TextSpec("desc"), VolumeAdjustmentsSpec("adjustments", default=[]), ] def __eq__(self, other): return self.adjustments == other __hash__ = Frame.__hash__ @property def HashKey(self): return '%s:%s' % (self.FrameID, self.desc) class RVAD(Frame): """Relative volume adjustment""" _framespec = [ RVASpec("adjustments", stereo_only=False), ] __hash__ = Frame.__hash__ def __eq__(self, other): if not isinstance(other, RVAD): return False return self.adjustments == other.adjustments # class EQUA: unsupported class RVRB(Frame): """Reverb.""" _framespec = [ SizedIntegerSpec('left', size=2, default=0), SizedIntegerSpec('right', size=2, default=0), ByteSpec('bounce_left', default=0), ByteSpec('bounce_right', default=0), ByteSpec('feedback_ltl', default=0), ByteSpec('feedback_ltr', default=0), ByteSpec('feedback_rtr', default=0), ByteSpec('feedback_rtl', default=0), ByteSpec('premix_ltr', default=0), ByteSpec('premix_rtl', default=0), ] 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'), PictureTypeSpec('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 _merge_frame(self, other): other.desc += u" " return other def _pprint(self): type_desc = str(self.type) if hasattr(self.type, "_pprint"): type_desc = self.type._pprint() return "%s, %s (%s, %d bytes)" % ( type_desc, 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', default=0), ] def __eq__(self, other): return self.count == other __hash__ = Frame.__hash__ def __pos__(self): return self.count def _pprint(self): return str(self.count) class PCST(Frame): """iTunes Podcast Flag""" _framespec = [ IntegerSpec('value', default=0), ] def __eq__(self, other): return self.value == other __hash__ = Frame.__hash__ def __pos__(self): return self.value def _pprint(self): return str(self.value) class POPM(Frame): """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', default=0), ] _optionalspec = [ IntegerSpec('count', default=0), ] @property def HashKey(self): return '%s:%s' % (self.FrameID, self.email) def __eq__(self, other): return self.rating == other __hash__ = Frame.__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(Frame): """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', size=3, default=0), ] _optionalspec = [ ByteSpec('info', default=0), SizedIntegerSpec('offset', size=4, default=0), ] def __eq__(self, other): return self.size == other __hash__ = Frame.__hash__ def __pos__(self): return self.size class AENC(Frame): """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', size=2, default=0), SizedIntegerSpec('preview_length', size=2, default=0), 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__ = Frame.__hash__ class LINK(Frame): """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 = [ FrameIDSpec('frameid', length=4), Latin1TextSpec('url'), BinaryDataSpec('data'), ] @property def HashKey(self): return "%s:%s:%s:%s" % ( self.FrameID, self.frameid, self.url, _bytes2key(self.data)) def __eq__(self, other): return (self.frameid, self.url, self.data) == other __hash__ = Frame.__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', default=1), IntegerSpec('position', default=0), ] 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) 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', length=3, default=u"XXX"), 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) class OWNE(Frame): """Ownership frame.""" _framespec = [ EncodingSpec('encoding'), Latin1TextSpec('price'), StringSpec('date', length=8, default=u"19700101"), 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(Frame): """Commercial frame.""" _framespec = [ EncodingSpec('encoding'), Latin1TextSpec('price'), StringSpec('valid_until', length=8, default=u"19700101"), Latin1TextSpec('contact'), ByteSpec('format', default=0), 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__ = Frame.__hash__ class ENCR(Frame): """Encryption method registration. The standard does not allow multiple ENCR frames with the same owner or the same method. Mutagen only verifies that the owner is unique. """ _framespec = [ Latin1TextSpec('owner'), ByteSpec('method', default=0x80), 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__ class GRID(Frame): """Group identification registration.""" _framespec = [ Latin1TextSpec('owner'), ByteSpec('group', default=0x80), 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__ = Frame.__hash__ 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__ class SIGN(Frame): """Signature frame.""" _framespec = [ ByteSpec('group', default=0x80), 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', default=0), ] 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", size=4, default=0), SizedIntegerSpec("L", size=4, default=0), SizedIntegerSpec("N", size=2, default=0), ByteSpec("b", default=0), ASPIIndexSpec("Fi", default=[]), ] 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 TST(TSOT): "Title Sort Order key" class TSA(TSOA): "Album Sort Order key" class TS2(TSO2): "iTunes Album Artist Sort" class TSP(TSOP): "Perfomer Sort Order key" class TSC(TSOC): "iTunes Composer Sort" 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): "Relative volume adjustment" _framespec = [ RVASpec("adjustments", stereo_only=True), ] def _to_other(self, other): if not isinstance(other, RVAD): raise TypeError other.adjustments = list(self.adjustments) # 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', length=3, default="JPG"), PictureTypeSpec('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 = [ FrameIDSpec('frameid', length=3), Latin1TextSpec('url'), BinaryDataSpec('data'), ] def _to_other(self, other): if not isinstance(other, LINK): raise TypeError if isinstance(other, LNK): new_frameid = self.frameid else: try: new_frameid = Frames_2_2[self.frameid].__bases__[0].__name__ except KeyError: new_frameid = self.frameid.ljust(4) # we could end up with invalid IDs here, so bypass the validation other._setattr("frameid", new_frameid) other.url = self.url 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 globals().items(): 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/id3/_id3v1.py0000664000175000017500000001500500000000000017252 0ustar00lazkalazka00000000000000# -*- 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 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. import errno from struct import error as StructError, unpack from mutagen._util import bchr from ._frames import TCON, TRCK, COMM, TDRC, TYER, TALB, TPE1, TIT2 def find_id3v1(fileobj, v2_version=4, known_frames=None): """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. v2_version: Decides whether ID3v2.3 or ID3v2.4 tags should be returned. Must be 3 or 4. known_frames (Dict[`mutagen.text`, `Frame`]): dict mapping frame IDs to Frame objects """ if v2_version not in (3, 4): raise ValueError("Only 3 and 4 possible for v2_version") # id3v1 is always at the end (after apev2) extra_read = b"APETAGEX".index(b"TAG") old_pos = fileobj.tell() 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) fileobj.seek(old_pos, 0) 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:], v2_version, known_frames) if tag is None: return (None, 0) offset = idx - len(data) return (tag, offset) # ID3v1.1 support. def ParseID3v1(data, v2_version=4, known_frames=None): """Parse an ID3v1 tag, returning a list of ID3v2 frames Returns a {frame_name: frame} dict or None. v2_version: Decides whether ID3v2.3 or ID3v2.4 tags should be returned. Must be 3 or 4. known_frames (Dict[`mutagen.text`, `Frame`]): dict mapping frame IDs to Frame objects """ if v2_version not in (3, 4): raise ValueError("Only 3 and 4 possible for v2_version") 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]) frame_class = { "TIT2": TIT2, "TPE1": TPE1, "TALB": TALB, "TYER": TYER, "TDRC": TDRC, "COMM": COMM, "TRCK": TRCK, "TCON": TCON, } for key in frame_class: if known_frames is not None: if key in known_frames: frame_class[key] = known_frames[key] else: frame_class[key] = None frames = {} if title and frame_class["TIT2"]: frames["TIT2"] = frame_class["TIT2"](encoding=0, text=title) if artist and frame_class["TPE1"]: frames["TPE1"] = frame_class["TPE1"](encoding=0, text=[artist]) if album and frame_class["TALB"]: frames["TALB"] = frame_class["TALB"](encoding=0, text=album) if year: if v2_version == 3 and frame_class["TYER"]: frames["TYER"] = frame_class["TYER"](encoding=0, text=year) elif frame_class["TDRC"]: frames["TDRC"] = frame_class["TDRC"](encoding=0, text=year) if comment and frame_class["COMM"]: frames["COMM"] = frame_class["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 frame_class["TRCK"] and ((track != 32) or (data[-3] == b'\x00'[0]))): frames["TRCK"] = TRCK(encoding=0, text=str(track)) if genre != 255 and frame_class["TCON"]: 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"] = bchr(+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"] = bchr(TCON.GENRES.index(genre)) if "genre" not in v1: v1["genre"] = b"\xff" if "TDRC" in id3: year = str(id3["TDRC"]).encode('ascii') elif "TYER" in id3: year = str(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"] ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/id3/_specs.py0000664000175000017500000006112700000000000017447 0ustar00lazkalazka00000000000000# -*- 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 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. import struct import codecs from struct import unpack, pack from .._util import total_ordering, decode_terminated, enum, flags, \ cdata, encode_endian, intround, bchr from ._util import BitPaddedInt, is_valid_frame_id @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""" def _pprint(self): return str(self).split(".", 1)[-1].lower().replace("_", " ") @flags class CTOCFlags(object): TOP_LEVEL = 0x2 """Identifies the CTOC root frame""" ORDERED = 0x1 """Child elements are ordered""" class SpecError(Exception): pass class Spec(object): handle_nodata = False """If reading empty data is possible and writing it back will again result in no data. """ def __init__(self, name, default): self.name = name self.default = default 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, header, frame, data): """ Returns: (value: object, left_data: bytes) Raises: SpecError """ raise NotImplementedError def write(self, config, frame, value): """ Returns: bytes: The serialized data Raises: SpecError """ raise NotImplementedError def validate(self, frame, value): """ Returns: the validated value Raises: ValueError TypeError """ raise NotImplementedError class ByteSpec(Spec): def __init__(self, name, default=0): super(ByteSpec, self).__init__(name, default) def read(self, header, frame, data): return bytearray(data)[0], data[1:] def write(self, config, frame, value): return bchr(value) def validate(self, frame, value): if value is not None: bchr(value) return value class PictureTypeSpec(ByteSpec): def __init__(self, name, default=PictureType.COVER_FRONT): super(PictureTypeSpec, self).__init__(name, default) def read(self, header, frame, data): value, data = ByteSpec.read(self, header, frame, data) return PictureType(value), data def validate(self, frame, value): value = ByteSpec.validate(self, frame, value) if value is not None: return PictureType(value) return value class CTOCFlagsSpec(ByteSpec): def read(self, header, frame, data): value, data = ByteSpec.read(self, header, frame, data) return CTOCFlags(value), data def validate(self, frame, value): value = ByteSpec.validate(self, frame, value) if value is not None: return CTOCFlags(value) return value class IntegerSpec(Spec): def read(self, header, frame, data): return int(BitPaddedInt(data, bits=8)), b'' def write(self, config, 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, default): self.name, self.__sz = name, size self.default = default def read(self, header, frame, data): return int(BitPaddedInt(data[:self.__sz], bits=8)), data[self.__sz:] def write(self, config, 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 __init__(self, name, default=Encoding.UTF16): super(EncodingSpec, self).__init__(name, default) def read(self, header, frame, data): enc, data = super(EncodingSpec, self).read(header, frame, data) if enc not in (Encoding.LATIN1, Encoding.UTF16, Encoding.UTF16BE, Encoding.UTF8): raise SpecError('Invalid Encoding: %r' % enc) return Encoding(enc), data def validate(self, frame, value): if value is None: raise TypeError if value not in (Encoding.LATIN1, Encoding.UTF16, Encoding.UTF16BE, Encoding.UTF8): raise ValueError('Invalid Encoding: %r' % value) return Encoding(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, default=None): if default is None: default = u" " * length super(StringSpec, self).__init__(name, default) self.len = length def read(s, header, frame, data): chunk = data[:s.len] try: ascii = chunk.decode("ascii") except UnicodeDecodeError: raise SpecError("not ascii") else: chunk = ascii return chunk, data[s.len:] def write(self, config, frame, value): value = value.encode("ascii") return (bytes(value) + b'\x00' * self.len)[:self.len] def validate(self, frame, value): if value is None: raise TypeError if not isinstance(value, str): raise TypeError("%s has to be str" % self.name) value.encode("ascii") if len(value) == self.len: return value raise ValueError('Invalid StringSpec[%d] data: %r' % (self.len, value)) class RVASpec(Spec): def __init__(self, name, stereo_only, default=[0, 0]): # two_chan: RVA has only 2 channels, while RVAD has 6 channels super(RVASpec, self).__init__(name, default) self._max_values = 4 if stereo_only else 12 def read(self, header, frame, data): # inc/dec flags spec = ByteSpec("flags", 0) flags, data = spec.read(header, frame, data) if not data: raise SpecError("truncated") # how many bytes per value bits, data = spec.read(header, frame, data) if bits == 0: # not allowed according to spec raise SpecError("bits used has to be > 0") bytes_per_value = (bits + 7) // 8 values = [] while len(data) >= bytes_per_value and len(values) < self._max_values: v = BitPaddedInt(data[:bytes_per_value], bits=8) data = data[bytes_per_value:] values.append(v) if len(values) < 2: raise SpecError("First two values not optional") # if the respective flag bit is zero, take as decrement for bit, index in enumerate([0, 1, 4, 5, 8, 10]): if not cdata.test_bit(flags, bit): try: values[index] = -values[index] except IndexError: break return values, data def write(self, config, frame, values): if len(values) < 2 or len(values) > self._max_values: raise SpecError( "at least two volume change values required, max %d" % self._max_values) spec = ByteSpec("flags", 0) flags = 0 values = list(values) for bit, index in enumerate([0, 1, 4, 5, 8, 10]): try: if values[index] < 0: values[index] = -values[index] else: flags |= (1 << bit) except IndexError: break buffer_ = bytearray() buffer_.extend(spec.write(config, frame, flags)) # serialized and make them all the same size (min 2 bytes) byte_values = [ BitPaddedInt.to_str(v, bits=8, width=-1, minwidth=2) for v in values] max_bytes = max([len(v) for v in byte_values]) byte_values = [v.ljust(max_bytes, b"\x00") for v in byte_values] bits = max_bytes * 8 buffer_.extend(spec.write(config, frame, bits)) for v in byte_values: buffer_.extend(v) return bytes(buffer_) def validate(self, frame, values): if len(values) < 2 or len(values) > self._max_values: raise ValueError("needs list of length 2..%d" % self._max_values) return values class FrameIDSpec(StringSpec): def __init__(self, name, length): super(FrameIDSpec, self).__init__(name, length, u"X" * length) def validate(self, frame, value): value = super(FrameIDSpec, self).validate(frame, value) if not is_valid_frame_id(value): raise ValueError("Invalid frame ID") return value class BinaryDataSpec(Spec): handle_nodata = True def __init__(self, name, default=b""): super(BinaryDataSpec, self).__init__(name, default) def read(self, header, frame, data): return data, b'' def write(self, config, frame, value): if isinstance(value, bytes): return value value = str(value).encode("ascii") return value def validate(self, frame, value): if value is None: raise TypeError if isinstance(value, bytes): return value else: raise TypeError("%s has to be bytes" % self.name) value = str(value).encode("ascii") return value def iter_text_fixups(data, encoding): """Yields a series of repaired text values for decoding""" yield data if encoding == Encoding.UTF16BE: # wrong termination yield data + b"\x00" elif encoding == Encoding.UTF16: # wrong termination yield data + b"\x00" # utf-16 is missing BOM, content is usually utf-16-le yield codecs.BOM_UTF16_LE + data # both cases combined yield codecs.BOM_UTF16_LE + data + b"\x00" 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 __init__(self, name, default=u""): super(EncodedTextSpec, self).__init__(name, default) def read(self, header, frame, data): enc, term = self._encodings[frame.encoding] err = None for data in iter_text_fixups(data, frame.encoding): try: value, data = decode_terminated(data, enc, strict=False) except ValueError as e: err = e else: # Older id3 did not support multiple values, but we still # read them. To not missinterpret zero padded values with # a list of empty strings, stop if everything left is zero. # https://github.com/quodlibet/mutagen/issues/276 if header.version < header._V24 and not data.strip(b"\x00"): data = b"" return value, data raise SpecError(err) def write(self, config, frame, value): enc, term = self._encodings[frame.encoding] try: return encode_endian(value, enc, le=True) + term except UnicodeEncodeError as e: raise SpecError(e) def validate(self, frame, value): return str(value) class MultiSpec(Spec): def __init__(self, name, *specs, **kw): super(MultiSpec, self).__init__(name, default=kw.get('default')) self.specs = specs self.sep = kw.get('sep') def read(self, header, frame, data): values = [] while data: record = [] for spec in self.specs: value, data = spec.read(header, frame, data) record.append(value) if len(self.specs) != 1: values.append(record) else: values.append(record[0]) return values, data def write(self, config, frame, value): data = [] if len(self.specs) == 1: for v in value: data.append(self.specs[0].write(config, frame, v)) else: for record in value: for v, s in zip(record, self.specs): data.append(s.write(config, frame, v)) return b''.join(data) def validate(self, frame, value): if self.sep and isinstance(value, str): value = value.split(self.sep) if isinstance(value, list): if len(self.specs) == 1: return [self.specs[0].validate(frame, v) for v in value] else: return [ [s.validate(frame, v) for (v, s) in zip(val, self.specs)] for val in value] raise ValueError('Invalid MultiSpec data: %r' % value) def _validate23(self, frame, value, **kwargs): if len(self.specs) != 1: return [[s._validate23(frame, v, **kwargs) for (v, s) in zip(val, self.specs)] for val in value] spec = self.specs[0] # Merge single text spec multispecs only. # (TimeStampSpec beeing the exception, but it's not a valid v2.3 frame) if not isinstance(spec, EncodedTextSpec) or \ isinstance(spec, TimeStampSpec): return value value = [spec._validate23(frame, v, **kwargs) for v in value] if kwargs.get("sep") is not None: return [spec.validate(frame, kwargs["sep"].join(value))] return value class EncodedNumericTextSpec(EncodedTextSpec): pass class EncodedNumericPartTextSpec(EncodedTextSpec): pass class Latin1TextSpec(Spec): def __init__(self, name, default=u""): super(Latin1TextSpec, self).__init__(name, default) def read(self, header, 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, config, data, value): return value.encode('latin1') + b'\x00' def validate(self, frame, value): return str(value) class ID3FramesSpec(Spec): handle_nodata = True def __init__(self, name, default=[]): super(ID3FramesSpec, self).__init__(name, default) def read(self, header, frame, data): from ._tags import ID3Tags tags = ID3Tags() return tags, tags._read(header, data) def _validate23(self, frame, value, **kwargs): from ._tags import ID3Tags v = ID3Tags() for frame in value.values(): v.add(frame._get_v23_frame(**kwargs)) return v def write(self, config, frame, value): return bytes(value._write(config)) def validate(self, frame, value): from ._tags import ID3Tags if isinstance(value, ID3Tags): return value tags = ID3Tags() for v in value: tags.add(v) return tags class Latin1TextListSpec(Spec): def __init__(self, name, default=[]): super(Latin1TextListSpec, self).__init__(name, default) self._bspec = ByteSpec("entry_count", default=0) self._lspec = Latin1TextSpec("child_element_id") def read(self, header, frame, data): count, data = self._bspec.read(header, frame, data) entries = [] for i in range(count): entry, data = self._lspec.read(header, frame, data) entries.append(entry) return entries, data def write(self, config, frame, value): b = self._bspec.write(config, frame, len(value)) for v in value: b += self._lspec.write(config, frame, v) return b def validate(self, frame, value): return [self._lspec.validate(frame, v) for v in value] @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, str): raise TypeError("not a str") 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 isinstance(other, ID3TimeStamp) and 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, header, frame, data): value, data = super(TimeStampSpec, self).read(header, frame, data) return self.validate(frame, value), data def write(self, config, frame, data): return super(TimeStampSpec, self).write(config, frame, data.text.replace(' ', 'T')) def validate(self, frame, value): try: return ID3TimeStamp(value) except TypeError: raise ValueError("Invalid ID3TimeStamp: %r" % value) class ChannelSpec(ByteSpec): (OTHER, MASTER, FRONTRIGHT, FRONTLEFT, BACKRIGHT, BACKLEFT, FRONTCENTRE, BACKCENTRE, SUBWOOFER) = range(9) class VolumeAdjustmentSpec(Spec): def read(self, header, frame, data): value, = unpack('>h', data[0:2]) return value / 512.0, data[2:] def write(self, config, frame, value): number = intround(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(None, frame, value) except SpecError: raise ValueError("out of range") return value class VolumePeakSpec(Spec): def read(self, header, 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 range(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, config, frame, value): number = intround(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(None, frame, value) except SpecError: raise ValueError("out of range") return value class SynchronizedTextSpec(EncodedTextSpec): def read(self, header, 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, config, frame, value): data = [] encoding, term = self._encodings[frame.encoding] for text, time in value: try: text = encode_endian(text, encoding, le=True) + term except UnicodeEncodeError as e: raise SpecError(e) data.append(text + struct.pack(">I", time)) return b"".join(data) def validate(self, frame, value): return value class KeyEventSpec(Spec): def read(self, header, frame, data): events = [] while len(data) >= 5: events.append(struct.unpack(">bI", data[:5])) data = data[5:] return events, data def write(self, config, frame, value): return b"".join(struct.pack(">bI", *event) for event in value) def validate(self, frame, value): return list(value) class VolumeAdjustmentsSpec(Spec): # Not to be confused with VolumeAdjustmentSpec. def read(self, header, 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, config, 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 list(value) class ASPIIndexSpec(Spec): def read(self, header, 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, config, 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 list(values) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/id3/_tags.py0000664000175000017500000005053300000000000017267 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2005 Michael Urman # Copyright 2016 Christoph Reiter # # 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. import re import struct from itertools import zip_longest from mutagen._tags import Tags from mutagen._util import DictProxy, convert_error, read_full from ._util import BitPaddedInt, unsynch, ID3JunkFrameError, \ ID3EncryptionUnsupportedError, is_valid_frame_id, error, \ ID3NoHeaderError, ID3UnsupportedVersionError, ID3SaveConfig from ._frames import TDRC, APIC, TDOR, TIME, TIPL, TORY, TDAT, Frames_2_2, \ TextFrame, TYER, Frame, IPLS, Frames 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)) _known_frames = None @property def known_frames(self): if self._known_frames is not None: return self._known_frames elif self.version >= ID3Header._V23: return Frames elif self.version >= ID3Header._V22: return Frames_2_2 @convert_error(IOError, error) def __init__(self, fileobj=None): """Raises ID3NoHeaderError, ID3UnsupportedVersionError or error""" if fileobj is None: # for testing self._flags = 0 return fn = getattr(fileobj, "name", "") data = fileobj.read(10) if len(data) != 10: raise ID3NoHeaderError("%s: too small" % fn) id3, vmaj, vrev, flags, size = struct.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: extsize_data = read_full(fileobj, 4) frame_id = extsize_data.decode("ascii", "replace") 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. # https://github.com/quodlibet/quodlibet/issues/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 = struct.unpack('>L', extsize_data)[0] self._extdata = read_full(fileobj, extsize) 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 = struct.unpack('>4sLH', part) size = BitPaddedInt(size) o += 10 + size 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 = struct.unpack('>4sLH', part) o += 10 + size 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 class ID3Tags(DictProxy, Tags): __module__ = "mutagen.id3" def __init__(self, *args, **kwargs): self.unknown_frames = [] self._unknown_v2_version = 4 super(ID3Tags, self).__init__(*args, **kwargs) def _read(self, header, data): frames, unknown_frames, data = read_frames( header, data, header.known_frames) for frame in frames: self._add(frame, False) self.unknown_frames = unknown_frames self._unknown_v2_version = header.version[1] return data def _write(self, config): # Sort frames by 'importance', then reverse frame size and then frame # hash to get a stable result order = ["TIT2", "TPE1", "TRCK", "TALB", "TPOS", "TDRC", "TCON"] framedata = [ (f, save_frame(f, config=config)) for f in self.values()] def get_prio(frame): try: return order.index(frame.FrameID) except ValueError: return len(order) def sort_key(items): frame, data = items return (get_prio(frame), len(data), frame.HashKey) framedata = [d for (f, d) in sorted(framedata, key=sort_key)] # only write unknown frames if they were loaded from the version # we are saving with. Theoretically we could upgrade frames # but some frames can be nested like CHAP, so there is a chance # we create a mixed frame mess. if self._unknown_v2_version == config.v2_version: framedata.extend(data for data in self.unknown_frames if len(data) > 10) return bytearray().join(framedata) def getall(self, key): """Return all frames with a given name (the list may be empty). Args: key (text): key for frames to get 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 setall(self, key, values): """Delete frames of the given type and add frames in 'values'. Args: key (text): key for frames to delete values (list[Frame]): frames to add """ self.delall(key) for tag in values: self[tag.HashKey] = tag def delall(self, key): """Delete all tags of a given kind; see getall. Args: key (text): key for frames to delete """ if key in self: del(self[key]) else: key = key + ":" for k in list(self.keys()): if k.startswith(key): del(self[k]) def pprint(self): """ Returns: text: 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 _add(self, frame, strict): """Add a frame. Args: frame (Frame): the frame to add strict (bool): if this should raise in case it can't be added and frames shouldn't be merged. """ if not isinstance(frame, Frame): raise TypeError("%r not a Frame instance" % frame) orig_frame = frame frame = frame._upgrade_frame() if frame is None: if not strict: return raise TypeError( "Can't upgrade %r frame" % type(orig_frame).__name__) hash_key = frame.HashKey if strict or hash_key not in self: self[hash_key] = frame return # Try to merge frames, or change the new one. Since changing # the new one can lead to new conflicts, try until everything is # either merged or added. while True: old_frame = self[hash_key] new_frame = old_frame._merge_frame(frame) new_hash = new_frame.HashKey if new_hash == hash_key: self[hash_key] = new_frame break else: assert new_frame is frame if new_hash not in self: self[new_hash] = new_frame break hash_key = new_hash def loaded_frame(self, tag): """Deprecated; use the add method.""" self._add(tag, True) def add(self, frame): """Add a frame to the 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. self.loaded_frame(frame) def __setitem__(self, key, tag): if not isinstance(tag, Frame): raise TypeError("%r not a Frame instance" % tag) super(ID3Tags, self).__setitem__(key, tag) 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 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() # TDAT, TYER, and TIME have been turned into TDRC. timestamps = [] old_frames = [self.pop(n, []) for n in ["TYER", "TDAT", "TIME"]] for y, d, t in zip_longest(*old_frames, fillvalue=u""): ym = re.match(r"([0-9]+)\Z", y) dm = re.match(r"([0-9]{2})([0-9]{2})\Z", d) tm = re.match(r"([0-9]{2})([0-9]{2})\Z", t) timestamp = "" if ym: timestamp += u"%s" % ym.groups() if dm: timestamp += u"-%s-%s" % dm.groups()[::-1] if tm: timestamp += u"T%s:%s:00" % tm.groups() if timestamp: timestamps.append(timestamp) if timestamps and "TDRC" not in self: self.add(TDRC(encoding=0, text=timestamps)) # 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"]: if key in self: del(self[key]) # Recurse into chapters for f in self.getall("CHAP"): f.sub_frames.update_to_v24() for f in self.getall("CTOC"): f.sub_frames.update_to_v24() 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() # 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]) # Recurse into chapters for f in self.getall("CHAP"): f.sub_frames.update_to_v23() for f in self.getall("CTOC"): f.sub_frames.update_to_v23() def _copy(self): """Creates a shallow copy of all tags""" items = self.items() subs = {} for f in (self.getall("CHAP") + self.getall("CTOC")): subs[f.HashKey] = f.sub_frames._copy() return (items, subs) def _restore(self, value): """Restores the state copied with _copy()""" items, subs = value self.clear() for key, value in items: self[key] = value if key in subs: value.sub_frames._restore(subs[key]) def save_frame(frame, name=None, config=None): if config is None: config = ID3SaveConfig() flags = 0 if isinstance(frame, TextFrame): if len(str(frame)) == 0: return b'' framedata = frame._writeData(config) 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 config.v2_version == 4: bits = 7 elif config.v2_version == 3: 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__ frame_name = frame_name.encode("ascii") header = struct.pack('>4s4sH', frame_name, datasize, flags) return header + framedata def read_frames(id3, data, frames): """Does not error out""" assert id3.version >= ID3Header._V22 result = [] unsupported_frames = [] if id3.version < ID3Header._V24 and id3.f_unsynch: try: data = unsynch.decode(data) except ValueError: pass if id3.version >= ID3Header._V23: if id3.version < ID3Header._V24: bpi = int else: bpi = determine_bpi(data, frames) while data: header = data[:10] try: name, size, flags = struct.unpack('>4sLH', header) except struct.error: break # not enough header if name.strip(b'\x00') == b'': break size = bpi(size) framedata = data[10:10 + size] data = data[10 + size:] if size == 0: continue # drop empty frames 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): unsupported_frames.append(header + framedata) else: try: result.append(tag._fromData(id3, flags, framedata)) except NotImplementedError: unsupported_frames.append(header + framedata) except ID3JunkFrameError: pass elif id3.version >= ID3Header._V22: while data: header = data[0:6] try: name, size = struct.unpack('>3s3s', header) except struct.error: break # not enough header size, = struct.unpack('>L', b'\x00' + size) if name.strip(b'\x00') == b'': break framedata = data[6:6 + size] data = data[6 + size:] if size == 0: continue # drop empty frames try: name = name.decode('ascii') except UnicodeDecodeError: continue try: tag = frames[name] except KeyError: if is_valid_frame_id(name): unsupported_frames.append(header + framedata) else: try: result.append( tag._fromData(id3, 0, framedata)) except (ID3EncryptionUnsupportedError, NotImplementedError): unsupported_frames.append(header + framedata) except ID3JunkFrameError: pass return result, unsupported_frames, data ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/id3/_util.py0000664000175000017500000001050700000000000017303 0ustar00lazkalazka00000000000000# -*- 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 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. from mutagen._util import MutagenError def is_valid_frame_id(frame_id): return frame_id.isalnum() and frame_id.isupper() class ID3SaveConfig(object): def __init__(self, v2_version=4, v23_separator=None): assert v2_version in (3, 4) self.v2_version = v2_version self.v23_separator = v23_separator class error(MutagenError): pass class ID3NoHeaderError(error, ValueError): pass class ID3UnsupportedVersionError(error, NotImplementedError): pass class ID3EncryptionUnsupportedError(error, NotImplementedError): pass class ID3JunkFrameError(error): 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, int): 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, int): if value < 0: raise ValueError 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 self = int.__new__(BitPaddedInt, numeric_value) self.bits = bits self.bigendian = bigendian return self class ID3BadUnsynchData(error, ValueError): """Deprecated""" class ID3BadCompressedData(error, ValueError): """Deprecated""" class ID3TagError(error, ValueError): """Deprecated""" class ID3Warning(error, UserWarning): """Deprecated""" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/mutagen/m4a.py0000644000175000017500000000375700000000000016200 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """ 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, Tags, StreamInfo from ._util import DictProxy, MutagenError, loadfile warnings.warn( "mutagen.m4a is deprecated; use mutagen.mp4 instead.", DeprecationWarning) class error(MutagenError): pass class M4AMetadataError(error): pass class M4AStreamInfoError(error): pass class M4AMetadataValueError(error): 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, Tags): 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"] @loadfile() def load(self, filething): 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") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/monkeysaudio.py0000664000175000017500000000643000000000000020217 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """Monkey's Audio streams with APEv2 tags. Monkey's Audio is a very efficient lossless audio compressor developed by Matt Ashland. For more information, see http://www.monkeysaudio.com/. """ __all__ = ["MonkeysAudio", "Open", "delete"] import struct from mutagen import StreamInfo from mutagen.apev2 import APEv2File, error, delete from mutagen._util import cdata, convert_error, endswith class MonkeysAudioHeaderError(error): pass class MonkeysAudioInfo(StreamInfo): """MonkeysAudioInfo() Monkey's Audio stream information. Attributes: channels (`int`): number of audio channels length (`float`): file length in seconds, as a float sample_rate (`int`): audio sampling rate in Hz bits_per_sample (`int`): bits per sample version (`float`): Monkey's Audio stream version, as a float (eg: 3.99) """ @convert_error(IOError, MonkeysAudioHeaderError) def __init__(self, fileobj): """Raises MonkeysAudioHeaderError""" 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.bits_per_sample = 0 if header[48:].startswith(b"WAVEfmt"): self.bits_per_sample = struct.unpack(" 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): """MonkeysAudio(filething) Arguments: filething (filething) Attributes: info (`MonkeysAudioInfo`) """ _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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1596184724.8191056 mutagen-1.45.1/mutagen/mp3/0000775000175000017500000000000000000000000015632 5ustar00lazkalazka00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/mp3/__init__.py0000664000175000017500000003505500000000000017753 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """MPEG audio stream information and tags.""" import struct from mutagen import StreamInfo from mutagen._util import MutagenError, enum, BitReader, BitReaderError, \ convert_error, intround, endswith from mutagen.id3 import ID3FileType, delete from mutagen.id3._util import BitPaddedInt from ._util import XingHeader, XingHeaderError, VBRIHeader, VBRIHeaderError __all__ = ["MP3", "Open", "delete", "MP3"] class error(MutagenError): pass class HeaderNotFoundError(error): pass class InvalidMPEGHeader(error): 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_desc: return BitrateMode.VBR return BitrateMode.UNKNOWN # Mode values. STEREO, JOINTSTEREO, DUALCHANNEL, MONO = range(4) class MPEGFrame(object): # 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 range(1, 4): __BITRATE[(2.5, i)] = __BITRATE[(2, i)] # Map version to sample rates. __RATES = { 1: [44100, 48000, 32000], 2: [22050, 24000, 16000], 2.5: [11025, 12000, 8000] } sketchy = False def __init__(self, fileobj): """Raises HeaderNotFoundError""" self.frame_offset = fileobj.tell() r = BitReader(fileobj) try: if r.bits(11) != 0x7ff: raise HeaderNotFoundError("invalid sync") version = r.bits(2) layer = r.bits(2) protection = r.bits(1) bitrate = r.bits(4) sample_rate = r.bits(2) padding = r.bits(1) r.skip(1) # private self.mode = r.bits(2) r.skip(6) except BitReaderError: raise HeaderNotFoundError("truncated header") assert r.get_position() == 32 and r.is_aligned() # try to be strict here to redice the chance of a false positive if version == 1 or layer == 0 or sample_rate == 0x3 or \ bitrate == 0xf or bitrate == 0: raise HeaderNotFoundError("invalid header") self.channels = 1 if self.mode == MONO else 2 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_size = 384 slot = 4 elif self.version >= 2 and self.layer == 3: frame_size = 576 slot = 1 else: frame_size = 1152 slot = 1 frame_length = ( ((frame_size // 8 * self.bitrate) // self.sample_rate) + padding) * slot self.sketchy = True # Try to find/parse the Xing header, which trumps the above length # and bitrate calculation. if self.layer == 3: self._parse_vbr_header(fileobj, self.frame_offset, frame_size, frame_length) fileobj.seek(self.frame_offset + frame_length, 0) def _parse_vbr_header(self, fileobj, frame_offset, frame_size, frame_length): """Does not raise""" # Xing xing_offset = XingHeader.get_offset(self) fileobj.seek(frame_offset + 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) self.encoder_settings = xing.get_encoder_settings() if xing.frames != -1: samples = frame_size * xing.frames if xing.bytes != -1 and samples > 0: # the first frame is only included in xing.bytes but # not in xing.frames, skip it. audio_bytes = max(0, xing.bytes - frame_length) self.bitrate = intround(( audio_bytes * 8 * self.sample_rate) / float(samples)) if lame is not None: samples -= lame.encoder_delay_start samples -= lame.encoder_padding_end if samples < 0: # older lame versions wrote bogus delay/padding for short # files with low bitrate samples = 0 self.length = float(samples) / self.sample_rate if xing.lame_version_desc: self.encoder_info = u"LAME %s" % xing.lame_version_desc 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(frame_offset + 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 skip_id3(fileobj): """Might raise IOError""" # WMP writes multiple id3s, so skip as many as we find while True: 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: fileobj.seek(insize, 1) else: fileobj.seek(-len(idata), 1) break def iter_sync(fileobj, max_read): """Iterate over a fileobj and yields on each mpeg sync. When yielding the fileobj offset is right before the sync and can be changed between iterations without affecting the iteration process. Might raise IOError. """ read = 0 size = 2 last_byte = b"" is_second = lambda b: ord(b) & 0xe0 == 0xe0 while read < max_read: data_offset = fileobj.tell() new_data = fileobj.read(min(max_read - read, size)) if not new_data: return read += len(new_data) if last_byte == b"\xff" and is_second(new_data[0:1]): fileobj.seek(data_offset - 1, 0) yield size *= 2 last_byte = new_data[-1:] find_offset = 0 while True: index = new_data.find(b"\xff", find_offset) # if not found or the last byte -> read more if index == -1 or index == len(new_data) - 1: break if is_second(new_data[index + 1:index + 2]): fileobj.seek(data_offset + index, 0) yield find_offset = index + 1 fileobj.seek(data_offset + len(new_data), 0) class MPEGInfo(StreamInfo): """MPEGInfo() 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: Attributes: length (`float`): audio length, in seconds channels (`int`): number of audio channels bitrate (`int`): audio bitrate, in bits per second. In case :attr:`bitrate_mode` is :attr:`BitrateMode.UNKNOWN` the bitrate is guessed based on the first frame. sample_rate (`int`): audio sample rate, in Hz encoder_info (`mutagen.text`): 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. encoder_settings (`mutagen.text`): a string containing a guess about the settings used for encoding. The format is undefined and depends on the encoder. bitrate_mode (`BitrateMode`): a :class:`BitrateMode` track_gain (`float` or `None`): replaygain track gain (89db) or None track_peak (`float` or `None`): replaygain track peak or None album_gain (`float` or `None`): replaygain album gain (89db) or None Useless attributes: Attributes: version (`float`): MPEG version (1, 2, 2.5) layer (`int`): 1, 2, or 3 mode (`int`): One of STEREO, JOINTSTEREO, DUALCHANNEL, or MONO (0-3) protected (`bool`): whether or not the file is "protected" sketchy (`bool`): if true, the file may not be valid MPEG audio """ sketchy = False encoder_info = u"" encoder_settings = u"" bitrate_mode = BitrateMode.UNKNOWN track_gain = track_peak = album_gain = album_peak = None @convert_error(IOError, error) 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. Raises HeaderNotFoundError, error """ if offset is None: fileobj.seek(0, 0) else: fileobj.seek(offset, 0) # skip anyway, because wmp stacks multiple id3 tags skip_id3(fileobj) # find a sync in the first 1024K, give up after some invalid syncs max_read = 1024 * 1024 max_syncs = 1500 enough_frames = 4 min_frames = 2 self.sketchy = True frames = [] first_frame = None for _ in iter_sync(fileobj, max_read): max_syncs -= 1 if max_syncs <= 0: break for _ in range(enough_frames): try: frame = MPEGFrame(fileobj) except HeaderNotFoundError: break frames.append(frame) if not frame.sketchy: break # if we have min frames, save it in case this is all we get if len(frames) >= min_frames and first_frame is None: first_frame = frames[0] # if the last frame was a non-sketchy one (has a valid vbr header) # we use that if frames and not frames[-1].sketchy: first_frame = frames[-1] self.sketchy = False break # if we have enough valid frames, use the first if len(frames) >= enough_frames: first_frame = frames[0] self.sketchy = False break # otherwise start over with the next sync del frames[:] if first_frame is None: raise HeaderNotFoundError("can't sync to MPEG frame") assert first_frame self.length = -1 sketchy = self.sketchy self.__dict__.update(first_frame.__dict__) self.sketchy = sketchy # no length, estimate based on file size if self.length == -1: fileobj.seek(0, 2) content_size = fileobj.tell() - first_frame.frame_offset self.length = 8 * content_size / float(self.bitrate) 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 if self.encoder_settings: info += ", %s" % self.encoder_settings 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): """MP3(filething) An MPEG audio (usually MPEG-1 Layer 3) file. Arguments: filething (filething) Attributes: info (`MPEGInfo`) tags (`mutagen.id3.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): """EasyMP3(filething) Like MP3, but uses EasyID3 for tags. Arguments: filething (filething) Attributes: info (`MPEGInfo`) tags (`mutagen.easyid3.EasyID3`) """ from mutagen.easyid3 import EasyID3 as ID3 ID3 = ID3 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/mp3/_util.py0000664000175000017500000003677200000000000017337 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """ http://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header http://wiki.hydrogenaud.io/index.php?title=MP3 """ from __future__ import division from functools import partial from io import BytesIO from mutagen._util import cdata, BitReader, iterbytes class LAMEError(Exception): pass class LAMEHeader(object): """http://gabriel.mp3-tech.org/mp3infotag.html""" vbr_method = 0 """0: unknown, 1: CBR, 2: ABR, 3/4/5: VBR, others: see the docs""" lowpass_filter = 0 """lowpass filter value in Hz. 0 means unknown""" quality = -1 """Encoding quality: 0..9""" vbr_quality = -1 """VBR quality: 0..9""" track_peak = None """Peak signal amplitude as float. 1.0 is maximal signal amplitude in decoded format. None if unknown. """ track_gain_origin = 0 """see the docs""" track_gain_adjustment = None """Track gain adjustment as float (for 89db replay gain) or None""" album_gain_origin = 0 """see the docs""" album_gain_adjustment = None """Album gain adjustment as float (for 89db replay gain) or None""" encoding_flags = 0 """see docs""" ath_type = -1 """see docs""" bitrate = -1 """Bitrate in kbps. For VBR the minimum bitrate, for anything else (CBR, ABR, ..) the target bitrate. """ encoder_delay_start = 0 """Encoder delay in samples""" encoder_padding_end = 0 """Padding in samples added at the end""" source_sample_frequency_enum = -1 """see docs""" unwise_setting_used = False """see docs""" stereo_mode = 0 """see docs""" noise_shaping = 0 """see docs""" mp3_gain = 0 """Applied MP3 gain -127..127. Factor is 2 ** (mp3_gain / 4)""" surround_info = 0 """see docs""" preset_used = 0 """lame preset""" music_length = 0 """Length in bytes excluding any ID3 tags""" music_crc = -1 """CRC16 of the data specified by music_length""" header_crc = -1 """CRC16 of this header and everything before (not checked)""" def __init__(self, xing, fileobj): """Raises LAMEError if parsing fails""" payload = fileobj.read(27) if len(payload) != 27: raise LAMEError("Not enough data") # extended lame header r = BitReader(BytesIO(payload)) revision = r.bits(4) if revision != 0: raise LAMEError("unsupported header revision %d" % revision) self.vbr_method = r.bits(4) self.lowpass_filter = r.bits(8) * 100 # these have a different meaning for lame; expose them again here self.quality = (100 - xing.vbr_scale) % 10 self.vbr_quality = (100 - xing.vbr_scale) // 10 track_peak_data = r.bytes(4) if track_peak_data == b"\x00\x00\x00\x00": self.track_peak = None else: # see PutLameVBR() in LAME's VbrTag.c self.track_peak = cdata.uint32_be(track_peak_data) / 2 ** 23 track_gain_type = r.bits(3) self.track_gain_origin = r.bits(3) sign = r.bits(1) gain_adj = r.bits(9) / 10.0 if sign: gain_adj *= -1 if track_gain_type == 1: self.track_gain_adjustment = gain_adj else: self.track_gain_adjustment = None assert r.is_aligned() album_gain_type = r.bits(3) self.album_gain_origin = r.bits(3) sign = r.bits(1) album_gain_adj = r.bits(9) / 10.0 if album_gain_type == 2: self.album_gain_adjustment = album_gain_adj else: self.album_gain_adjustment = None self.encoding_flags = r.bits(4) self.ath_type = r.bits(4) self.bitrate = r.bits(8) self.encoder_delay_start = r.bits(12) self.encoder_padding_end = r.bits(12) self.source_sample_frequency_enum = r.bits(2) self.unwise_setting_used = r.bits(1) self.stereo_mode = r.bits(3) self.noise_shaping = r.bits(2) sign = r.bits(1) mp3_gain = r.bits(7) if sign: mp3_gain *= -1 self.mp3_gain = mp3_gain r.skip(2) self.surround_info = r.bits(3) self.preset_used = r.bits(11) self.music_length = r.bits(32) self.music_crc = r.bits(16) self.header_crc = r.bits(16) assert r.is_aligned() def guess_settings(self, major, minor): """Gives a guess about the encoder settings used. Returns an empty string if unknown. The guess is mostly correct in case the file was encoded with the default options (-V --preset --alt-preset --abr -b etc) and no other fancy options. Args: major (int) minor (int) Returns: text """ version = major, minor if self.vbr_method == 2: if version in ((3, 90), (3, 91), (3, 92)) and self.encoding_flags: if self.bitrate < 255: return u"--alt-preset %d" % self.bitrate else: return u"--alt-preset %d+" % self.bitrate if self.preset_used != 0: return u"--preset %d" % self.preset_used elif self.bitrate < 255: return u"--abr %d" % self.bitrate else: return u"--abr %d+" % self.bitrate elif self.vbr_method == 1: if self.preset_used == 0: if self.bitrate < 255: return u"-b %d" % self.bitrate else: return u"-b 255+" elif self.preset_used == 1003: return u"--preset insane" return u"-b %d" % self.preset_used elif version in ((3, 90), (3, 91), (3, 92)): preset_key = (self.vbr_quality, self.quality, self.vbr_method, self.lowpass_filter, self.ath_type) if preset_key == (1, 2, 4, 19500, 3): return u"--preset r3mix" if preset_key == (2, 2, 3, 19000, 4): return u"--alt-preset standard" if preset_key == (2, 2, 3, 19500, 2): return u"--alt-preset extreme" if self.vbr_method == 3: return u"-V %s" % self.vbr_quality elif self.vbr_method in (4, 5): return u"-V %s --vbr-new" % self.vbr_quality elif version in ((3, 93), (3, 94), (3, 95), (3, 96), (3, 97)): if self.preset_used == 1001: return u"--preset standard" elif self.preset_used == 1002: return u"--preset extreme" elif self.preset_used == 1004: return u"--preset fast standard" elif self.preset_used == 1005: return u"--preset fast extreme" elif self.preset_used == 1006: return u"--preset medium" elif self.preset_used == 1007: return u"--preset fast medium" if self.vbr_method == 3: return u"-V %s" % self.vbr_quality elif self.vbr_method in (4, 5): return u"-V %s --vbr-new" % self.vbr_quality elif version == (3, 98): if self.vbr_method == 3: return u"-V %s --vbr-old" % self.vbr_quality elif self.vbr_method in (4, 5): return u"-V %s" % self.vbr_quality elif version >= (3, 99): if self.vbr_method == 3: return u"-V %s --vbr-old" % self.vbr_quality elif self.vbr_method in (4, 5): p = self.vbr_quality adjust_key = (p, self.bitrate, self.lowpass_filter) # https://sourceforge.net/p/lame/bugs/455/ p = { (5, 32, 0): 7, (5, 8, 0): 8, (6, 8, 0): 9, }.get(adjust_key, p) return u"-V %s" % p return u"" @classmethod def parse_version(cls, fileobj): """Returns a version string and True if a LAMEHeader follows. The passed file object will be positioned right before the lame header if True. Raises LAMEError if there is no lame version info. """ # http://wiki.hydrogenaud.io/index.php?title=LAME_version_string data = fileobj.read(20) if len(data) != 20: raise LAMEError("Not a lame header") if not data.startswith((b"LAME", b"L3.99")): raise LAMEError("Not a lame header") data = data.lstrip(b"EMAL") major, data = data[0:1], data[1:].lstrip(b".") minor = b"" for c in iterbytes(data): if not c.isdigit(): break minor += c data = data[len(minor):] try: major = int(major.decode("ascii")) minor = int(minor.decode("ascii")) except ValueError: raise LAMEError # the extended header was added sometimes in the 3.90 cycle # e.g. "LAME3.90 (alpha)" should still stop here. # (I have seen such a file) if (major, minor) < (3, 90) or ( (major, minor) == (3, 90) and data[-11:-10] == b"("): flag = data.strip(b"\x00").rstrip().decode("ascii") return (major, minor), u"%d.%d%s" % (major, minor, flag), False if len(data) < 11: raise LAMEError("Invalid version: too long") flag = data[:-11].rstrip(b"\x00") flag_string = u"" patch = u"" if flag == b"a": flag_string = u" (alpha)" elif flag == b"b": flag_string = u" (beta)" elif flag == b"r": patch = u".1+" elif flag == b" ": if (major, minor) > (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 (major, minor), \ 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 = (0, 0) """The LAME version as two element tuple (major, minor)""" lame_version_desc = 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, self.lame_version_desc, has_header = \ LAMEHeader.parse_version(fileobj) if has_header: self.lame_header = LAMEHeader(self, fileobj) except LAMEError: pass def get_encoder_settings(self): """Returns the guessed encoder settings""" if self.lame_header is None: return u"" return self.lame_header.guess_settings(*self.lame_version) @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 range(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 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1596184724.8231053 mutagen-1.45.1/mutagen/mp4/0000775000175000017500000000000000000000000015633 5ustar00lazkalazka00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/mp4/__init__.py0000664000175000017500000012037200000000000017751 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """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 io import BytesIO from collections.abc import Sequence from datetime import timedelta from mutagen import FileType, Tags, StreamInfo, PaddingInfo from mutagen._constants import GENRES from mutagen._util import cdata, insert_bytes, DictProxy, MutagenError, \ hashable, enum, get_size, resize_bytes, loadfile, convert_error, bchr, \ reraise from ._atom import Atoms, Atom, AtomError from ._util import parse_full_atom from ._as_entry import AudioSampleEntry, ASEntryError class error(MutagenError): pass class MP4MetadataError(error): pass class MP4StreamInfoError(error): pass class MP4NoTrackError(MP4StreamInfoError): 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 (`AtomDataType`): 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 (`AtomDataType`): 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): return name.decode("latin-1") def _key2name(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 def _item_sort_key(key, value): # iTunes always writes the tags in order of "relevance", try # to copy it as closely as possible. order = ["\xa9nam", "\xa9ART", "\xa9wrt", "\xa9alb", "\xa9gen", "gnre", "trkn", "disk", "\xa9day", "cpil", "pgap", "pcst", "tmpo", "\xa9too", "----", "covr", "\xa9lyr"] order = dict(zip(order, range(len(order)))) last = len(order) # If there's no key-based way to distinguish, order by length. # If there's still no way, go by string comparison on the # values, so we at least have something determinstic. return (order.get(key[:4], last), len(repr(value)), repr(value)) class MP4Tags(DictProxy, Tags): r"""MP4Tags() 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 * '\\xa9wrk' -- work * '\\xa9mvn' -- movement 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 Integer values: * 'tmpo' -- tempo/BPM * '\\xa9mvc' -- Movement Count * '\\xa9mvi' -- Movement Index * 'shwm' -- work/movement * 'stik' -- Media Kind * 'hdvd' -- HD Video * 'rtng' -- Content Rating * 'tves' -- TV Episode * 'tvsn' -- TV Season * 'plID', 'cnID', 'geID', 'atID', 'sfID', 'cmID', 'akID' -- Various iTunes Internal IDs Others: * '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__() if args or kwargs: self.load(*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") self._render(key, value) super(MP4Tags, self).__setitem__(key, value) @classmethod def _can_load(cls, atoms): return b"moov.udta.meta.ilst" in atoms def _render(self, key, value): atom_name = _key2name(key)[:4] if atom_name in self.__atoms: render_func = self.__atoms[atom_name][1] render_args = self.__atoms[atom_name][2:] else: render_func = type(self).__render_text render_args = [] return render_func(self, key, value, *render_args) @convert_error(IOError, error) @loadfile(writable=True) def save(self, filething=None, padding=None): values = [] items = sorted(self.items(), key=lambda kv: _item_sort_key(*kv)) for key, value in items: try: values.append(self._render(key, value)) except (TypeError, ValueError) as s: reraise(MP4MetadataValueError, s, sys.exc_info()[2]) for key, failed in self._failed_atoms.items(): # 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. try: atoms = Atoms(filething.fileobj) except AtomError as err: reraise(error, err, sys.exc_info()[2]) self.__save(filething.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 __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]) if length < 1: raise MP4MetadataError( "atom %r has a length of zero" % atom.name) 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)) if length < 1: raise MP4MetadataError( "atom %r has a length of zero" % 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 v in value: try: track, total = v except TypeError: raise ValueError 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_integer(self, atom, data): values = [] for version, flags, data in self.__parse_data(atom, data): if version != 0: raise MP4MetadataValueError("unsupported version") if flags not in (AtomDataType.IMPLICIT, AtomDataType.INTEGER): raise MP4MetadataValueError("unsupported type") if len(data) == 1: value = cdata.int8(data) elif len(data) == 2: value = cdata.int16_be(data) elif len(data) == 3: value = cdata.int32_be(data + b"\x00") >> 8 elif len(data) == 4: value = cdata.int32_be(data) elif len(data) == 8: value = cdata.int64_be(data) else: raise MP4MetadataValueError( "invalid value size %d" % len(data)) values.append(value) key = _name2key(atom.name) self.__add(key, values) def __render_integer(self, key, value, min_bytes): assert min_bytes in (1, 2, 4, 8) data_list = [] try: for v in value: # We default to the int size of the usual values written # by itunes for compatibility. if cdata.int8_min <= v <= cdata.int8_max and min_bytes <= 1: data = cdata.to_int8(v) elif cdata.int16_min <= v <= cdata.int16_max and \ min_bytes <= 2: data = cdata.to_int16_be(v) elif cdata.int32_min <= v <= cdata.int32_max and \ min_bytes <= 4: data = cdata.to_int32_be(v) elif cdata.int64_min <= v <= cdata.int64_max and \ min_bytes <= 8: data = cdata.to_int64_be(v) else: raise MP4MetadataValueError( "value out of range: %r" % value) data_list.append(data) except (TypeError, ValueError, cdata.error) as e: raise MP4MetadataValueError(e) return self.__render_data(key, 0, AtomDataType.INTEGER, data_list) 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, [bchr(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 length < 1: raise MP4MetadataError( "atom %r has a length of zero" % atom.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, str): value = [value] encoded = [] for v in value: if not isinstance(v, str): raise TypeError("%r not str" % v) 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"plID": (__parse_integer, __render_integer, 8), b"cnID": (__parse_integer, __render_integer, 4), b"geID": (__parse_integer, __render_integer, 4), b"atID": (__parse_integer, __render_integer, 4), b"sfID": (__parse_integer, __render_integer, 4), b"cmID": (__parse_integer, __render_integer, 4), b"akID": (__parse_integer, __render_integer, 1), b"tvsn": (__parse_integer, __render_integer, 4), b"tves": (__parse_integer, __render_integer, 4), b"tmpo": (__parse_integer, __render_integer, 2), b"\xa9mvi": (__parse_integer, __render_integer, 2), b"\xa9mvc": (__parse_integer, __render_integer, 2), b"cpil": (__parse_bool, __render_bool), b"pgap": (__parse_bool, __render_bool), b"pcst": (__parse_bool, __render_bool), b"shwm": (__parse_integer, __render_integer, 1), b"stik": (__parse_integer, __render_integer, 1), b"hdvd": (__parse_integer, __render_integer, 1), b"rtng": (__parse_integer, __render_integer, 1), 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, str) if isinstance(value, str): return u"%s=%s" % (key, value) return u"%s=%r" % (key, value) values = [] for key, value in sorted(self.items()): if not isinstance(key, str): 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 Chapter(object): """Chapter() Chapter information container """ def __init__(self, start, title): self.start = start self.title = title class MP4Chapters(Sequence): """MP4Chapters() MPEG-4 Chapter information. Supports the 'moov.udta.chpl' box. A sequence of Chapter objects with the following members: start (`float`): position from the start of the file in seconds title (`str`): title of the chapter """ def __init__(self, *args, **kwargs): self._timescale = None self._duration = None self._chapters = [] super(MP4Chapters, self).__init__() if args or kwargs: self.load(*args, **kwargs) def __len__(self): return self._chapters.__len__() def __getitem__(self, key): return self._chapters.__getitem__(key) def load(self, atoms, fileobj): try: mvhd = atoms.path(b"moov", b"mvhd")[-1] except KeyError as key: return MP4MetadataError(key) self._parse_mvhd(mvhd, fileobj) if not self._timescale: raise MP4MetadataError("Unable to get timescale") try: chpl = atoms.path(b"moov", b"udta", b"chpl")[-1] except KeyError as key: return MP4MetadataError(key) self._parse_chpl(chpl, fileobj) @classmethod def _can_load(cls, atoms): return b"moov.udta.chpl" in atoms and b"moov.mvhd" in atoms def _parse_mvhd(self, atom, fileobj): assert atom.name == b"mvhd" ok, data = atom.read(fileobj) if not ok: raise MP4StreamInfoError("Invalid mvhd") version = data[0] pos = 4 if version == 0: pos += 8 # created, modified self._timescale = struct.unpack(">l", data[pos:pos + 4])[0] pos += 4 self._duration = struct.unpack(">l", data[pos:pos + 4])[0] pos += 4 elif version == 1: pos += 16 # created, modified self._timescale = struct.unpack(">l", data[pos:pos + 4])[0] pos += 4 self._duration = struct.unpack(">q", data[pos:pos + 8])[0] pos += 8 def _parse_chpl(self, atom, fileobj): assert atom.name == b"chpl" ok, data = atom.read(fileobj) if not ok: raise MP4StreamInfoError("Invalid atom") chapters = data[8] pos = 9 for i in range(chapters): start = struct.unpack(">Q", data[pos:pos + 8])[0] / 10000 pos += 8 title_len = data[pos] pos += 1 try: title = data[pos:pos + title_len].decode() except UnicodeDecodeError as e: raise MP4MetadataError("chapter %d title: %s" % (i, e)) pos += title_len self._chapters.append(Chapter(start / self._timescale, title)) def pprint(self): chapters = ["%s %s" % (timedelta(seconds=chapter.start), chapter.title) for chapter in self._chapters] return "chapters=%s" % '\n '.join(chapters) class MP4Info(StreamInfo): """MP4Info() MPEG-4 stream information. Attributes: bitrate (`int`): bitrate in bits per second, as an int length (`float`): file length in seconds, as a float channels (`int`): number of audio channels sample_rate (`int`): audio sampling rate in Hz bits_per_sample (`int`): bits per sample codec (`mutagen.text`): * 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 (`mutagen.text`): Name of the codec used (ALAC, AAC LC, AC-3...). Values might change in the future, use for display purposes only. """ bitrate = 0 length = 0.0 channels = 0 sample_rate = 0 bits_per_sample = 0 codec = u"" codec_description = u"" def __init__(self, *args, **kwargs): if args or kwargs: self.load(*args, **kwargs) @convert_error(IOError, MP4StreamInfoError) def load(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 MP4NoTrackError("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 = BytesIO(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): """MP4(filething) 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. Arguments: filething (filething) Attributes: info (`MP4Info`) tags (`MP4Tags`) """ MP4Tags = MP4Tags MP4Chapters = MP4Chapters _mimes = ["audio/mp4", "audio/x-m4a", "audio/mpeg4", "audio/aac"] @loadfile() def load(self, filething): fileobj = filething.fileobj try: atoms = Atoms(fileobj) except AtomError as err: reraise(error, err, sys.exc_info()[2]) self.info = MP4Info() try: self.info.load(atoms, fileobj) except MP4NoTrackError: pass except error: raise except Exception as err: reraise(MP4StreamInfoError, err, sys.exc_info()[2]) if not MP4Tags._can_load(atoms): self.tags = None else: try: self.tags = self.MP4Tags(atoms, fileobj) except error: raise except Exception as err: reraise(MP4MetadataError, err, sys.exc_info()[2]) if not MP4Chapters._can_load(atoms): self.chapters = None else: try: self.chapters = self.MP4Chapters(atoms, fileobj) except error: raise except Exception as err: reraise(MP4MetadataError, err, sys.exc_info()[2]) @property def _padding(self): if self.tags is None: return 0 else: return self.tags._padding def save(self, *args, **kwargs): """save(filething=None, padding=None)""" super(MP4, self).save(*args, **kwargs) def pprint(self): """ Returns: text: stream information, comment key=value pairs and chapters. """ stream = "%s (%s)" % (self.info.pprint(), self.mime[0]) try: tags = self.tags.pprint() except AttributeError: pass else: stream += ((tags and "\n" + tags) or "") try: chapters = self.chapters.pprint() except AttributeError: pass else: stream += "\n" + chapters return stream 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 @convert_error(IOError, error) @loadfile(method=False, writable=True) def delete(filething): """ delete(filething) Arguments: filething (filething) Raises: mutagen.MutagenError Remove tags from a file. """ t = MP4(filething) filething.fileobj.seek(0) t.delete(filething) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/mp4/_as_entry.py0000664000175000017500000004104400000000000020173 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. from io import BytesIO from mutagen.aac import ProgramConfigElement from mutagen._util import BitReader, BitReaderError, cdata 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 = BytesIO(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 = BytesIO(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 = BytesIO(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 = BytesIO(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 range(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: fileobj.seek(left, 1) else: # XXX: In case the instance length is shorted than the content # assume the size is wrong and just continue parsing # https://github.com/quodlibet/mutagen/issues/444 pass 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 str(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") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/mp4/_atom.py0000664000175000017500000001427500000000000017315 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. import struct from mutagen._util import convert_error # 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 @convert_error(IOError, AtomError) 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. """ @convert_error(IOError, AtomError) def __init__(self, fileobj): self.atoms = [] fileobj.seek(0, 2) end = fileobj.tell() fileobj.seek(0) while fileobj.tell() + 8 <= end: self.atoms.append(Atom(fileobj)) def path(self, *names): """Look up and return the complete path of an atom. For example, atoms.path('moov', 'udta', 'meta') will return a list of three atoms, corresponding to the moov, udta, and meta atoms. """ path = [self] for name in names: path.append(path[-1][name, ]) return path[1:] def __contains__(self, names): try: self[names] except KeyError: return False return True def __getitem__(self, names): """Look up a child atom. 'names' may be a list of atoms (['moov', 'udta']) or a string specifying the complete path ('moov.udta'). """ if isinstance(names, 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]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/mutagen/mp4/_util.py0000644000175000017500000000123100000000000017314 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. 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:] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/musepack.py0000664000175000017500000002316500000000000017324 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """Musepack audio streams with APEv2 tags. Musepack is an audio format originally based on the MPEG-1 Layer-2 algorithms. Stream versions 4 through 7 are supported. For more information, see http://www.musepack.net/. """ __all__ = ["Musepack", "Open", "delete"] import struct from mutagen import StreamInfo from mutagen.apev2 import APEv2File, error, delete from mutagen.id3._util import BitPaddedInt from mutagen._util import cdata, convert_error, intround, endswith 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 range(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): """MusepackInfo() Musepack stream information. Attributes: channels (`int`): number of audio channels length (`float`): file length in seconds, as a float sample_rate (`int`): audio sampling rate in Hz bitrate (`int`): audio bitrate, in bits per second version (`int`) Musepack stream version Optional Attributes: Attributes: title_gain (`float`): Replay Gain for this song title_peak (`float`): Peak data for this song album_gain (`float`): Replay Gain for this album album_peak (`float`): 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. """ @convert_error(IOError, MusepackHeaderError) def __init__(self, fileobj): """Raises MusepackHeaderError""" 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 = intround(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": if frame_type not in mandatory_packets: raise MusepackHeaderError("Duplicate SH packet") mandatory_packets.remove(frame_type) self.__parse_stream_header(fileobj, data_size) elif frame_type == b"RG": if frame_type not in mandatory_packets: raise MusepackHeaderError("Duplicate RG packet") 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, IndexError): 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 or len(data) < 2: raise MusepackHeaderError("SH packet ended unexpectedly.") rate_index = (bytearray(data)[0] >> 5) try: self.sample_rate = RATES[rate_index] except IndexError: raise MusepackHeaderError("Invalid sample rate") 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): """Musepack(filething) Arguments: filething (filething) Attributes: info (`MusepackInfo`) """ _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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/ogg.py0000664000175000017500000004724700000000000016277 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """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 io import BytesIO from mutagen import FileType from mutagen._util import cdata, resize_bytes, MutagenError, loadfile, \ seek_end, bchr, reraise class error(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 given 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 (`int`): stream structure version (currently always 0) position (`int`): absolute stream position (default -1) serial (`int`): logical stream serial number (default 0) sequence (`int`): page sequence number within logical stream (default 0) offset (`int` or `None`): offset this page was read from (default None) complete (`bool`): if the last packet on this page is complete (default True) packets (list[bytes]): 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): """Raises error, IOError, EOFError""" 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 + bchr(rem)) lacing_data = b"".join(lacing_data) if not self.complete and lacing_data.endswith(b"\x00"): lacing_data = lacing_data[:-1] data.append(bchr(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 occurred, 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.packets: 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 zip(new_pages, range(first, first + len(new_pages))): page.sequence = seq page.serial = old_pages[0].serial new_pages[0].first = old_pages[0].first new_pages[0].last = old_pages[0].last new_pages[0].continued = old_pages[0].continued new_pages[-1].first = old_pages[-1].first new_pages[-1].last = old_pages[-1].last new_pages[-1].complete = old_pages[-1].complete if not new_pages[-1].complete and len(new_pages[-1].packets) == 1: new_pages[-1].position = -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 zip(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, finishing=False): """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. If finishing is True it returns the last page which contains a packet finishing on it. If there exist pages but none with finishing packets returns None. Returns None in case no page with the serial exists. Raises error in case this isn't a valid ogg stream. Raises IOError. """ # For non-muxed streams, look at the last page. seek_end(fileobj, 256 * 256) data = fileobj.read() try: index = data.rindex(b"OggS") except ValueError: raise error("unable to find final Ogg header") bytesobj = BytesIO(data[index:]) def is_valid(page): return not finishing or page.position != -1 best_page = None try: page = OggPage(bytesobj) except error: pass else: if page.serial == serial and is_valid(page): 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 True: if page.serial == serial: if is_valid(page): best_page = page if page.last: break page = OggPage(fileobj) return best_page except error: return best_page except EOFError: return best_page class OggFileType(FileType): """OggFileType(filething) An generic Ogg file. Arguments: filething (filething) """ _Info = None _Tags = None _Error = None _mimes = ["application/ogg", "application/x-ogg"] @loadfile() def load(self, filething): """load(filething) Load file information from a filename. Args: filething (filething) Raises: mutagen.MutagenError """ fileobj = filething.fileobj try: self.info = self._Info(fileobj) self.tags = self._Tags(fileobj, self.info) self.info._post_tags(fileobj) except (error, IOError) as e: reraise(self._Error, e, sys.exc_info()[2]) except EOFError: raise self._Error("no appropriate stream found") @loadfile(writable=True) def delete(self, filething=None): """delete(filething=None) Remove tags from a file. If no filename is given, the one most recently loaded is used. Args: filething (filething) Raises: mutagen.MutagenError """ fileobj = filething.fileobj self.tags.clear() # TODO: we should delegate the deletion to the subclass and not through # _inject. try: 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") except IOError as e: reraise(self._Error, e, sys.exc_info()[2]) def add_tags(self): raise self._Error @loadfile(writable=True) def save(self, filething=None, padding=None): """save(filething=None, padding=None) Save a tag to a file. If no filename is given, the one most recently loaded is used. Args: filething (filething) padding (:obj:`mutagen.PaddingFunction`) Raises: mutagen.MutagenError """ try: self.tags._inject(filething.fileobj, padding) except (IOError, error) as e: reraise(self._Error, e, sys.exc_info()[2]) except EOFError: raise self._Error("no appropriate stream found") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/oggflac.py0000664000175000017500000001226600000000000017116 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """Read and write Ogg FLAC comments. This module handles FLAC files wrapped in an Ogg bitstream. The first FLAC stream found is used. For 'naked' FLACs, see mutagen.flac. This module is based off the specification at http://flac.sourceforge.net/ogg_mapping.html. """ __all__ = ["OggFLAC", "Open", "delete"] import struct from io import BytesIO from mutagen import StreamInfo from mutagen.flac import StreamInfo as FLACStreamInfo, error as FLACError from mutagen._vorbis import VCommentDict from mutagen._util import loadfile, convert_error from mutagen.ogg import OggPage, OggFileType, error as OggError class error(OggError): pass class OggFLACHeaderError(error): pass class OggFLACStreamInfo(StreamInfo): """OggFLACStreamInfo() Ogg FLAC stream info. Attributes: length (`float`): File length in seconds, as a float channels (`float`): Number of channels sample_rate (`int`): Sample rate in Hz" """ length = 0 channels = 0 sample_rate = 0 def __init__(self, fileobj): page = OggPage(fileobj) while not page.packets[0].startswith(b"\x7FFLAC"): page = OggPage(fileobj) major, minor, self.packets, flac = 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 = BytesIO(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, finishing=True) if page is None: raise OggFLACHeaderError 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 = BytesIO(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): """OggFLAC(filething) An Ogg FLAC file. Arguments: filething (filething) Attributes: info (`OggFLACStreamInfo`) tags (`mutagen._vorbis.VCommentDict`) """ _Info = OggFLACStreamInfo _Tags = OggFLACVComment _Error = OggFLACHeaderError _mimes = ["audio/x-oggflac"] info = None tags = None @staticmethod def score(filename, fileobj, header): return (header.startswith(b"OggS") * ( (b"FLAC" in header) + (b"fLaC" in header))) Open = OggFLAC @convert_error(IOError, error) @loadfile(method=False, writable=True) def delete(filething): """ delete(filething) Arguments: filething (filething) Raises: mutagen.MutagenError Remove tags from a file. """ t = OggFLAC(filething) filething.fileobj.seek(0) t.delete(filething) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/oggopus.py0000664000175000017500000001235600000000000017177 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """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 io import BytesIO from mutagen import StreamInfo from mutagen._util import get_size, loadfile, convert_error 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): """OggOpusInfo() Ogg Opus stream information. Attributes: length (`float`): File length in seconds, as a float channels (`int`): Number of channels """ length = 0 channels = 0 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, finishing=True) if page is None: raise OggOpusHeaderError 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): """OggOpus(filething) An Ogg Opus file. Arguments: filething (filething) Attributes: info (`OggOpusInfo`) tags (`mutagen._vorbis.VCommentDict`) """ _Info = OggOpusInfo _Tags = OggOpusVComment _Error = OggOpusHeaderError _mimes = ["audio/ogg", "audio/ogg; codecs=opus"] info = None tags = None @staticmethod def score(filename, fileobj, header): return (header.startswith(b"OggS") * (b"OpusHead" in header)) Open = OggOpus @convert_error(IOError, error) @loadfile(method=False, writable=True) def delete(filething): """ delete(filething) Arguments: filething (filething) Raises: mutagen.MutagenError Remove tags from a file. """ t = OggOpus(filething) filething.fileobj.seek(0) t.delete(filething) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/mutagen/oggspeex.py0000644000175000017500000001216300000000000017327 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """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, loadfile, convert_error from mutagen._tags import PaddingInfo class error(OggError): pass class OggSpeexHeaderError(error): pass class OggSpeexInfo(StreamInfo): """OggSpeexInfo() Ogg Speex stream information. Attributes: length (`float`): file length in seconds, as a float channels (`int`): number of channels bitrate (`int`): nominal bitrate in bits per second. The reference encoder does not set the bitrate; in this case, the bitrate will be 0. """ length = 0 channels = 0 bitrate = 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, finishing=True) if page is None: raise OggSpeexHeaderError 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): """OggSpeex(filething) An Ogg Speex file. Arguments: filething (filething) Attributes: info (`OggSpeexInfo`) tags (`mutagen._vorbis.VCommentDict`) """ _Info = OggSpeexInfo _Tags = OggSpeexVComment _Error = OggSpeexHeaderError _mimes = ["audio/x-speex"] info = None tags = None @staticmethod def score(filename, fileobj, header): return (header.startswith(b"OggS") * (b"Speex " in header)) Open = OggSpeex @convert_error(IOError, error) @loadfile(method=False, writable=True) def delete(filething): """ delete(filething) Arguments: filething (filething) Raises: mutagen.MutagenError Remove tags from a file. """ t = OggSpeex(filething) filething.fileobj.seek(0) t.delete(filething) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/oggtheora.py0000664000175000017500000001256100000000000017471 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """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, loadfile, convert_error 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): """OggTheoraInfo() Ogg Theora stream information. Attributes: length (`float`): File length in seconds, as a float fps (`float`): Video frames per second, as a float bitrate (`int`): Bitrate in bps (int) """ length = 0 fps = 0 bitrate = 0 def __init__(self, fileobj): page = OggPage(fileobj) while not page.packets or \ 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] if len(data) < 42: raise OggTheoraHeaderError("Truncated header") 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]) if not fps_den or not fps_num: raise OggTheoraHeaderError("FRN or FRD is equal to zero") 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, finishing=True) if page is None: raise OggTheoraHeaderError position = page.position mask = (1 << self.granule_shift) - 1 frames = (position >> self.granule_shift) + (position & mask) assert self.fps 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) packets = OggPage.to_packets(pages) if not packets: raise error("Missing metadata packet") data = packets[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 or \ 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): """OggTheora(filething) An Ogg Theora file. Arguments: filething (filething) Attributes: info (`OggTheoraInfo`) tags (`mutagen._vorbis.VCommentDict`) """ _Info = OggTheoraInfo _Tags = OggTheoraCommentDict _Error = OggTheoraHeaderError _mimes = ["video/x-theora"] info = None tags = None @staticmethod def score(filename, fileobj, header): return (header.startswith(b"OggS") * ((b"\x80theora" in header) + (b"\x81theora" in header)) * 2) Open = OggTheora @convert_error(IOError, error) @loadfile(method=False, writable=True) def delete(filething): """ delete(filething) Arguments: filething (filething) Raises: mutagen.MutagenError Remove tags from a file. """ t = OggTheora(filething) filething.fileobj.seek(0) t.delete(filething) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/oggvorbis.py0000664000175000017500000001333400000000000017512 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """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, loadfile, convert_error 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): """OggVorbisInfo() Ogg Vorbis stream information. Attributes: length (`float`): File length in seconds, as a float channels (`int`): Number of channels bitrate (`int`): Nominal ('average') bitrate in bits per second sample_rate (`int`): Sample rate in Hz """ length = 0.0 channels = 0 bitrate = 0 sample_rate = 0 def __init__(self, fileobj): """Raises ogg.error, IOError""" page = OggPage(fileobj) if not page.packets: raise OggVorbisHeaderError("page has not packets") 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") if len(page.packets[0]) < 28: raise OggVorbisHeaderError( "page contains a packet too short to be valid") (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): """Raises ogg.error""" page = OggPage.find_last(fileobj, self.serial, finishing=True) if page is None: raise OggVorbisHeaderError 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): """OggVorbis(filething) Arguments: filething (filething) An Ogg Vorbis file. Attributes: info (`OggVorbisInfo`) tags (`mutagen._vorbis.VCommentDict`) """ _Info = OggVorbisInfo _Tags = OggVCommentDict _Error = OggVorbisHeaderError _mimes = ["audio/vorbis", "audio/x-vorbis"] info = None tags = None @staticmethod def score(filename, fileobj, header): return (header.startswith(b"OggS") * (b"\x01vorbis" in header)) Open = OggVorbis @convert_error(IOError, error) @loadfile(method=False, writable=True) def delete(filething): """ delete(filething) Arguments: filething (filething) Raises: mutagen.MutagenError Remove tags from a file. """ t = OggVorbis(filething) filething.fileobj.seek(0) t.delete(filething) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/optimfrog.py0000664000175000017500000000626500000000000017524 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """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 ._util import convert_error, endswith from mutagen import StreamInfo from mutagen.apev2 import APEv2File, error, delete SAMPLE_TYPE_BITS = { 0: 8, 1: 8, 2: 16, 3: 16, 4: 24, 5: 24, 6: 32, 7: 32, } class OptimFROGHeaderError(error): pass class OptimFROGInfo(StreamInfo): """OptimFROGInfo() OptimFROG stream information. Attributes: channels (`int`): number of audio channels length (`float`): file length in seconds, as a float sample_rate (`int`): audio sampling rate in Hz bits_per_sample (`int`): the audio sample size encoder_info (`mutagen.text`): encoder version, e.g. "5.100" """ @convert_error(IOError, OptimFROGHeaderError) def __init__(self, fileobj): """Raises OptimFROGHeaderError""" header = fileobj.read(76) if len(header) != 76 or not header.startswith(b"OFR "): raise OptimFROGHeaderError("not an OptimFROG file") data_size = struct.unpack("= 15: encoder_id = struct.unpack("> 4) + 4500) self.encoder_info = "%s.%s" % (version[0], version[1:]) else: self.encoder_info = "" def pprint(self): return u"OptimFROG, %.2f seconds, %d Hz" % (self.length, self.sample_rate) class OptimFROG(APEv2File): """OptimFROG(filething) Attributes: info (`OptimFROGInfo`) tags (`mutagen.apev2.APEv2`) """ _Info = OptimFROGInfo @staticmethod def score(filename, fileobj, header): filename = filename.lower() return (header.startswith(b"OFR") + endswith(filename, b".ofr") + endswith(filename, b".ofs")) Open = OptimFROG ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/smf.py0000664000175000017500000001304000000000000016270 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """Standard MIDI File (SMF)""" import struct from mutagen import StreamInfo, MutagenError from mutagen._file import FileType from mutagen._util import loadfile, endswith class SMFError(MutagenError): pass def _var_int(data, offset=0): val = 0 while 1: try: x = data[offset] except IndexError: raise SMFError("Not enough data") offset += 1 val = (val << 7) + (x & 0x7F) if not (x & 0x80): return val, offset def _read_track(chunk): """Retuns a list of midi events and tempo change events""" TEMPO, MIDI = range(2) # Deviations: The running status should be reset on non midi events, but # some files contain meta events inbetween. # TODO: Offset and time signature are not considered. tempos = [] events = [] chunk = bytearray(chunk) deltasum = 0 status = 0 off = 0 while off < len(chunk): delta, off = _var_int(chunk, off) deltasum += delta event_type = chunk[off] off += 1 if event_type == 0xFF: meta_type = chunk[off] off += 1 num, off = _var_int(chunk, off) # TODO: support offset/time signature if meta_type == 0x51: data = chunk[off:off + num] if len(data) != 3: raise SMFError tempo = struct.unpack(">I", b"\x00" + bytes(data))[0] tempos.append((deltasum, TEMPO, tempo)) off += num elif event_type in (0xF0, 0xF7): val, off = _var_int(chunk, off) off += val else: if event_type < 0x80: # if < 0x80 take the type from the previous midi event off += 1 event_type = status elif event_type < 0xF0: off += 2 status = event_type else: raise SMFError("invalid event") if event_type >> 4 in (0xD, 0xC): off -= 1 events.append((deltasum, MIDI, delta)) return events, tempos def _read_midi_length(fileobj): """Returns the duration in seconds. Can raise all kind of errors...""" TEMPO, MIDI = range(2) def read_chunk(fileobj): info = fileobj.read(8) if len(info) != 8: raise SMFError("truncated") chunklen = struct.unpack(">I", info[4:])[0] data = fileobj.read(chunklen) if len(data) != chunklen: raise SMFError("truncated") return info[:4], data identifier, chunk = read_chunk(fileobj) if identifier != b"MThd": raise SMFError("Not a MIDI file") if len(chunk) != 6: raise SMFError("truncated") format_, ntracks, tickdiv = struct.unpack(">HHH", chunk) if format_ > 1: raise SMFError("Not supported format %d" % format_) if tickdiv >> 15: # fps = (-(tickdiv >> 8)) & 0xFF # subres = tickdiv & 0xFF # never saw one of those raise SMFError("Not supported timing interval") # get a list of events and tempo changes for each track tracks = [] first_tempos = None for tracknum in range(ntracks): identifier, chunk = read_chunk(fileobj) if identifier != b"MTrk": continue events, tempos = _read_track(chunk) # In case of format == 1, copy the first tempo list to all tracks first_tempos = first_tempos or tempos if format_ == 1: tempos = list(first_tempos) events += tempos events.sort() tracks.append(events) # calculate the duration of each track durations = [] for events in tracks: tempo = 500000 parts = [] deltasum = 0 for (dummy, type_, data) in events: if type_ == TEMPO: parts.append((deltasum, tempo)) tempo = data deltasum = 0 else: deltasum += data parts.append((deltasum, tempo)) duration = 0 for (deltasum, tempo) in parts: quarter, tpq = deltasum / float(tickdiv), tempo duration += (quarter * tpq) duration /= 10 ** 6 durations.append(duration) # return the longest one return max(durations) class SMFInfo(StreamInfo): """SMFInfo() Attributes: length (`float`): Length in seconds """ def __init__(self, fileobj): """Raises SMFError""" self.length = _read_midi_length(fileobj) def pprint(self): return u"SMF, %.2f seconds" % self.length class SMF(FileType): """SMF(filething) Standard MIDI File (SMF) Attributes: info (`SMFInfo`) tags: `None` """ _mimes = ["audio/midi", "audio/x-midi"] @loadfile() def load(self, filething): try: self.info = SMFInfo(filething.fileobj) except IOError as e: raise SMFError(e) def add_tags(self): raise SMFError("doesn't support tags") @staticmethod def score(filename, fileobj, header): filename = filename.lower() return header.startswith(b"MThd") and ( endswith(filename, ".mid") or endswith(filename, ".midi")) Open = SMF error = SMFError __all__ = ["SMF"] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/tak.py0000664000175000017500000001562600000000000016276 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008 Lukáš Lalinský # Copyright (C) 2019 Philipp Wolfer # # 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. """Tom's lossless Audio Kompressor (TAK) streams with APEv2 tags. TAK is a lossless audio compressor developed by Thomas Becker. For more information, see: * http://www.thbeck.de/Tak/Tak.html * http://wiki.hydrogenaudio.org/index.php?title=TAK """ __all__ = ["TAK", "Open", "delete"] import struct from mutagen import StreamInfo from mutagen.apev2 import ( APEv2File, delete, error, ) from mutagen._util import ( BitReader, BitReaderError, convert_error, enum, endswith, ) @enum class TAKMetadata(object): END = 0 STREAM_INFO = 1 SEEK_TABLE = 2 # Removed in TAK 1.1.1 SIMPLE_WAVE_DATA = 3 ENCODER_INFO = 4 UNUSED_SPACE = 5 # New in TAK 1.0.3 MD5 = 6 # New in TAK 1.1.1 LAST_FRAME_INFO = 7 # New in TAK 1.1.1 CRC_SIZE = 3 ENCODER_INFO_CODEC_BITS = 6 ENCODER_INFO_PROFILE_BITS = 4 ENCODER_INFO_TOTAL_BITS = ENCODER_INFO_CODEC_BITS + ENCODER_INFO_PROFILE_BITS SIZE_INFO_FRAME_DURATION_BITS = 4 SIZE_INFO_SAMPLE_NUM_BITS = 35 SIZE_INFO_TOTAL_BITS = (SIZE_INFO_FRAME_DURATION_BITS + SIZE_INFO_SAMPLE_NUM_BITS) AUDIO_FORMAT_DATA_TYPE_BITS = 3 AUDIO_FORMAT_SAMPLE_RATE_BITS = 18 AUDIO_FORMAT_SAMPLE_BITS_BITS = 5 AUDIO_FORMAT_CHANNEL_NUM_BITS = 4 AUDIO_FORMAT_HAS_EXTENSION_BITS = 1 AUDIO_FORMAT_BITS_MIN = 31 AUDIO_FORMAT_BITS_MAX = 31 + 102 SAMPLE_RATE_MIN = 6000 SAMPLE_BITS_MIN = 8 CHANNEL_NUM_MIN = 1 STREAM_INFO_BITS_MIN = (ENCODER_INFO_TOTAL_BITS + SIZE_INFO_TOTAL_BITS + AUDIO_FORMAT_BITS_MIN) STREAM_INFO_BITS_MAX = (ENCODER_INFO_TOTAL_BITS + SIZE_INFO_TOTAL_BITS + AUDIO_FORMAT_BITS_MAX) STREAM_INFO_SIZE_MIN = (STREAM_INFO_BITS_MIN + 7) / 8 STREAM_INFO_SIZE_MAX = (STREAM_INFO_BITS_MAX + 7) / 8 class _LSBBitReader(BitReader): """BitReader implementation which reads bits starting at LSB in each byte. """ def _lsb(self, count): value = self._buffer & 0xff >> (8 - count) self._buffer = self._buffer >> count self._bits -= count return value def bits(self, count): """Reads `count` bits and returns an uint, LSB read first. May raise BitReaderError if not enough data could be read or IOError by the underlying file object. """ if count < 0: raise ValueError value = 0 if count <= self._bits: value = self._lsb(count) else: # First read all available bits shift = 0 remaining = count if self._bits > 0: remaining -= self._bits shift = self._bits value = self._lsb(self._bits) assert self._bits == 0 # Now add additional bytes n_bytes = (remaining - 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): if remaining > 8: # Use full byte remaining -= 8 value = (b << shift) | value shift += 8 else: self._buffer = b self._bits = 8 b = self._lsb(remaining) value = (b << shift) | value assert 0 <= self._bits < 8 return value class TAKHeaderError(error): pass class TAKInfo(StreamInfo): """TAK stream information. Attributes: channels (`int`): number of audio channels length (`float`): file length in seconds, as a float sample_rate (`int`): audio sampling rate in Hz bits_per_sample (`int`): audio sample size encoder_info (`mutagen.text`): encoder version """ channels = 0 length = 0 sample_rate = 0 bitrate = 0 encoder_info = "" @convert_error(IOError, TAKHeaderError) @convert_error(BitReaderError, TAKHeaderError) def __init__(self, fileobj): stream_id = fileobj.read(4) if len(stream_id) != 4 or not stream_id == b"tBaK": raise TAKHeaderError("not a TAK file") bitreader = _LSBBitReader(fileobj) while True: type = TAKMetadata(bitreader.bits(7)) bitreader.skip(1) # Unused size = struct.unpack(" 0: self.length = self.number_of_samples / float(self.sample_rate) def _parse_stream_info(self, bitreader, size): if size < STREAM_INFO_SIZE_MIN or size > STREAM_INFO_SIZE_MAX: raise TAKHeaderError("stream info has invalid length") # Encoder Info bitreader.skip(ENCODER_INFO_CODEC_BITS) bitreader.skip(ENCODER_INFO_PROFILE_BITS) # Size Info bitreader.skip(SIZE_INFO_FRAME_DURATION_BITS) self.number_of_samples = bitreader.bits(SIZE_INFO_SAMPLE_NUM_BITS) # Audio Format bitreader.skip(AUDIO_FORMAT_DATA_TYPE_BITS) self.sample_rate = (bitreader.bits(AUDIO_FORMAT_SAMPLE_RATE_BITS) + SAMPLE_RATE_MIN) self.bits_per_sample = (bitreader.bits(AUDIO_FORMAT_SAMPLE_BITS_BITS) + SAMPLE_BITS_MIN) self.channels = (bitreader.bits(AUDIO_FORMAT_CHANNEL_NUM_BITS) + CHANNEL_NUM_MIN) bitreader.skip(AUDIO_FORMAT_HAS_EXTENSION_BITS) def _parse_encoder_info(self, bitreader, size): patch = bitreader.bits(8) minor = bitreader.bits(8) major = bitreader.bits(8) self.encoder_info = "TAK %d.%d.%d" % (major, minor, patch) def pprint(self): return u"%s, %d Hz, %d bits, %.2f seconds, %d channel(s)" % ( self.encoder_info or "TAK", self.sample_rate, self.bits_per_sample, self.length, self.channels) class TAK(APEv2File): """TAK(filething) Arguments: filething (filething) Attributes: info (`TAKInfo`) """ _Info = TAKInfo _mimes = ["audio/x-tak"] @staticmethod def score(filename, fileobj, header): return header.startswith(b"tBaK") + endswith(filename.lower(), ".tak") Open = TAK ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/trueaudio.py0000664000175000017500000000504200000000000017507 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """True Audio audio stream information and tags. True Audio is a lossless format designed for real-time encoding and decoding. This module is based on the documentation at http://www.true-audio.com/TTA_Lossless_Audio_Codec\\_-_Format_Description True Audio files use ID3 tags. """ __all__ = ["TrueAudio", "Open", "delete", "EasyTrueAudio"] from mutagen import StreamInfo from mutagen.id3 import ID3FileType, delete from mutagen._util import cdata, MutagenError, convert_error, endswith class error(MutagenError): pass class TrueAudioHeaderError(error): pass class TrueAudioInfo(StreamInfo): """TrueAudioInfo() True Audio stream information. Attributes: length (`float`): audio length, in seconds sample_rate (`int`): audio sample rate, in Hz """ @convert_error(IOError, TrueAudioHeaderError) def __init__(self, fileobj, offset): """Raises TrueAudioHeaderError""" 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): """TrueAudio(filething, ID3=None) A True Audio file. Arguments: filething (filething) ID3 (mutagen.id3.ID3) Attributes: info (`TrueAudioInfo`) tags (`mutagen.id3.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): """EasyTrueAudio(filething, ID3=None) Like MP3, but uses EasyID3 for tags. Arguments: filething (filething) ID3 (mutagen.id3.ID3) Attributes: info (`TrueAudioInfo`) tags (`mutagen.easyid3.EasyID3`) """ from mutagen.easyid3 import EasyID3 as ID3 ID3 = ID3 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/wave.py0000664000175000017500000001326100000000000016452 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2017 Borewit # Copyright (C) 2019-2020 Philipp Wolfer # # 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. """Microsoft WAVE/RIFF audio file/stream information and tags.""" import sys import struct from mutagen import StreamInfo, FileType from mutagen.id3 import ID3 from mutagen._riff import RiffFile, InvalidChunk from mutagen._iff import error as IffError from mutagen.id3._util import ID3NoHeaderError, error as ID3Error from mutagen._util import ( convert_error, endswith, loadfile, reraise, ) __all__ = ["WAVE", "Open", "delete"] class error(IffError): """WAVE stream parsing errors.""" class _WaveFile(RiffFile): """Representation of a RIFF/WAVE file""" def __init__(self, fileobj): RiffFile.__init__(self, fileobj) if self.file_type != u'WAVE': raise error("Expected RIFF/WAVE.") # Normalize ID3v2-tag-chunk to lowercase if u'ID3' in self: self[u'ID3'].id = u'id3' class WaveStreamInfo(StreamInfo): """WaveStreamInfo() Microsoft WAVE file information. Information is parsed from the 'fmt' & 'data'chunk of the RIFF/WAVE file Attributes: length (`float`): audio length, in seconds bitrate (`int`): audio bitrate, in bits per second channels (`int`): The number of audio channels sample_rate (`int`): audio sample rate, in Hz bits_per_sample (`int`): The audio sample size """ length = 0.0 bitrate = 0 channels = 0 sample_rate = 0 bits_per_sample = 0 SIZE = 16 @convert_error(IOError, error) def __init__(self, fileobj): """Raises error""" wave_file = _WaveFile(fileobj) try: format_chunk = wave_file[u'fmt'] except KeyError as e: raise error(str(e)) data = format_chunk.read() if len(data) < 16: raise InvalidChunk() # RIFF: http://soundfile.sapp.org/doc/WaveFormat/ # Python struct.unpack: # https://docs.python.org/2/library/struct.html#byte-order-size-and-alignment info = struct.unpack(' 0: try: data_chunk = wave_file[u'data'] self._number_of_samples = data_chunk.data_size / block_align except KeyError: pass if self.sample_rate > 0: self.length = self._number_of_samples / self.sample_rate def pprint(self): return u"%d channel RIFF @ %d bps, %s Hz, %.2f seconds" % ( self.channels, self.bitrate, self.sample_rate, self.length) class _WaveID3(ID3): """A Wave file with ID3v2 tags""" def _pre_load_header(self, fileobj): try: fileobj.seek(_WaveFile(fileobj)[u'id3'].data_offset) except (InvalidChunk, KeyError): raise ID3NoHeaderError("No ID3 chunk") @convert_error(IOError, error) @loadfile(writable=True) def save(self, filething, v1=1, v2_version=4, v23_sep='/', padding=None): """Save ID3v2 data to the Wave/RIFF file""" fileobj = filething.fileobj wave_file = _WaveFile(fileobj) if u'id3' not in wave_file: wave_file.insert_chunk(u'id3') chunk = wave_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]) chunk.resize(len(data)) chunk.write(data) def delete(self, filething): """Completely removes the ID3 chunk from the RIFF/WAVE file""" delete(filething) self.clear() @convert_error(IOError, error) @loadfile(method=False, writable=True) def delete(filething): """Completely removes the ID3 chunk from the RIFF/WAVE file""" try: _WaveFile(filething.fileobj).delete_chunk(u'id3') except KeyError: pass class WAVE(FileType): """WAVE(filething) A Waveform Audio File Format (WAVE, or more commonly known as WAV due to its filename extension) Arguments: filething (filething) Attributes: tags (`mutagen.id3.ID3`) info (`WaveStreamInfo`) """ _mimes = ["audio/wav", "audio/wave"] @staticmethod def score(filename, fileobj, header): filename = filename.lower() return (header.startswith(b"RIFF") + (header[8:12] == b'WAVE') + endswith(filename, b".wav") + endswith(filename, b".wave")) def add_tags(self): """Add an empty ID3 tag to the file.""" if self.tags is None: self.tags = _WaveID3() else: raise error("an ID3 tag already exists") @convert_error(IOError, error) @loadfile() def load(self, filething, **kwargs): """Load stream and tag information from a file.""" fileobj = filething.fileobj self.info = WaveStreamInfo(fileobj) fileobj.seek(0, 0) try: self.tags = _WaveID3(fileobj, **kwargs) except ID3NoHeaderError: self.tags = None except ID3Error as e: raise error(e) else: self.tags.filename = self.filename Open = WAVE ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/mutagen/wavpack.py0000664000175000017500000001051200000000000017140 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. """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, convert_error 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 @convert_error(IOError, WavPackHeaderError) 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 (int): number of audio channels (1 or 2) length (float): file length in seconds, as a float sample_rate (int): audio sampling rate in Hz bits_per_sample (int): audio sample size version (int): 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] self.bits_per_sample = ((header.flags & 3) + 1) * 8 # most common multiplier (DSD64) if (header.flags >> 31) & 1: self.sample_rate *= 4 self.bits_per_sample = 1 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): """WavPack(filething) Arguments: filething (filething) Attributes: info (`WavPackInfo`) """ _Info = WavPackInfo _mimes = ["audio/x-wavpack"] @staticmethod def score(filename, fileobj, header): return header.startswith(b"wvpk") * 2 Open = WavPack ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1596184724.8191056 mutagen-1.45.1/mutagen.egg-info/0000775000175000017500000000000000000000000016625 5ustar00lazkalazka00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1596184724.0 mutagen-1.45.1/mutagen.egg-info/PKG-INFO0000664000175000017500000000404200000000000017722 0ustar00lazkalazka00000000000000Metadata-Version: 1.2 Name: mutagen Version: 1.45.1 Summary: read and write audio tags for many formats Home-page: https://github.com/quodlibet/mutagen Author: Christoph Reiter Author-email: reiter.christoph@gmail.com License: GPL-2.0-or-later Description: .. image:: https://raw.githubusercontent.com/quodlibet/mutagen/master/docs/images/logo.svg :align: center :width: 400px | Mutagen is a Python module to handle audio metadata. It supports ASF, FLAC, MP4, 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 with Python 3.5+ (CPython and PyPy) on Linux, Windows and macOS, and has no dependencies outside the Python standard library. Mutagen is licensed under the GPL version 2 or later. For more information visit https://mutagen.readthedocs.org .. image:: https://codecov.io/gh/quodlibet/mutagen/branch/master/graph/badge.svg :target: https://codecov.io/gh/quodlibet/mutagen Platform: UNKNOWN Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+) Classifier: Topic :: Multimedia :: Sound/Audio Requires-Python: >=3.5, <4 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1596184724.0 mutagen-1.45.1/mutagen.egg-info/SOURCES.txt0000664000175000017500000001540400000000000020515 0ustar00lazkalazka00000000000000COPYING MANIFEST.in NEWS README.rst setup.cfg setup.py docs/Makefile docs/changelog.rst docs/conf.py docs/contact.rst docs/extra.css docs/id3_frames_gen.py docs/index.rst docs/api/aac.rst docs/api/ac3.rst docs/api/aiff.rst docs/api/ape.rst docs/api/asf.rst docs/api/base.rst docs/api/dsdiff.rst docs/api/dsf.rst docs/api/flac.rst docs/api/id3.rst docs/api/id3_frames.rst docs/api/index.rst docs/api/monkeysaudio.rst docs/api/mp3.rst docs/api/mp4.rst docs/api/musepack.rst docs/api/ogg.rst docs/api/oggflac.rst docs/api/oggopus.rst docs/api/oggspeex.rst docs/api/oggtheora.rst docs/api/oggvorbis.rst docs/api/optimfrog.rst docs/api/smf.rst docs/api/tak.rst docs/api/trueaudio.rst docs/api/vcomment.rst docs/api/wave.rst docs/api/wavpack.rst docs/images/Makefile docs/images/favicon.ico docs/images/logo-small.svg docs/images/logo.svg docs/man/Makefile docs/man/index.rst docs/man/mid3cp.rst docs/man/mid3iconv.rst docs/man/mid3v2.rst docs/man/moggsplit.rst docs/man/mutagen-inspect.rst docs/man/mutagen-pony.rst docs/user/apev2.rst docs/user/classes.rst docs/user/filelike.rst docs/user/gettingstarted.rst docs/user/id3.rst docs/user/index.rst docs/user/mp4.rst docs/user/padding.rst docs/user/vcomment.rst docs/user/examples/fileobj-iface.py man/mid3cp.1 man/mid3iconv.1 man/mid3v2.1 man/moggsplit.1 man/mutagen-inspect.1 man/mutagen-pony.1 mutagen/__init__.py mutagen/_constants.py mutagen/_file.py mutagen/_iff.py mutagen/_riff.py mutagen/_tags.py mutagen/_util.py mutagen/_vorbis.py mutagen/aac.py mutagen/ac3.py mutagen/aiff.py mutagen/apev2.py mutagen/dsdiff.py mutagen/dsf.py mutagen/easyid3.py mutagen/easymp4.py mutagen/flac.py mutagen/m4a.py mutagen/monkeysaudio.py mutagen/musepack.py mutagen/ogg.py mutagen/oggflac.py mutagen/oggopus.py mutagen/oggspeex.py mutagen/oggtheora.py mutagen/oggvorbis.py mutagen/optimfrog.py mutagen/smf.py mutagen/tak.py mutagen/trueaudio.py mutagen/wave.py mutagen/wavpack.py mutagen.egg-info/PKG-INFO mutagen.egg-info/SOURCES.txt mutagen.egg-info/dependency_links.txt mutagen.egg-info/entry_points.txt mutagen.egg-info/top_level.txt mutagen/_senf/README.rst mutagen/_senf/__init__.py mutagen/_senf/__init__.pyi mutagen/_senf/_argv.py mutagen/_senf/_compat.py mutagen/_senf/_environ.py mutagen/_senf/_fsnative.py mutagen/_senf/_print.py mutagen/_senf/_stdlib.py mutagen/_senf/_temp.py mutagen/_senf/_winansi.py mutagen/_senf/_winapi.py mutagen/_senf/py.typed mutagen/_tools/__init__.py mutagen/_tools/_util.py mutagen/_tools/mid3cp.py mutagen/_tools/mid3iconv.py mutagen/_tools/mid3v2.py mutagen/_tools/moggsplit.py mutagen/_tools/mutagen_inspect.py mutagen/_tools/mutagen_pony.py mutagen/asf/__init__.py mutagen/asf/_attrs.py mutagen/asf/_objects.py mutagen/asf/_util.py mutagen/id3/__init__.py mutagen/id3/_file.py mutagen/id3/_frames.py mutagen/id3/_id3v1.py mutagen/id3/_specs.py mutagen/id3/_tags.py mutagen/id3/_util.py mutagen/mp3/__init__.py mutagen/mp3/_util.py mutagen/mp4/__init__.py mutagen/mp4/_as_entry.py mutagen/mp4/_atom.py mutagen/mp4/_util.py tests/__init__.py tests/test___init__.py tests/test__id3frames.py tests/test__id3specs.py tests/test__id3util.py tests/test__iff.py tests/test__riff.py tests/test__util.py tests/test__vorbis.py tests/test_aac.py tests/test_ac3.py tests/test_aiff.py tests/test_apev2.py tests/test_asf.py tests/test_dsdiff.py tests/test_dsf.py tests/test_easyid3.py tests/test_easymp4.py tests/test_encoding.py tests/test_flac.py tests/test_id3.py tests/test_m4a.py tests/test_monkeysaudio.py tests/test_mp3.py tests/test_mp4.py tests/test_musepack.py tests/test_ogg.py tests/test_oggflac.py tests/test_oggopus.py tests/test_oggspeex.py tests/test_oggtheora.py tests/test_oggvorbis.py tests/test_optimfrog.py tests/test_smf.py tests/test_tak.py tests/test_tools.py tests/test_tools_mid3cp.py tests/test_tools_mid3iconv.py tests/test_tools_mid3v2.py tests/test_tools_moggsplit.py tests/test_tools_mutagen_inspect.py tests/test_tools_mutagen_pony.py tests/test_tools_util.py tests/test_trueaudio.py tests/test_wave.py tests/test_wavpack.py tests/data/106-invalid-streaminfo.flac tests/data/106-short-picture-block-size.flac tests/data/11k-1ch-2s-silence.aif tests/data/145-invalid-item-count.apev2 tests/data/2822400-1ch-0s-silence.dff tests/data/2822400-1ch-0s-silence.dsf tests/data/48k-2ch-s16-silence.aif tests/data/52-overwritten-metadata.flac tests/data/52-too-short-block-size.flac tests/data/5644800-2ch-s01-silence-dst.dff tests/data/5644800-2ch-s01-silence.dff tests/data/5644800-2ch-s01-silence.dsf tests/data/64bit.mp4 tests/data/8k-1ch-1s-silence.aif tests/data/8k-1ch-3.5s-silence.aif tests/data/8k-4ch-1s-silence.aif tests/data/97-unknown-23-update.mp3 tests/data/adif.aac tests/data/alac.m4a tests/data/almostempty.mpc tests/data/apev2-lyricsv2.mp3 tests/data/audacious-trailing-id32-apev2.mp3 tests/data/audacious-trailing-id32-id31.mp3 tests/data/bad-POPM-frame.mp3 tests/data/bad-TYER-frame.mp3 tests/data/bad-xing.mp3 tests/data/brokentag.apev2 tests/data/click.mpc tests/data/covr-with-name.m4a tests/data/dsd.wv tests/data/empty.aac tests/data/empty.ofr tests/data/empty.ofs tests/data/empty.ogg tests/data/empty.oggflac tests/data/empty.spx tests/data/empty.tta tests/data/emptyfile.mp3 tests/data/ep7.m4b tests/data/ep9.m4b tests/data/example.opus tests/data/flac_application.flac tests/data/has-tags.m4a tests/data/has-tags.tak tests/data/id3v1v2-combined.mp3 tests/data/id3v22-test.mp3 tests/data/id3v23_unsynch.id3 tests/data/image.jpg tests/data/issue_21.id3 tests/data/issue_29.wma tests/data/lame-peak.mp3 tests/data/lame.mp3 tests/data/lame397v9short.mp3 tests/data/mac-390-hdr.ape tests/data/mac-396.ape tests/data/mac-399.ape tests/data/multipage-setup.ogg tests/data/multipagecomment.ogg tests/data/multiplexed.spx tests/data/nero-chapters.m4b tests/data/no-tags.3g2 tests/data/no-tags.flac tests/data/no-tags.m4a tests/data/no_length.wv tests/data/oldtag.apev2 tests/data/ooming-header.flac tests/data/sample.mid tests/data/sample.oggtheora tests/data/sample_bitrate.oggtheora tests/data/sample_length.oggtheora tests/data/silence-1.wma tests/data/silence-2.wma tests/data/silence-2s-44100-16.ofr tests/data/silence-2s-44100-16.ofs tests/data/silence-2s-PCM-16000-08-ID3v23.wav tests/data/silence-2s-PCM-16000-08-notags.wav tests/data/silence-2s-PCM-44100-16-ID3v23.wav tests/data/silence-3.wma tests/data/silence-44-s-mpeg2.mp3 tests/data/silence-44-s-mpeg25.mp3 tests/data/silence-44-s-v1.mp3 tests/data/silence-44-s.ac3 tests/data/silence-44-s.eac3 tests/data/silence-44-s.flac tests/data/silence-44-s.mp3 tests/data/silence-44-s.tak tests/data/silence-44-s.wv tests/data/sv4_header.mpc tests/data/sv5_header.mpc tests/data/sv8_header.mpc tests/data/too-short.mp3 tests/data/truncated-64bit.mp4 tests/data/variable-block.flac tests/data/vbri.mp3 tests/data/with-id3.aif tests/data/with-id3.dsf tests/data/without-id3.dsf tests/data/xing.mp3 tests/quality/__init__.py tests/quality/test_flake8.py././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1596184724.0 mutagen-1.45.1/mutagen.egg-info/dependency_links.txt0000664000175000017500000000000100000000000022673 0ustar00lazkalazka00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1596184724.0 mutagen-1.45.1/mutagen.egg-info/entry_points.txt0000664000175000017500000000047700000000000022133 0ustar00lazkalazka00000000000000[console_scripts] mid3cp = mutagen._tools.mid3cp:entry_point mid3iconv = mutagen._tools.mid3iconv:entry_point mid3v2 = mutagen._tools.mid3v2:entry_point moggsplit = mutagen._tools.moggsplit:entry_point mutagen-inspect = mutagen._tools.mutagen_inspect:entry_point mutagen-pony = mutagen._tools.mutagen_pony:entry_point ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1596184724.0 mutagen-1.45.1/mutagen.egg-info/top_level.txt0000664000175000017500000000001000000000000021346 0ustar00lazkalazka00000000000000mutagen ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1596184724.831105 mutagen-1.45.1/setup.cfg0000664000175000017500000000061100000000000015312 0ustar00lazkalazka00000000000000[coverage:run] include = mutagen/* tests/* omit = mutagen/_senf/* [flake8] ignore = E128,W601,E402,E731,W503,E741,E305,E121,E124,W504 exclude = mutagen/_senf,build,dist max-line-length = 88 [tool:pytest] markers = quality junit_family = xunit2 [mypy] python_version = 3.6 [mypy-setup] ignore_errors = True [mypy-tests.*] ignore_errors = True [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1596184722.0 mutagen-1.45.1/setup.py0000775000175000017500000002276200000000000015221 0ustar00lazkalazka00000000000000#!/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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. import glob import os import shutil import sys import subprocess import tarfile from setuptools import setup, Command, Distribution from distutils import dir_util def get_command_class(name): # Returns the right class for either distutils or setuptools return Distribution({}).get_command_class(name) distutils_clean = get_command_class("clean") 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) distutils_sdist = get_command_class("sdist") def check_setuptools_for_dist(): if "setuptools" not in sys.modules: raise Exception("setuptools not available") version = tuple(map(int, sys.modules["setuptools"].__version__.split("."))) if version < (24, 2, 0): raise Exception("setuptools too old") class sdist(distutils_sdist): def run(self): check_setuptools_for_dist() distutils_sdist.run(self) class distcheck(sdist): def _check_manifest(self): assert self.get_archive_files() # make sure MANIFEST.in includes all tracked files if subprocess.call(["git", "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( ["git", "ls-tree", "-r", "HEAD", "--name-only"], stdout=subprocess.PIPE, universal_newlines=True) out, err = process.communicate() assert process.returncode == 0 tracked_files = out.splitlines() for ignore in [".gitignore", ".codecov.yml", ".github/workflows/test.yml"]: tracked_files.remove(ignore) tracked_files = [ f for f in tracked_files if os.path.dirname(f) != "fuzzing"] 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", "--root", "../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([ sys.executable, "-m", "sphinx", "-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"), ("no-quality", None, "skip code quality tests"), ] def initialize_options(self): self.to_run = [] self.exitfirst = False self.no_quality = False def finalize_options(self): if self.to_run: self.to_run = self.to_run.split(",") self.exitfirst = bool(self.exitfirst) self.no_quality = bool(self.no_quality) def run(self): import tests status = tests.unit(self.to_run, self.exitfirst, self.no_quality) if status != 0: raise SystemExit(status) class quality_cmd(Command): description = "run flake8 tests" user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): import tests status = tests.check() if status != 0: raise SystemExit(status) 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/*"], omit=["mutagen/_senf/*"]) print("Coverage summary: file://%s/index.html" % dest) if __name__ == "__main__": if sys.version_info[0] < 3: raise Exception("Python 2 no longer supported") from mutagen import version with open('README.rst') as h: long_description = h.read() # convert to a setuptools compatible version string if version[-1] == -1: version_string = ".".join(map(str, version[:-1])) + ".dev0" else: version_string = ".".join(map(str, version)) cmd_classes = { "clean": clean, "test": test_cmd, "quality": quality_cmd, "coverage": coverage_cmd, "distcheck": distcheck, "build_sphinx": build_sphinx, "sdist": sdist, } setup(cmdclass=cmd_classes, name="mutagen", version=version_string, url="https://github.com/quodlibet/mutagen", description="read and write audio tags for many formats", author="Christoph Reiter", author_email="reiter.christoph@gmail.com", license="GPL-2.0-or-later", classifiers=[ 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ('License :: OSI Approved :: ' 'GNU General Public License v2 or later (GPLv2+)'), 'Topic :: Multimedia :: Sound/Audio', ], packages=[ "mutagen", "mutagen.id3", "mutagen.mp4", "mutagen.asf", "mutagen.mp3", "mutagen._senf", "mutagen._tools", ], data_files=[ ('share/man/man1', glob.glob("man/*.1")), ], python_requires=( '>=3.5, <4'), entry_points={ 'console_scripts': [ 'mid3cp=mutagen._tools.mid3cp:entry_point', 'mid3iconv=mutagen._tools.mid3iconv:entry_point', 'mid3v2=mutagen._tools.mid3v2:entry_point', 'moggsplit=mutagen._tools.moggsplit:entry_point', 'mutagen-inspect=mutagen._tools.mutagen_inspect:entry_point', 'mutagen-pony=mutagen._tools.mutagen_pony:entry_point', ], }, long_description=long_description, ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1596184724.8231053 mutagen-1.45.1/tests/0000775000175000017500000000000000000000000014635 5ustar00lazkalazka00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/__init__.py0000664000175000017500000000624200000000000016752 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import re import os import sys import shutil import contextlib from io import StringIO from unittest import TestCase as BaseTestCase try: import pytest except ImportError: raise SystemExit("pytest missing: sudo apt-get install python-pytest") from mutagen._senf import text2fsn, fsn2text, path2fsn, mkstemp, fsnative DATA_DIR = os.path.join( os.path.dirname(os.path.realpath(path2fsn(__file__))), "data") assert isinstance(DATA_DIR, fsnative) if fsn2text(text2fsn(u"öäü")) != u"öäü": raise RuntimeError("This test suite needs a unicode locale encoding. " "Try setting LANG=C.UTF-8") def get_temp_copy(path): """Returns a copy of the file with the same extension""" ext = os.path.splitext(path)[-1] fd, filename = mkstemp(suffix=ext) os.close(fd) shutil.copy(path, filename) return filename def get_temp_empty(ext=""): """Returns an empty file with the extension""" fd, filename = mkstemp(suffix=ext) os.close(fd) return filename @contextlib.contextmanager def capture_output(): """ with capture_output() as (stdout, stderr): some_action() print stdout.getvalue(), stderr.getvalue() """ err = StringIO() out = StringIO() old_err = sys.stderr old_out = sys.stdout sys.stderr = err sys.stdout = out try: yield (out, err) finally: sys.stderr = old_err sys.stdout = old_out 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 assertEquals = BaseTestCase.assertEqual assertNotEquals = BaseTestCase.assertNotEqual assert_ = BaseTestCase.assertTrue 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) 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) def check(): return pytest.main(args=[os.path.join("tests", "quality")]) def unit(run=[], exitfirst=False, no_quality=False): args = [] if run: args.append("-k") args.append(" or ".join(run)) if exitfirst: args.append("-x") if no_quality: args.extend(["-m", "not quality"]) args.append("tests") return pytest.main(args=args) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1596184724.827105 mutagen-1.45.1/tests/data/0000775000175000017500000000000000000000000015546 5ustar00lazkalazka00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/106-invalid-streaminfo.flac0000644000175000017500000001112400000000000022471 0ustar00lazkalazka00000000000000fLaC Bz<YkYlYeeYb)YwQYpZ$YycY~YS+9Y TLY ]Y ZY OY HaYAX YFxYgYYY^vY7Y-BY YY#\_Y$*Y-Y*eY?Y8Y1/kY6Y FY!i3Y"Py#XE././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/106-short-picture-block-size.flac0000644000175000017500000001112400000000000023546 0ustar00lazkalazka00000000000000fLaC" BzAf鄚0 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<AIFFCOMM@@ SSND>././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/8k-1ch-3.5s-silence.aif0000644000175000017500000015536600000000000021346 0ustar00lazkalazka00000000000000FORMAIFFCOMMm`@ SSND././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/8k-4ch-1s-silence.aif0000644000175000017500000017511200000000000021173 0ustar00lazkalazka00000000000000FORMBAIFFCHAN sCOMM@@ SSND././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/97-unknown-23-update.mp30000644000175000017500000004000000000000000021615 0ustar00lazkalazka00000000000000ID3 TIT2aaaaaaaaaaaaaaaaaaaaaaa vvvvvvvvvvvvvvvvveeeeeerrrrrrrrrrrrrrrryyyyyyyyyyyyy loooooooooooooooooooooooooooooonnnnnnggggggggggggg ttttttttttttttttiiiiiiiiiiiiiittttttttttllllllllllllllleeeeeeeeeeeeeeeeeeeTPE1aaaaaaaaaaaaaaaaaaaaaaa vvvvvvvvvvvvvvvvveeeeeerrrrrrrrrrrrrrrryyyyyyyyyyyyy loooooooooooooooooooooooooooooonnnnnnggggggggggggg artist namedu=@"`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 4TAGaaaaaaaaaaaaaaaaaaaaaaa vvvvvvaaaaaaaaaaaaaaaaaaaaaaa vvvvvv././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/adif.aac0000644000175000017500000001000000000000000017104 0ustar00lazkalazka00000000000000ADIF>! d! d! d! d! d8! d7p! d7p! d7p! d7p! d7p! d7p! d7p! d7p! d7p! d7p! d7p! d././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/alac.m4a0000644000175000017500000002240400000000000017051 0ustar00lazkalazka00000000000000 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                                                                                 _W././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/almostempty.mpc0000644000175000017500000000002100000000000020614 0ustar00lazkalazka00000000000000ID3 dsfdsfds MP+ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/apev2-lyricsv2.mp30000644000175000017500000014135200000000000020763 0ustar00lazkalazka00000000000000ID3 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#././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/audacious-trailing-id32-apev2.mp30000644000175000017500000000553200000000000023530 0ustar00lazkalazka00000000000000Xing $$$$000888AAAAIIIQQQYYYYaaaeeemmmmuuu}}}9LAME3.99r8,$QB A( Vx5I# @Ap&bFa9O˿\>#Ed{܎FH "zn`-D<"@BmuTQH ^]* WЖBcbY- l/3\gc俳Y%*&Su:ޜ ޜ9333+`ܥqZɀG80m&JHj9ϧ(ٽ7[&쩓eeXR} /n fn=Md`he34Z82F2"te"C GڤɊnҮtK|b8R9(Q/vtscT`B=iӪ >nJO("~FtF?j!vKЄ!@ aaNJ]"/gq R휐⣫n(>F0D**]8@qB0h: .c Ȳ޶;,<&31<a"`( ^F,@Gh^WlKj!A,RtŹ? jEB( ^J, N0QN :ANp;ӦY75!{ma "@Db( 0 Qi{Yd}oasl" E[C/&Q hܗ}eA4$]hXuP( 8`F&W?9;4&dzGiR@úb mi2w%;i# ^F,Pɒ0#7*`( h^,XaP4 ֙ZJd&w"Wt[E`!FZj( h^Ʉ0D]1 Fg:Y Mns%XPm- ^( ƸPFp"E}5}Nvc+YjL:6j*oP\ QIJ  -d" z>0Em>1㴐>.f ( ʼ>FL 2oՍ#C(!v dO(#ćnl(h4Q戍ݡCMoa^ʃ~eQD+ʂc *̋CO8>D0Zuj7DlMjqv`aMibܪ9H'̥+"4* ߋ:  [rI@@Bֺ%7_KgwI(yy>oihjjKɁ(R[V Ht%%T(>F,$*cjT|1s @jS b .bb~Jz*Hj?GboVd0x( P^{Lh~j@ [.$%{4$ZMc8\7&-q( bߒ 2,StJi di K I  ] # ٞxB!}ZZU!( 0yL j\V(pGc&IhBrmϔA:˘ewD8E"Wr @8S ( y.3jμvZEb$ז F뭿oߋ VۗX= RS>@( n:D)Udt^ؤWu(E* fږTI $6h8( HI:p pA2OC2(kЦ`¢$a7Z=HM%,B}+ HZ0e ϛ|XI*LX#!_xk6n$n4E6L8Un{wINV+аV/fD3*amFv<+*fƅ=bعű=M_W}_I#8(o؂Ǚʘ".)Sx/vJyW3F^{ (^)ࠠAG(($P_ĕLAMAPETAGEXArtistadfsasafTitledsafdasAlbumgsagCommentsadsagGenreHard RockfdsagdsagTrack32Year2001APETAGEXID3uTALBsafdsaTRCK42TYER2009COMM engsafdsafTIT2safdsafTPE1dsdgsgTCONblub3DIu././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/audacious-trailing-id32-id31.mp30000644000175000017500000003565000000000000023257 0ustar00lazkalazka00000000000000du=@"`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 Data2004ID36TDRC2004TCONSilenceCOMM engsafsdfTRCK2TPE1pimanTALBQuod Libet Test DataTIT1 SilenceTIT2SilenceTYER2004TLEN30003DI6././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/bad-POPM-frame.mp30000644000175000017500000000372000000000000020556 0ustar00lazkalazka00000000000000ID3 TENCWXXXTCOPTIT2Emit and exudeTRCK4TDRC2004TCON12TALBemit and exudePOPM#Windows Media Player 9 Series{eTCOM pjat lainTOPETPE1sheCOMM  häst Xing>. !$&(,.0268:=@BEGJMOQTWY[_aceikmpsuxz}7LAME3.92 $E.0+v ?@  ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/bad-TYER-frame.mp30000644000175000017500000011400000000000000020560 0ustar00lazkalazka00000000000000ID3 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.net././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/bad-xing.mp30000644000175000017500000001000000000000000017645 0ustar00lazkalazka00000000000000ID3 $TRCK12COMMFurukawa Toshio, Tominaga Miina, Ikemizu Michihiro, Gouri DaisukeTIT209-28-2001TCON AnimeTYER1992TALB=Patlabor CD Box Deluxe Disc 3TPE1Ito KazunoriTXXXAreplaygain_track_gain-0.61 dBTXXXAreplaygain_track_peak1.039857TXXXAreplaygain_album_gain-5.44 dBTXXXAreplaygain_album_peak1.1361020LXing,d`lɷ2uxzl'&Znee&:a@). УQǁ# -ع1D2՘F8([>ƅ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&lq|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$ҤLTQR[ 'yPFO54#iQRqfY V@=q,n837#'Xx#P 4FgъD\g6ohI;.@*+o NDZ g' 4Eʘd$,|`{YٔCʴ-Kee{&׃xN5YA_N5 x5):_{FZaV&g) <˩%lb~أ~,2da3fW [,գpF,Qz {v% j ^ !촒 N~c.|.-IE OQ*ե8ZTHHWsoO*ޕ^37seU0t˻y4G:9$ y( ^ڪ˗ TW?j5 EzgȰ4Y"\~_UL,㸄s_̀a`3ȵV6 27$КyNʛ-sUcK \#D~4Y $EYv똙7&P̆{\0oRm\)œ,RUHx?zx~hk |lN| _'ef{ox"fpzhf&<+}BÕCbz65cq"͂o$>;f O`L<І!"nS1rܼkyt7ښ9٠!,."7jj(Ntj59 6gJzqbυa\ ;7ZOJl#er[qC"WF*WLYB˚9Uq<({)?J `K{3r_zR7!t9I>e0kyk-ՠW{(‹$ԉӡLyT%B D+K#Ν3ZP7^."Ijo6e0 FJB׌]"oS)UY)xOA$܉<_(蒊Ku qꮲ 6-=!A=OlEBɶ՛gI۪\C fߍU\ӆSw؁4q[#[V}|oi.=֌3A4ivB-3\ȲZ1Goy5(t˶/@ 4~>4mV}Ӱhڭ&\Ȯw4[6|QPxRE0Z+gylk ize!o~4#?WJծ iv{ ֥˳BvgisgV%qXm)`FH%+A %<.m bi) 9#555ʢUPM݂ˊQ|YF'#fLaTh7 hGϗY HLZ}?ީ=t[P =E|Dd V':Ka*G;2M4M~""Y儝TNPZsdj<_I)jqmw@|0u6:oEFHn4<`8T!O}| n=ϹEg4x, Lk3$'mfY609.%do_L]E+l*Ӗ0L뎤}kt _ʐP #Y4A/=ϔI ?i:r;(ّDV."f={. xQ1$/bMa|DQp&)r^L4=E#YO fVpk7s%if7`Hl"BՊfH'jkDW`!Uu'"p.R h٢Pw#i5'h>Y~Q):SY&f0L =Ge&W>;?4(Ie{](p޶5Hil"G*0 šb { $-0j[ ]; "y?,{KGp 䩕S4]y'r#R֥> ÑkDv K)lڌ&hy>%@W,(k3L$݉v:¯(b>8qcoJN^i9O'T3:m<9\wsQRQ ˔^1_im?\*x@v~Z1@p[A#pm'Q+/IR= jje``si HR~^ToM'kP$M̛弜@O_f_!+n& hnQql]1lؖӮ\(gy&+ d!ۼM"SKHWǖLI U-q :2PoNgP^oj!i6!]0@fm~ N]Sz@<-U"z-B]1 ^xT@P%_w0suA}JJ&>g!=z^=6MhPG"K* Kk {^e/Mib[:bQH4H}aI-˙\O Clkؽ/L}m]ߟ#N~e]R9 20n&(;^ >*HdjW.`GFblH5P$yl!\ np76 c&r^򖹀FѪ5xao?9H [ 1HmPGW$ 'qU1H~ױIetظB,bܜvZwrN,b'% FhYoQL-7nq}QF1EfB8]dlUzG0/1f././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/empty.aac0000644000175000017500000000502100000000000017346 0ustar00lazkalazka00000000000000P?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#././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/empty.ofr0000644000175000017500000000020000000000000017402 0ustar00lazkalazka00000000000000OFR D@ HEAD,RIFF$ WAVEfmt Ddata COMP%Q_A@5TAIL././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/empty.ofs0000644000175000017500000000073000000000000017413 0ustar00lazkalazka00000000000000OFR D@ HEAD,RIFF$ WAVEfmt Ddata COMP}w @5.<TAIL././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/empty.ogg0000644000175000017500000001035000000000000017377 0ustar00lazkalazka00000000000000OggSۿ;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}[ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/empty.oggflac0000644000175000017500000014506000000000000020234 0ustar00lazkalazka00000000000000OggSaNF(3FLACfLaC" BzOggSaNF(:,( reference libFLAC 1.1.2 20050205OggSaNF(>˩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????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_mm@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ԭpGj! dܫ2 0+3gSq_]=%=,k%q>a~GVDbTf@žֿOHD Ԡ !Sʼn&]C, {njf#>bgj:6=:$}{)uչtiتF( l_\ӧ b H@1w!Պ+QQA(`on4EcVE+jxKH<;ǝi ]l1.lPېXuD-HTjv]  8 ՛!2FO@=!S a#jXҒݗޔ]HʥMghO J3$-RT^r: C\ߍ\to!SO n>ʩg%&V8/ @UTP3."S@,kXHbQڏv sEtѿx!S4 ̀*'~GFOЦk IVX!mR1[(m1zׇ]I-43B m\]pv2}BJ2:!VS4e8#{¯G/=deGcXuÔǐNX`TU=P Sjz+fmu#8tbS !P !SNKE*A{~Fԗ@~'{P]3*J X~ JQ XP}bR1LѿX!ՉOAP@UB-JBRpk %xo!. \T@Xu \u֋I+c0P p4"۹T7[ |b: ހ;iE0vh q\Hى4 !S Pe6>hi}M ՈY[LӢpX]@3Qnc;)heaoJ/v:7a! 7ԀW2ת\ ~i$G]'Zh\vg143lU!0jȚҎIZLcڹ Z7wV[!T)&5R T{Os]Js%z¬,C sg36 -jcm -r_TC(f2U8\=RTѿ!ӥSvr:\!9Xz,!ӽDE]e35 bCǿ=~q`d?,?kyDKO#MY0wzy '!~~^ I͢p38*.<_d!k.9r~ )O#rȹ,`CtV)ppy[z`Fn~]vҰtޓV(ޤ ^&̹d H0 5ћ3fp_I;MaF7pu~hnD7zO?Lvϧs J !uDO4-bQx`08 >K;P9>LlZ1;i&cck[N6聜KTi['Q.MVwCW!w~C9!~BfcʄW$ g[$.]#<96etF$$de&yLxPN0q,y ,욢x DNʚ<&KN4nMOY9iu@Cb $SS$XN+ߢBC.3`ɹ7e7&Pd#p5"X" C$Z8MrM:0MC3^w!993| N!(D(Q:H%7Ҫaԡ} Bh90ZhR4!A᷷r8]5NA<҄6XLTq<V!G#XಸOS086:+90 Z1bDpM"& Z%LxiЏQR0ʐm'I 1 cM a 4ysKQ! 6WA(ш9  !'Edt}-X&<gyDwvWσ˳@ f .9R|C92FeXd8ćۑhÅ^ɪ'{5LA 9\RcNSIcHp;'Z L&s 76"IBAH9`A}u!> A`8I0W"6*'Y OMtA,y b6~@TEV#pIJP B|OLܿ& 7dJOu'n9s&s2N=krqdHM&3d5 H724B6x60B ) ӈOB|Y(bj怘HO '̓\OJO 'NYh1U E9L "ukh )&Eذk_qyh5qi_9 *i93HFF}U@1"QFBRqKY'Z5.ie 0+P!TbxKp09If T$g'"U!$! BkjCrp NeʀDt8PS 3ab˱\)1-bi;bN D$~{$K' ^%A3[wLu hkTp+9dēA&xEc6@SOE/=jޑg7۬>JLbuX)`u@?!aqX9@- 66"{LKA$ToM!.|8?]P:nX~&tT(BIʭ.h+j;,6,^ XO=A=ʰ !TEbp'<ؾt3!9A>e2EɛCcݦGgH#_Pų`J>np\=Va;gKtmأ2hc5o?9SW:Zmۃ'DhR?-0b"V5 u)'"O*[ 1쓔,4ua#u8uYSIS9t=Kt Id ''q}[{S{X~Dbַ!.yZv>Yu!z k6߾-~ug8fXf}`VlN; gAǸ\ou!]Eapȩ8rhv_F mX_<4'"1,O !f)1wf?xȓ[vI>'gn˺јBtI59 2Gyox9Ҭ +-nP -欬>oM-v=V᝕u SH@=;od׋k@DŽzE:1n'kɾoLW4 !eB@bZ­>½!V 'I*.mI ⱶhYeI߿}Kɱ'ةNDaȱෆA$ iEGޙ^℀;/*rb vB%L~ˁ8яheaas3-eVq;Kޕw .m_,k)a#?|Gۋ$41Um~ ;?/J (*"~]h=X(:!UBzk}{q_0ad$\ Xb7|dShC(S`Q;D>.✻8ra}S嶕͐ƒ$t.RjQf(|ce.V-1P?ٕ//L6F&=oꍑFvx[ A;ku:y dC!*``+v;IbcxRm82cjԌN="r0CbgwW/]h {sO-@@>!5GkZߑcsa$.c}.tY)~w5欂(ÖKa~RU?ܿtv*US-s>$vY)eY9Rk(z}# }/>ϾPyu{gxuۤV*fTT k "1I$:{,2̚ɤ/*@WofW1 6Nt7*Zk'֑M`Υ#/JI1! DPF99x C88,Iqr"K^ASf%w !-B/>+v|X8 -&wAQH$t1 }hk H|_#mb;ᴛ- & 4X!۸y_@L}v9ɢǢNI#J>,HB2hd^|Sc{Vq ߛZJ>LqhV8nLsӇz.+?ݐݎ;uv< rz~4wJ&R_\w6#[<'f$ !{ń蜆1: I=0ąXfN)dڃ_ɀE[C) &J˪+xX *"e/DΤJDEen|~gcruy1B na1P:.?o R==foGHG!L-kqu cBt%MY 4D|ןqn)ʂGIkZLy*@ӟt0>Y] RN}4Z!1#~(yJn|spM5ZZ~[s@ ނ!-o5H"YPpəRҤǒ )<ǝIy+(so<;X{@@zyXRVBYjǏwI"EpՋ w>QN?e>a9g,`& CI7{wY& _N8kȌh;U%!~][&B,qM ŮvA)C_`DU)|H tO^u? v zQ*>/Y,M5R~vHzjVJhuˠp]QЀ֓ ɟU|mE)BE6l <7+直 mJo=YWedO r\Wt7ӧq]I@׼Nܪ9^A*bsWn4/Yi ޚ!TE!Ws rBSEgA]z۵{ZsfC "0w>ޙj?|=I)5޳i*dFoȑ)YyZ#>aCݤE^DQ{$"<&y7 L}|{f}r6'Dr(lv}:)ݷ-ZY׹` :2es/w>$W՜x3(?7~r!tuBho~ +[Ԕ⦆Mڄ>OyV @!HGyYEL_t/]臡lCtjCc#JT:=b\$fZn *v!V=kl} P?#1|<:f Q@>]Ygsל}Ӽ6CC~H7`[xc{/s ᫾Y|. U73t,}~ u@s8Q!iecLl1?\-eot-VLK,b- `T!\Э̐jJT!}a1 迄| i` %]'a%7' ^T<\~j 0IvDhSo-B&y(h KAğ%)QRmreƑ-;GW*rI!bBz:-']e@ZD&TYQ"X|rS)M "K ܆kC擀@4Y(dM(0"eP g,4EF?A#v޹L`sozs:z+= =!MEaQ&UFG暾uκk^ O$0G$K9`O=ʆbg@`g?gwg=~Va$i8D~WΪ@gp膎X7\" Uc|V=ɨsVeZ=P 5ZdvDI?Lp$GKm/ͧEQM1ǵlrv8rhEd O!,M{ jP!T}brþp囤֑_ '/a!\ (BvHpz`+;w!(4~# yYdW%Zȋ S|N;nW=G]cl.l挣#S?oZt^t4/`䗃;r gBŽ6P}[(RO NBeraϡ^& @<2ZRo2T~҄i8yhSj˰9R%֐;f}PUI t|=h+yjCUPoXD"g77JmI ґlt(1A34qkAKFF֖ 39kbԧ@'F`+ P;-ϷUq!mBGr{pv=jP} /ݔy gUm#t8'/Ϭ;Bƒ") vԮNY@^݆t;˚څ؉!Ǻn\9֤X6VBʰ07qJ_̤|oH8Y`Ÿ3\Ǚ -0:2v}̘Lb)-;vRh,ɫtXݥžQT '+@ !}B` =[/ۯ< sV?z#ݹql-JCECzd%$tuEE;HP!*#$=j"?{+,?vXed~cgRn.bKL4t[ /U>E y#,UqlptWN]F_E  %\q&ش0K~)9`84N`O^Ha1O>) L'>)`@9!]iNXC;NI,?^$FDS%ðl8EDX({lf/ytA ÷@!?x(^5R_:U`G= J: Xe(>U7pw5_e`-rܗ0y-tE2&֒?m&bc 7&!sRr) F=a=X ʔtKM^Ȗݢ:?}E=nZ ܋πP !(m!P5\[\|k/*ǡ'_G<6c 9lџښSa@WI|h"ځnyĚZ8_^ 4O)N;BQ2ߖB\2J+#-VD/#jm!//Zjڛړ1n~6`d㌽\4<|-2M1{2r\ӏcg%Iɣ `M&|JM 8S"28mzޤ̹8>D5-g oz[}_\HꦿsV]0v0^ u(8!LE_!/K#i/d˒駁"NEI (T3L/v.|AU2kB̂*EEsvؓbwH|VE}+".orGѢIع=Ⱦqܾ*),DL3ܓ@ V*?Gy_biIheKJ'6~րȼ{[-Pgb} a^> :1ya19NOGr]3,m!#ث =" YQ3L&8,E#CL@z7z!LB!/ G~Ut0~J>ݴes؃Vl"'cKg?Rw/%ތdWx^ZEH~Y舁zZmpEdp=z(Fu[:ϼPZC﬚:ݮȜ鼹+>@MImu9N!p.<_B6fXjHUwzc_x!d3=h'gT`MP271=]&0O]΀=Co"!xTFek˯~)k]lIomKlAlm;)-A77{g՜i'Lb [\븗!n^.Y4]NiCzYME\{̙<CEc78>$B`!Ř0(su0UM4WH>;>,`u J0P.Q`ŔǐXFh(PPDN !mb8>}3ǟ>4F8+IROêF)tCSpP'Q)I9 ]Lj99Upxf PDqH`N2`I4 D"'V;7 T42 ASbN;O;%C'!a'GO'B'C!@PO1%i ^̇kka޲Ny D|ZA4DB|QJN<"c19&~/RuO 'N:G]j P~X׶VR_'gW,%$ #L!W?l 3=wv_E.-#Z@!ԍa1XL[g\W't)Qr Q a)\$2^&9v^1<* !X~C!#m?U^E=t>#d~d!!vϐǙ0$-nԄ`m4>Cf2L'r7ױ vt{>н:Am_1Ѥiu>uˬs9[k|P{ z\8};&>p#>5$bP7z!TalG;޲鯳IAX!#exM&5JFF8oH0wӋX-d濠dIؒ]3ɐF=Of%RpLP+82CӹҍZ"Hˌ5y䣃ݬ*csVK=JFz%n@cM+ɀ F$2mb+V[:/+'*pYkpPF0!T]b؏scqf25j8*Co=eGhTjK'sR)R7f٣Ali$2m7 l_yhdwC=[;fOa6kv[2rV3ovY}R%>-\rH\gmJB[e6yஓO 0ԊCurİ_7cP !Tmbgӗ +侥g$*<ʦ!~:dK®ԥF{,\/Jt]8%)TI\ho J_qgvMqy檌k(2!sLzC个C;gk|,Y"6QsEX@|X,4@ Yư,or朑j$Cp!TCb[Ƽl~qodȤ ^9듀L ;IqF9߼D "Y3 g 9@Xu'8EN͜ouwDF0L3ܘ>voiXzn;[c :(j0 w:KYrbioBAF1>!kG|_gIX|wVfGXHP7{!B<6®k+ibP%PŮϱ cmP.˩L`"0JEk"ҧnK[ڥKԸ>CӾw0u`Ց< ] ,Ր`Q-qJٿT^񝃲su[$ 2"3'GU)xIUpJkt9yy=at`c1~C{=/cI8 Tne`G-}7i=:\,!BSĜu̾';(EssR';%rk $vIt*'ߗr5VtTIFo>KQQ~s,N! ]a'XåBw _[ L?M'A7uq-$s[썅ȼ6I৬A]~/v塐x]"g lApGo;$*2ym?{ݽcUg6ͩAc+ !<>\f{qX0DidȪ%U`( YDcʡ=Kʹ;v@,QXSeq0X9%2cIy:!-/YEK^s{a?9~/ 5 |c/gVB!DmE"!}wC9^ydT+U("R???|3 P7u!ԝB~/CB5&Ȧ N"I"0q]C 8bh"dN W~}RW-<K˄2¢=m%=IKJlܳeAͿiWdJmHN.MOi$ BH)"h@J"QdM8fB1X W@ޖ!"ķߪk;McP 8XԱC;D˚ /w ,9."Ys{^3?Rָ(gl\fԢ xA@Rd9 J~7~uyհ>G,=D^->#pH ӉS}̮@(Ժcf`V~m+& z77Iŕ9B|Bʘw!ԍWMבJ:Fb,ExXU pRh(ҝUpqG$Ղ!)0˰pY1AZb(r9ڏḎcl5C*5'L8th__4bqmL!}~7N\:wAk|)F&ѿe4H^H'~sտ).my1bb,>g\ ;awZ7Kǰek&Pq;o!yál ^Ugص/ԥ ǀQ3A[ a! M&pmD6!o[ា-q\ExoY!TuO |\WHw J}uIcn] WX&6PLFLsO99'PϞ֭-SMr[Σ#s׊o=l7 OlT;J*[l=ڛgt@-  ғ&>( {uMTA:;?PVs|DzSes':7q!ԅBX/#{<5Õѓ1'罋$/7|/4n-%L=do{\ώ*yK7CA"==ο= t~ w RB([SPl !TӸ"!&L]KBGHΠߧ&,z$aW㹫aXiX6e NAп}D{GG[m@@ߟA*7t!MHS?o5ۯ.{8;y kuqT5I1۰`|?]ݟI(r~[c' ZT7$sjh0}HA*+ʖ"CwCsd_~ (?,{iz5IY Ƞpi>eU-Bޘ0 QU7aMY-l.f১/Hv:9ܯ۶"h!, uv _˦x+!dUIWoD!-B$N:%V6_ #LJR̰mRR *|5al"uYE" DZxz5C#ɂḍD S)?QsRER°{mfS/e|j}kIĬIr6\Bd dРEU|cz!Dfy:^AjH ˪2"Q:eZA$@rnO5vD啇VݓyܽW FZ+oDo~Di+y_#/7w]z׫`D/ԿH!$| d^ɪp~_ Ĺ}n$Ctk{|<*ai/;6qSJ_ٽyTdB!8 (g٧Y*(PEPt`!չaLDu CjU1Kx0 '_S+ĐxG뛇9+4 ii*uXo֝޵.T_mhz<4 !CFJd]&Y?TԃNuG9Gy{O7 na]v(TPXU-E->9tK))P #i5iivU3!]`T#VִYfr ߲!!, 6 V#Q&q $k#|Ej4U㸽`1d1sutB :PsHx[vX.E T'I>CZxo!0ˬ,,P?[1}B u0Sb>@v`kyψN ?QODјa'%\mv2Vm/{d;n#CL.uK󈟚U  q&# gtῌ!$TPwWNI8}ʤjqzp|.iMMk:0ٌғfQˉp/6L 0ZmR1KEMY*!0B^*|%_.1Oso|v? @Z4N7F^jϊQ#4uٛaV y /nʹ!őID-@nDdL(Ayz6]"O O )oWB:Zlu!t$-; @ AYnm=bM#ü <wޅxo!Sőn7]X {! ~vZCʡDӌB}DUP;5ZKp[8f3ٳӭ  EU "^VG>ѿ!S݅UT*hĿ3d\ݕB\'=ϝQ'Ta݄p-1]ײWHS$ѿ!hLY @>!7!7~moovlmvhd@trak\tkhd@$edtselsttref chapmdia mdhdD\-hdlrsounSoundHandlerminfsmhd$dinfdref url ]stblgstsdWmp4aD3esds"@sttsWstscWpstszWnlmlmllmmllmmlmlmllnllw`HmmlmllmlmmlmmkmmmkmmlmlmlmlmmlmlmmkmlmmmlmllmmkmmmlmlmlmlmlmlstcoCsgpdrollsbgprollWtrak\tkhd@$edtselstmdia mdhd0hdlrtextSubtitleHandler'minfLgmhdgmin@,text@$dinfdref url stblKstsd;text ftabsttsstscstszstco,udtameta!hdlrmdirapplilstfree#chpl Chapter 1././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/data/ep9.m4b0000664000175000017500000004236300000000000016657 0ustar00lazkalazka00000000000000ftypisomisomiso2mp41freeo2&Y¼jAMEXS+PS8k9_(#} "ߑ.KaƈZ1G}u(ZVfmTUSFx€>Qd!ʼną5;Y^Zy|hb0Pf +ۅq];7}"!8Xkung9A5Y1tx (&@-@(N! `q`P7f͂Q!.'6KAq&EZM |1ޫ sEyY v rWw@_@?q`B48uy FazN L;lyF>Qp!TRl⵲RI!"u zD8.XM),:<G38 Şg7 `tu)^~@9q.Fa@J{oxB>Q!!JB%E3Ec蘾xW K Ć^Ф4b1|ةCl* VP @I;GQ]uC^L7zcyk~Lho5!!`*Γ,f 2|^ ⮸vA}vWIO(?kC\35ҌNGq&lc_Z.UJ;_t;Jr{۟T ,RND$ٿ-@(8! 6' ژ Ba-j~{!x֘5 sZ=jf<VOxQ·F`36}_?\Ӥ @ fLo%!GC ڴ@ YL3 ߥRK9̊=A6$84;o[#JQbb ~W5%rVGD7![.9q cޡuR"e"RE@(T!a S-HoAP6mw͠C]EP O KnP"t7"#.}&ֻs߶qv1(n7K$we]筠D8Us4ы!͉#qVerNuLm~Q(2LF6|GYak!CjDW+tȁ:Apo5ق!KLQ!ʼn9ZT+, +Tj.>Ǝ~` DFo_ d1R:c7e*`zdVX0>?S#mn:s Ir{ T(@:!0'HXvEu/DqƂMDO[03)MY{\ ʭ/Aw!PGSP[d%qG=V-dhLP>Q!ŕ, ` YZ[G`-'ZQ6I ɽ&%#c(930eRlضJspl{j9h (S3 X I/N}>Ԛ|y8!`HXU(r.K,8㷏|:zڅēUXUBqVPـ4OVhgPKpnIQ 2ƻv!ղ mYu^%9J&ՄxdB]L'A9ԉ̖NtbnI2.G?ɱK"WW÷-yYN*ZYlϳ}'AUs0Q>WA tC2-Lu6mBO_RƪHYO_=w(@9!BꘀD+6$d¥G%GCH(HM&,qKx:ۅ5#W*juOx qG};aNA ya$wь'Ȭf3 YX6iD6Z7^&7/u1X+=5tV`SsZ!P@(!]Ώa(@bvxT%9 :dee&M~L b7! 0vjaIa9mDj_@ 5dFBMήJG1ڲ)z|o6;}QQE2z,nkBĞpRƿ:kb\4.; љF\iWX!O&2oZyΔ=(Ò!AQAvRW ULPHQ9&5u^-HgIuE&0psbzMַW n%u[740;c94Oy!Q&YaV}FD,!ZNYC (NjMTIsʱG^k o '[j6~cW9@ }͒MvG~`FtxbӚ7{oHլC 3zEʚ5I( Mz)ȹIz)'K]chɬ6GAOU kQwܵz_ Y69|[H㵗JYO?%ձ+j/}p-q@!THT fD@fY:)9.~fì59jaݱ+!s@ZXU8]0-ǘs'4> m6a-3twhb{ߩǗ͗Z9`^U{"ޗʂ"IMӷ<-+?dy='Wd^/Av !TDT"h쏵xQ>t ܜyTY cH6/$aBni~k~. QQbat'=?+Ycxa%vFtpav7H'w3){Λ$_[z^UGz ƀ SCwajut}!TҊ@L(#,I X軪J=b&Խb(VJe*j,5{ ZzEsX Nf8pڞnC^BWaeQWWm|kگ4f4Mws-V[Љr^jLxWN:8!ݺT ă ( ^R)8-wb7m.*m}oMu69+أ U 7iMG:ef^8_2*g}i7W<2oQPn[4~5ZXXF*_tp%{Q:!ݖ `N3ͯ(@fc!kl⺍ǠND1oW3:sdD !) 4xwiQ74$c .d-cck9b]c m ]WMݶn7'q~l2{ut7WN4eX 9! 6%@@%?"lЏB( K nt7'yP {dɀCQS~jpMBc74Y eWuFfyë缏<(ل$Gˮ@d< ػ WFi #˟Ge@!Vdpi-}<@%C*7V@0+LUADB%=-Bom>'̤+<]iqp_Gz|7A \6w솣˫ yTϕގn՛79߂/֞)? 飷.jA)-yն| tz/sx!9E@%Y %/KbA1H=V%ҫPI%eVݽe`BZ* Qþ@ ^*<,~OF~UN\HZdggqSamw7FzB9PT5woLX]ƥ@2E |ӭwM0!:å@*B?ebX B\_#ƶȢYQ`19A+W(;hF[){l;>a&Iy^1znFn:#}|ƷLLzβiq=ZogGȐ VJ0Ag)1/Cf՘ͽCma7=ouaeYPx!KHY@:x6^8iW: "_*gqnű?~jԄ'WY"Lgn9bzmSD MӍ_%Z]c)5&~nJ@܅L#J4Dg* 9=k21L?Z)hԹs<!wT<_+*+j~W&p"+쳘6UX=}C@w EvlsHk۴ۍ2F&霘}B zbSs}sLv6#Oa&٧bشFR=.}nghvTURFh-ЃQ e (!TBPC8(kA+Xtb(x{:䱚0)KMik.RubXy Nxcz OW%C6G.cw{14֠ᾓ\i1H[<ŦEݟO _?ǂ&Ys*uts!T=PKDbsauMwm?XBK=yU콞j,T,2,` nl^PaȤNxZ7qd'2S7u3~"oJGaM&.Zd%u8ig' 49Z:$krZ:SOLv|T!ͺH0 .͆H>JoG"li@YVt2[t0:-y0:3Ǯ{r%X5_z8V9)OVQĽ[5"ر%[(hԌ8OeV2{yQWrH>Ͼqňfm[ã% C ,3p!KP2e#,XJyz`}e<p5$)QRY e2Sx҉g)5oꛭBK*m3M'g'Rcs[»)SU.zzEzbe=N GEN@!՚JP[b)$!xܝ T ML u7\vڌ˾(~6 dұDzlaCK3m.]}dUU [m`)t;gEt~cLRrlYBͅh]ϾK , a4rSa\?/!)0@E(E'? {=qKҧm]KIr}_Z9IVfh{fM۬}%Yd8- )u :Ev 1B"F/cmѡ`jC=P"d% NjSqQ^&N |!KmF_ Df- vbq/Ub]P߹pv;< 3H:dPu0 Ȅ_͓D+Tf"UfSDu|DC\x2 R5ެ@=k-9`wїZS H@i_@d*ANSfUk6-m4Ge%J-5e0ܤ"2!p(<!y F( "*Q)KO`6Ldļ21\ޙP|zN/' E[=hn!dcg>ZEy?W/oyYF!&)X'z&Qa *Gܟ[տsULںG-|^Wq>Q!T]8zkP D \;bT2HOu'Ehg@D"x)X"];oNwFMi6gd|E9g/h_{~pQG}_1Jdg3rS"i s a@!TCa0L 1 h,D@=lBKZI3]P*Ge)ؙ,+h۱}8^!R/;7a<0(!TAPlGK0.:\B1HAI]fM׳ϙU8M-Vm4w%WFkc5ng*8g0#׊á{;.G,:5v9F ?=Aճ= 'Tq?{i ?"D( }Z,uFPtw!Tպ$WXL&!ͳ,J!;uשU=%9kJ!'tKމ[q6rtTC9Cޱ:oyyc)t* 9{=?s}^ :_`!Tݶb`F2iX-b3rϟwlq]Z#&GEOӫeY'nͷcFwny<%d}׵;W-6Ns4چn$ka*(Q׎v`]>ß20sPm?OA ]&ɜPG!TΈ @N!}RZNu%?w(~+4iиUejpZ*lI]aE<<&Ȝ\~=uRuP~dc`h2<^ rtq<]NGmk7 o6_]t@!岗0JҫP*P!D Mz)2SLif(@9F&S0qdTqR*̰"ۛ9ʾck)&I'.6-a^I GV ]OL|!!h^(kv>̧9sԙJ[Z77+|de<(!@ w0j+,gK9*>}}nqb.?oQ  P#d V3\fwlp6Qhdܾ{67M%QnF#%PW^jJ͒ւkY'. щS2$H}UOaJ8\}d4wY@!TͲqW[Z"8*Z;0|ҷ.=.so]`\ЗN-H4OKlvi$/L& ¡Hdd<~K R+ku=r6202^vt$ IH4+)xUD%!phlM@ʙL ieW\O'fEtu! 0%5 RSٻBSSg_/*7i5^eӬ?-Cvۡd<1#^תF_$ShMKm:$BɖŪM;^Oyn,+Dz=4GDeѩ.ZƸlڢ!O+kmTH@!"a,J঱.n(۬p$nC<ǙK>ED}Í;90סq?nGWtJp)\ǧnCC1P|rP"՗qP.xbTn7;NڠF׫a~P96Q7!a@BAzw[ _c<|t䓈͊@f!.&AP|)!n- yWx{6>~`;:8v7R5|MaY 6)K n_J44y|>'m߁>9rP"2qnsieE<ײix+7͗"b^) uK*)&AD_ (X@(!E`BypRMVD_YHZKq+ YTR\>Cg+>Ex b|R%$@@-2k_l8Y߀2#t&^,uF_Vd9cIsqD.n'%'@)mC[:¸r6àN€>1!E tLWΫ?JΗKK葐 a*OU G+ae8hڛ`Bd7kS,O3`m`ZkLǹTs7]9M\6XQZ*}S[⟐팎pw,_FQb(Ø!ͱG@ Q,5*A8Cj_&ݗC{j+ܽ Wͪ@6mP ޱgSRYՎH {;4 =O<[^mӪV0 o" @(9jbjS*7x{>#vcm'[Lg1ҭg瞕2,8Tsv͐uXڞV|[i?0>Q!ͭFQ@ 1%ݺ*-Yɠzg9{^,ol 4I5/q>[(Z^~J'x6i~:Ov,@M` @z!X`@FUҤUR.m`O"R)*Rjr\漡!caЂWo/ pM9JjIR/(Gl9TgZԩfG&#2 +D9s `N^-xۤ]rFs[ P2UByYt!OGQ@\|>⭓L?[I> V95Uv% Bw><&C+6stIq Y&u(!EQМ A FW@& 1a]%&&hU\E#e%{~6T@ KR}$rL=wʶw^X-ɤQ0^sWϦM𱸩2F\"Ѻ%Yș֡K/A~k b0|!ȡJ\ T&+T + SkM;M|:XzidRp\{'IsG{Dv(V-E^.χZ<6T5>idўn%voMG>I~}VO5iٓNO\i.9T\y֒`BRy.@uikgX>Q!9DQЌ 1Tq-q%[MbU=\hj'] صOXCR…Y!c158 1wy<$`sZl kYLl.'Βjx?xCL]eWgQEyiƶXH}\a4KqP:!咔ȡN ejbC|U.7ˠn5ߣ^K~̿{+x"$h+I)Qkf8y^@6~֗y%*tH*[ZPYL"Aܬ[ Gp2-iR_vV34_}H@y!'nTfRW"+$x@FnYKQf`bZ6lkq6agw6B* ؉^a ǽBf4CmS[LczbʂD1 A[ i./T8h@b'J΂>Q! U23Ju3adA|?lL {}:1^Òb68{64z RMCAtx樭m A|DxpN_3h(O)MmX6z}W IBYg!IRy*֕nCٛ΁6Ҷ Pʸ=y1~T flGU j7;Kѡ~A5E//(!`@!ɮl,*T(% ?bVAarIM8lpJ,.hw|[4]=E+'x?b},0iWL dr(!ŕK+z1j2j&ё8Av/ULsbaN|; /:n4{I!vNKPM"Vyٟס7#В\~(}k\dVP>!$C P ̀yd =-$Ǒ! m %*a->=+!lp( " rBS_ţY|>J[Xhg!͑2'TPP##J 5"IJ[~t/q$I*R+VAqP,ӹʦJ5Tk9q~  ]VY{F`!$K2*e[X˚pڊPp`zY.=rs Gmg” aT8R g4 ^-t!FRX&k)D%WYM%ADZ ǘ_>Q`!EPF ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ^!EPF ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ^!}`>T||?!KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK!.&0$DDz#6 H NZaSBA[)u9ʩo;yPn :[;\Qn>{~=RDz!! F!@|d嗗RL-d,s-6 f)/L$QCSvGw-}emoovlmvhd@trak\tkhd@$edtselsttref chapmdia mdhdD\-hdlrsounSoundHandlerminfsmhd$dinfdref url ]stblgstsdWmp4aD3esds"@sttsWstscWpstszW~stcoCsgpdrollsbgprollWtrak\tkhd@$edtselstmdia mdhd0hdlrtextSubtitleHandler'minfLgmhdgmin@,text@$dinfdref url stblKstsd;text ftabsttsstscstszstco,udtameta!hdlrmdirapplilstfree#chpl Chapter 1././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/example.opus0000644000175000017500000017602000000000000020115 0ustar00lazkalazka00000000000000OggS@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#$./_././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/flac_application.flac0000644000175000017500000003131000000000000021661 0ustar00lazkalazka00000000000000fLaC""6 B"E⾘vGPx- Y  ppC 0D7!Wu(Pk/-5N<C@IJϳPWp{^0 ;d 6'k1 r`D+RyVuiW~/B@̋Gf״p0'Mc 3 reference libFLAC 1.2.1 20070917replaygain_track_peak=0.9976>musicbrainz_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"hD././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/has-tags.m4a0000644000175000017500000001176400000000000017667 0ustar00lazkalazka00000000000000ftypmp42mp42isommdatlibfaac 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" ?jfree././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574016271.0 mutagen-1.45.1/tests/data/has-tags.tak0000644000175000017500000000422000000000000017752 0ustar00lazkalazka00000000000000tBaK @M  IJ몆"%QP5pvL5,RIFF49WAVEfmt Ddata9C@M W {nFpOP 0E >;=~5\7q\/eg p4SWN{FAc SMz w'^knHŸ$11TB1BA6_.L ];y7{d{翕݃ly%:&3ן??};?6YK}_({R'@-gASC(H+Sx-ȚI3d3rw&Ľ:pn9}u4:x ;?^}_P.kxUwW=ΏSn7}=C:f:!A -- 'KF1˃Ơtn }>1a @yp<P :b߉*xOLO*~G_y|:)'tý2a|]N"]nSs#Cq[}x_z L\h4 jRL TS4=|O> tJho"52//_s8-c<*x8zk|`?+ܝ|R}*m޿_]wK%xJ^a_s]lN1ENj)nt~zI1˓/'Z>[?AMw:}Eg)swWSI[/ٮ>UL$=)|mHqMy_^E*0?uMtUt+$F$,V/X4'w/ tv-z:k.5[ß|rW.as9SWӭ%dA/V=jJ0P@ %},ZJ/@7chYjOaz !2AlPd ^4Ӭg{qu}_*c4APETAGEX9 ArtistThe ArtistAPETAGEX9././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/id3v1v2-combined.mp30000644000175000017500000001220000000000000021134 0ustar00lazkalazka00000000000000ID3'TIT2cosmic americanTPE1Anais MitchellTRCK3/11TYER2004TENC iTunes v4.6COMMengiTunes_CDDB_TrackNumber3COMM-engWaterbug Records, www.anaismitchell.comCOMMhengiTunNORM 0000044E 00000061 00009B67 000044C3 00022478 00022182 00007FCC 00007E5C 0002245E 0002214ECOMMiengiTunes_CDDB_19D09130B+174405+11+150+14097+27391+43983+65786+84877+99399+113226+132452+146426+163829@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`ڀ@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`ڀ}G˿50}KR"J\ĻC圂g././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/image.jpg0000644000175000017500000000134700000000000017335 0ustar00lazkalazka00000000000000JFIF``C    $.' ",#(7),01444'9=82<.342C  2!!22222222222222222222222222222222222222222222222222" }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr $4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ?+k3jFgh%$ݫq&n6j3Gq!Ͻq7>)5we;Yʇ.N;`NsZMtkQ=3MKn턀? lێ@././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/issue_21.id30000644000175000017500000010000000000000000017566 0ustar00lazkalazka00000000000000ID3@vTIT2 Punk To FunkTALBBetter Living Through ChemistrTRCK9PRIVPeakValuePRIVAverageLevelJTDRC1996TPE1 FatBoy SlimTCONBigbeatDR`wL A_Ǻ6 7P0Bmc8Z8t6p.p@n'0Ղ N v(\eK p&(1%@Ħ.q(re \ef<|f0'0s |s!Pr:w9?X6!oen= @l   . 5 R`AAJc&  q@PJbbP'Pb"b!XȘD3L\DlsȘa89O`9 @S S s^T9w.ܻrbL$ Q$$( $a( D:"&BP 6G&$$IIZ\ddrZիjoAE7 P+)AAAAAAAAAAAAAAAA0NJ & ff%/ bܰʙS*eΜP  Pl XXp6**@`l $TTTTUUaacUUU~UU_fkk[ᖿVWUYfU_UffffeUUUUffffeUUUUffffeUUUUffffeUUURYaJ owgw;[_7d&52&\ Vp_i)e&UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU owgw;[_7_i)e&Jd52&\ Vp owgw;[_7_i)e&UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUd52&\ VpUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU owgw;[_7_i)e&Jd52&\ Vp owgw;[_7_i)e&UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUd52&\ VpUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU owgw;[_7_i)e&Jd52&\ Vp owgw;[_7_i)e&UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUd52&\ VpUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU owgw;[_7_i)e&Jd52&\ Vp owgw;[_7_i)e&UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUd52&\ VpUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU owgw;[_7_i)e&Jd52&\ Vp owgw;[_7_i)e&UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUd52&\ VpUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU owgw;[_7_i)e&Jd52&\ Vp owgw;[_7_i)e&UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUd52&\ VpUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU owgw;[_7_i)e&Jd52&\ Vp owgw;[_7_i)e&UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUd52&\ VpUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU owgw;[_7_i)e&Jd52&\ Vp owgw;[_7_i)e&UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUd52&\ VpUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU owgw;[_7_i)e&Jd52&\ Vp owgw;[_7_i)e&UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUd52&\ VpUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU owgw;[_7_i)e&Jd52&\ Vp owgw;[_7_i)e&UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUd52&\ VpUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU owgw;[_7_i)e&Jd52&\ Vp owgw;[_7_i)e&UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUd52&\ VpUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU owgw;[_7_i)e&Jd52&\ Vp owgw;[_7_i)e&UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUd52&\ VpUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU owgw;[_7_i)e&Jd52&\ Vp owgw;[_7_i)e&UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUd52&\ VpUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU L%` ,^5 L0 @  `L`"`4`,X ߡh`&Z^d!k 0 0`@\ QpDMAXԐI5L,, ۡeMU@1p``\p@h R<0IIz]~y]uw/J{45s~Xa}S&ݷ-._II}IDb1bVC>0E"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غ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- "Y././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/lame-peak.mp30000644000175000017500000000405000000000000020020 0ustar00lazkalazka00000000000000dInfo(@@@@@@@@@@@@@@@@@@@@@@@@9LAME3.99r,D$#N(gdRdO"ʍi )?=42:ZX0)쓚D8#Px N3&\biz^g)JR<~߿~ǏX> >>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*.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   4UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/lame397v9short.mp30000644000175000017500000000105000000000000020701 0ustar00lazkalazka00000000000000dXing("""""""""""""""""""""""""""""""""LAME3.97 d,$M(c|$ddX Y@4 ` G(DPx~xhex٣V'T@<A  J0`bJ g6h3(8@Y1*3DaLt0OBN#;]O<OX澻_gWM:5~Mod_ TD `Œ>1@60/Ǝ)~T(Ț(88cp:Lq14sp'=<T帨3₆‚ |> :-LAME3.97././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/mac-390-hdr.ape0000644000175000017500000000020000000000000020047 0ustar00lazkalazka00000000000000MAC <D, nd' RIFF*WAVEfmt Ddata*|L~c-d= YkRJnw././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/mac-396.ape0000644000175000017500000000015000000000000017306 0ustar00lazkalazka00000000000000MAC xD,zRIFF$ WAVEfmt Ddata X././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/mac-399.ape0000644000175000017500000000025400000000000017316 0ustar00lazkalazka00000000000000MAC 4 ,(fu3 qi :DRIFF$ WAVEfmt Ddata bb././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/multipage-setup.ogg0000644000175000017500000022626700000000000021406 0ustar00lazkalazka00000000000000OggSokt7&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

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%ID3  ID3TIT2 AIFF title././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/with-id3.dsf0000644000175000017500000000217300000000000017675 0ustar00lazkalazka00000000000000DSD {\fmt 4+data ID3TIT2 DSF title././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/without-id3.dsf0000644000175000017500000001013400000000000020421 0ustar00lazkalazka00000000000000DSD \fmt 4+data ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/xing.mp30000644000175000017500000002002000000000000017124 0ustar00lazkalazka00000000000000du=@"`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@././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1596184724.827105 mutagen-1.45.1/tests/quality/0000775000175000017500000000000000000000000016325 5ustar00lazkalazka00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594553628.0 mutagen-1.45.1/tests/quality/__init__.py0000664000175000017500000000046400000000000020442 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594553628.0 mutagen-1.45.1/tests/quality/test_flake8.py0000664000175000017500000000167400000000000021120 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2020 Christoph Reiter # # 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. import os import mutagen import pytest try: from flake8.api import legacy as flake8 except ImportError: flake8 = None from .. import TestCase, capture_output @pytest.mark.quality class TFlake8(TestCase): def test_all(self): assert flake8 is not None, "flake8 is missing" style_guide = flake8.get_style_guide() root = os.path.dirname(mutagen.__path__[0]) root = os.path.relpath(root, os.getcwd()) with capture_output() as (o, e): style_guide.check_files([root]) errors = o.getvalue().splitlines() if errors: raise Exception("\n" + "\n".join(errors)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594504659.0 mutagen-1.45.1/tests/test___init__.py0000664000175000017500000005405000000000000020011 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os import sys from tempfile import mkstemp import shutil import warnings from io import BytesIO from hypothesis.strategies import composite, integers, one_of from hypothesis import given from tests import TestCase, DATA_DIR, get_temp_copy from mutagen import File, Metadata, FileType, MutagenError, PaddingInfo from mutagen._util import loadfile, get_size 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.id3 import ID3FileType 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 mutagen.ac3 import AC3 from mutagen.smf import SMF from mutagen.tak import TAK from mutagen.dsdiff import DSDIFF from mutagen.dsf import DSF from mutagen.wave import WAVE from os import devnull class TMetadata(TestCase): class FakeMeta(Metadata): def __init__(self): pass def test_virtual_constructor(self): self.failUnlessRaises(NotImplementedError, Metadata, BytesIO()) def test_load(self): m = Metadata() self.failUnlessRaises(NotImplementedError, m.load, BytesIO()) def test_virtual_save(self): self.failUnlessRaises( NotImplementedError, self.FakeMeta().save, BytesIO()) self.failUnlessRaises( NotImplementedError, self.FakeMeta().save, BytesIO()) def test_virtual_delete(self): self.failUnlessRaises( NotImplementedError, self.FakeMeta().delete, BytesIO()) self.failUnlessRaises( NotImplementedError, self.FakeMeta().delete, BytesIO()) 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 MyFileType(FileType): @loadfile() def load(self, filething, arg=1): self.filename = filething.filename self.fileobj = filething.fileobj self.arg = arg class TFileTypeLoad(TestCase): filename = os.path.join(DATA_DIR, "empty.ogg") def test_old_argument_handling(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") f = MyFileType() self.assertFalse(hasattr(f, "a")) f = MyFileType(self.filename) self.assertEquals(f.arg, 1) f = MyFileType(self.filename, 42) self.assertEquals(f.arg, 42) self.assertEquals(f.filename, self.filename) f = MyFileType(self.filename, arg=42) self.assertEquals(f.arg, 42) f = MyFileType(filename=self.filename, arg=42) self.assertEquals(f.arg, 42) self.assertRaises(TypeError, MyFileType, self.filename, nope=42) self.assertRaises(TypeError, MyFileType, nope=42) self.assertRaises(TypeError, MyFileType, self.filename, 42, 24) def test_both_args(self): # fileobj wins, but filename is saved x = BytesIO() f = MyFileType(filename="foo", fileobj=x) self.assertTrue(f.fileobj is x) self.assertEquals(f.filename, "foo") def test_fileobj(self): x = BytesIO() f = MyFileType(fileobj=x) self.assertTrue(f.fileobj is x) self.assertTrue(f.filename is None) def test_magic(self): x = BytesIO() f = MyFileType(x) self.assertTrue(f.fileobj is x) self.assertTrue(f.filename is None) def test_filething(self): # while load() has that arg, we don't allow it as kwarg, either # pass per arg, or be explicit about the type. x = BytesIO() self.assertRaises(TypeError, MyFileType, filething=x) def test_filename_explicit(self): x = BytesIO() self.assertRaises(ValueError, MyFileType, filename=x) class TFileType(TestCase): def setUp(self): self.vorbis = File(os.path.join(DATA_DIR, "empty.ogg")) filename = get_temp_copy(os.path.join(DATA_DIR, "xing.mp3")) 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): with warnings.catch_warnings(): warnings.simplefilter("ignore") 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.assertTrue(self.mp3_notags.filename) self.mp3_notags.save() self.assertTrue(self.mp3_notags.tags is None) class _TestFileObj(object): """A file-like object which fails in various ways""" def __init__(self, fileobj, stop_after=-1, fail_after=-1): """ Args: stop_after (int): size of data to return on read in total fail_after (int): after this number of operations every method will raise IOError """ self._fileobj = fileobj self._stop_after = stop_after self._fail_after = fail_after self.dataread = 0 self.operations = 0 fileobj.seek(0, 0) def _check_fail(self): self.operations += 1 if self._fail_after != -1: if self.operations > self._fail_after: raise IOError("fail") def tell(self): self._check_fail() return self._fileobj.tell() def write(self, data): try: self._check_fail() except IOError: # we use write(b"") to check if the fileobj is writable if len(data): raise self._fileobj.write(data) def truncate(self, *args, **kwargs): self._check_fail() self._fileobj.truncate(*args, **kwargs) def flush(self): self._fileobj.flush() def read(self, size=-1): try: self._check_fail() except IOError: # we use read(0) to test for the file object type, so don't error # out in that case if size != 0: raise data = self._fileobj.read(size) self.dataread += len(data) if self._stop_after != -1 and self.dataread > self._stop_after: data = data[:self._stop_after - self.dataread] return data def seek(self, offset, whence=0): self._check_fail() # make sure we don't go negative if whence == 0: final_position = offset elif whence == 1: final_position = self._fileobj.tell() + offset elif whence == 2: final_position = get_size(self._fileobj) + offset assert final_position >= 0, final_position return self._fileobj.seek(offset, whence) def generate_test_file_objects(fileobj, func): """Given a file object yields the same file object which fails differently each time """ t = _TestFileObj(fileobj) # first figure out how much a successful attempt reads and how many # file object operations it executes. func(t) @composite def strategy(draw): stop_strat = integers( min_value=0, max_value=t.dataread).map( lambda i: _TestFileObj(fileobj, stop_after=i)) fail_strat = integers( min_value=0, max_value=t.operations).map( lambda i: _TestFileObj(fileobj, fail_after=i)) x = draw(one_of(stop_strat, fail_strat)) return x return strategy() class TAbstractFileType(object): PATH = None KIND = None def setUp(self): self.filename = get_temp_copy(self.PATH) self.audio = self.KIND(self.filename) def tearDown(self): try: os.remove(self.filename) except OSError: pass def test_fileobj_load(self): with open(self.filename, "rb") as h: self.KIND(h) def test_fileobj_save(self): with open(self.filename, "rb+") as h: f = self.KIND(h) h.seek(0) f.save(h) h.seek(0) f.delete(h) def test_module_delete_fileobj(self): mod = sys.modules[self.KIND.__module__] if hasattr(mod, "delete"): with open(self.filename, "rb+") as h: mod.delete(fileobj=h) def test_stringio(self): with open(self.filename, "rb") as h: fileobj = BytesIO(h.read()) self.KIND(fileobj) # make sure it's not closed fileobj.read(0) def test_testfileobj(self): with open(self.filename, "rb") as h: self.KIND(_TestFileObj(h)) def test_test_fileobj_load(self): with open(self.filename, "rb") as h: @given(generate_test_file_objects(h, self.KIND)) def run(t): try: self.KIND(t) except MutagenError: pass run() def test_test_fileobj_save(self): with open(self.filename, "rb+") as h: o = self.KIND(_TestFileObj(h)) @given(generate_test_file_objects(h, lambda t: o.save(fileobj=t))) def run(t): try: o.save(fileobj=t) except MutagenError: pass run() def test_test_fileobj_delete(self): with open(self.filename, "rb+") as h: o = self.KIND(_TestFileObj(h)) @given(generate_test_file_objects( h, lambda t: o.delete(fileobj=t))) def run(t): try: o.delete(fileobj=t) except MutagenError: pass run() 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_not_file(self): self.failUnlessRaises(MutagenError, self.KIND, "/dev/doesnotexist") def test_pprint(self): res = self.audio.pprint() self.assertTrue(res) self.assertTrue(isinstance(res, str)) 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, str)) def test_mime(self): self.assertTrue(self.audio.mime) self.assertTrue(isinstance(self.audio.mime, list)) def test_load(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") x = self.KIND() x.load(self.filename) x.save() def test_delete(self): self.audio.delete(self.filename) self.audio.delete() def test_delete_nonexisting(self): # if there are none, add them first if not self.audio.tags: try: self.audio.add_tags() except MutagenError: pass else: self.audio.save() os.remove(self.filename) try: self.audio.delete() except MutagenError: pass def test_save_nonexisting(self): os.remove(self.filename) tags = self.audio.tags # Metadata creates a new file if not isinstance(tags, Metadata): try: self.audio.save() except MutagenError: pass 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"), ], AC3: [ os.path.join(DATA_DIR, "silence-44-s.ac3"), os.path.join(DATA_DIR, "silence-44-s.eac3"), ], 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"), os.path.join(DATA_DIR, "ep7.m4b"), os.path.join(DATA_DIR, "ep9.m4b"), ], SMF: [ os.path.join(DATA_DIR, "sample.mid"), ], TAK: [ os.path.join(DATA_DIR, "silence-44-s.tak"), os.path.join(DATA_DIR, "has-tags.tak"), ], DSDIFF: [ os.path.join(DATA_DIR, '2822400-1ch-0s-silence.dff'), os.path.join(DATA_DIR, '5644800-2ch-s01-silence.dff'), os.path.join(DATA_DIR, '5644800-2ch-s01-silence-dst.dff'), ], DSF: [ os.path.join(DATA_DIR, '2822400-1ch-0s-silence.dsf'), os.path.join(DATA_DIR, '5644800-2ch-s01-silence.dsf'), os.path.join(DATA_DIR, 'with-id3.dsf'), os.path.join(DATA_DIR, 'without-id3.dsf'), ], WAVE: [ os.path.join(DATA_DIR, 'silence-2s-PCM-16000-08-ID3v23.wav'), os.path.join(DATA_DIR, 'silence-2s-PCM-16000-08-notags.wav'), os.path.join(DATA_DIR, 'silence-2s-PCM-44100-16-ID3v23.wav'), ] } _FILETYPES[ID3FileType] = _FILETYPES[MP3] _FILETYPES[APEv2File] = _FILETYPES[MonkeysAudio] def create_filetype_tests(): 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}) tests[new_type.__name__] = new_type for name, test_type in sorted(tests.items()): globals()[name] = test_type create_filetype_tests() class TFile(TestCase): @property def filenames(self): for kind, paths in _FILETYPES.items(): for path in paths: yield path 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(MutagenError, File, "/dev/doesnotexist") def test_no_options(self): for filename in self.filenames: filename = os.path.join(DATA_DIR, filename) self.failIf(File(filename, options=[])) def test_fileobj(self): for filename in self.filenames: with open(filename, "rb") as h: self.assertTrue(File(h) is not None) with open(filename, "rb") as h: fileobj = BytesIO(h.read()) self.assertTrue(File(fileobj, filename=filename) is not None) def test_mock_fileobj(self): for filename in self.filenames: with open(filename, "rb") as h: @given(generate_test_file_objects(h, File)) def run(t): try: File(t) except MutagenError: pass run() 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 = BytesIO(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 = BytesIO(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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test__id3frames.py0000664000175000017500000016446100000000000020276 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import operator from tests import TestCase from mutagen._constants import GENRES from mutagen.id3._tags import read_frames, save_frame, ID3Header from mutagen.id3._util import ID3SaveConfig, is_valid_frame_id, \ ID3JunkFrameError from mutagen.id3 import APIC, CTOC, CHAP, TPE2, Frames, Frames_2_2, CRA, \ AENC, PIC, LNK, LINK, SIGN, PRIV, GRID, ENCR, COMR, USER, UFID, GEOB, \ POPM, EQU2, RVA2, COMM, SYLT, USLT, WXXX, TXXX, WCOM, TextFrame, \ UrlFrame, NumericTextFrame, NumericPartTextFrame, TPE1, TIT2, \ TimeStampTextFrame, TCON, ID3TimeStamp, Frame, RVRB, RBUF, CTOCFlags, \ PairedTextFrame, BinaryFrame, ETCO, MLLT, SYTC, PCNT, PCST, POSS, OWNE, \ SEEK, ASPI, PictureType, CRM, RVAD, RVA, ID3Tags _22 = ID3Header() _22.version = (2, 2, 0) _23 = ID3Header() _23.version = (2, 3, 0) _24 = ID3Header() _24.version = (2, 4, 0) class TVariousFrames(TestCase): DATA = [ ['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)], ['MVNM', b'\x00ab\x00', 'ab', '', dict(encoding=0)], ['MVIN', b'\x001/3\x00', '1/3', 1, dict(encoding=0)], ['GRP1', b'\x00ab\x00', 'ab', '', 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'), "[1ms]: foo\n[16ms]: bar", '', 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', b''), '', dict(frameid='TIT1', url='http://www.example.org/TIT1.txt', data=b'') ], [ '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)], ['TKWD', b'\x00ii', u'ii', '', dict(encoding=0)], ['TCAT', b'\x00ii', u'ii', '', dict(encoding=0)], ['WFED', b'http://zzz', 'http://zzz', '', {}], ['PCST', b'\x00\x00\x00\x00', 0, 0, dict(value=0)], # Chapter extension ['CHAP', (b'foo\x00\x11\x11\x11\x11\x22\x22\x22\x22' b'\x33\x33\x33\x33\x44\x44\x44\x44'), CHAP(element_id=u'foo', start_time=286331153, end_time=572662306, start_offset=858993459, end_offset=1145324612), '', dict()], ['CTOC', b'foo\x00\x03\x01bla\x00', CTOC(element_id=u'foo', flags=CTOCFlags.ORDERED | CTOCFlags.TOP_LEVEL, child_element_ids=[u'bla']), '', dict()], ['RVAD', b'\x03\x10\x00\x00\x00\x00', RVAD(adjustments=[0, 0]), '', dict()], ['RVAD', b'\x03\x08\x00\x01\x02\x03\x04\x05\x06\x07\x00\x00\x00\x00', RVAD(adjustments=[0, 1, 2, 3, -4, -5, 6, 7, 0, 0, 0, 0]), '', dict()], # 2.2 tags ['RVA', b'\x03\x10\x00\x00\x00\x00', RVA(adjustments=[0, 0]), '', dict()], ['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'), "[1ms]: foo\n[16ms]: bar", '', 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')], ['TSC', b'\x00ab', 'ab', '', dict(encoding=0)], ['TSA', b'\x00ab', 'ab', '', dict(encoding=0)], ['TS2', b'\x00ab', 'ab', '', dict(encoding=0)], ['TST', b'\x00ab', 'ab', '', dict(encoding=0)], ['TSP', b'\x00ab', 'ab', '', dict(encoding=0)], ['MVN', b'\x00ab\x00', 'ab', '', dict(encoding=0)], ['MVI', b'\x001/3\x00', '1/3', 1, dict(encoding=0)], ['GP1', b'\x00ab\x00', 'ab', '', dict(encoding=0)], ['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', b''), '', dict(frameid='TT1', url='http://www.example.org/TIT1.txt', data=b'') ], [ 'CRM', b'foo@example.org\x00test\x00woo', b'woo', '', dict(owner='foo@example.org', desc='test', data=b'woo') ], [ 'CRM', b'\x00\x00', b'', '', dict(owner='', desc='', data=b'') ], ] def _get_frame(self, id_): return getattr(getattr(__import__('mutagen.id3'), "id3"), id_) def test_all_tested(self): check = dict.fromkeys(list(Frames.keys()) + list(Frames_2_2.keys())) tested = [l[0] for l in self.DATA] for t in tested: check.pop(t, None) self.assertEqual(list(check.keys()), []) def test_tag_repr(self): for frame_id, data, value, intval, info in self.DATA: kind = self._get_frame(frame_id) tag = kind._fromData(_23, 0, data) self.assertTrue(isinstance(tag.__str__(), str)) self.assertTrue(isinstance(tag.__repr__(), str)) if hasattr(tag, "__bytes__"): self.assertTrue(isinstance(tag.__bytes__(), bytes)) def test_tag_write(self): for frame_id, data, value, intval, info in self.DATA: kind = self._get_frame(frame_id) tag = kind._fromData(_24, 0, data) towrite = tag._writeData() tag2 = kind._fromData(_24, 0, towrite) for spec in kind._framespec: attr = spec.name self.assertEquals(getattr(tag, attr), getattr(tag2, attr)) for spec in kind._optionalspec: attr = spec.name other = object() self.assertEquals( getattr(tag, attr, other), getattr(tag2, attr, other)) def test_tag_write_v23(self): for frame_id, data, value, intval, info in self.DATA: kind = self._get_frame(frame_id) tag = kind._fromData(_24, 0, data) config = ID3SaveConfig(3, "/") towrite = tag._writeData(config) tag2 = kind._fromData(_23, 0, towrite) tag3 = kind._fromData(_23, 0, tag2._writeData(config)) for spec in kind._framespec: attr = spec.name self.assertEquals(getattr(tag2, attr), getattr(tag3, attr)) for spec in kind._optionalspec: attr = spec.name other = object() self.assertEquals( getattr(tag2, attr, other), getattr(tag3, attr, other)) self.assertEqual(hasattr(tag, attr), hasattr(tag2, attr)) def test_tag(self): for frame_id, data, value, intval, info in self.DATA: kind = self._get_frame(frame_id) tag = kind._fromData(_23, 0, data) self.failUnless(tag.HashKey) self.failUnless(tag.pprint()) self.assertEquals(value, tag) if 'encoding' not in info: self.assertRaises(AttributeError, getattr, tag, 'encoding') for attr, value in info.items(): t = tag if not isinstance(value, list): value = [value] t = [t] for value, t in zip(value, iter(t)): if isinstance(value, float): self.failUnlessAlmostEqual(value, getattr(t, attr), 5) else: self.assertEquals(value, getattr(t, attr)) if isinstance(intval, int): self.assertEquals(intval, operator.pos(t)) else: self.assertRaises(TypeError, operator.pos, t) class TPCST(TestCase): def test_default(self): frame = PCST() self.assertEqual(frame.value, 0) class TETCO(TestCase): def test_default(self): frame = ETCO() self.assertEqual(frame.format, 1) self.assertEqual(frame.events, []) def test_default_mutable(self): frame = ETCO() frame.events.append(1) self.assertEqual(ETCO().events, []) class TSYTC(TestCase): def test_default(self): frame = SYTC() self.assertEqual(frame.format, 1) self.assertEqual(frame.data, b"") class TCRA(TestCase): def test_upgrade(self): 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") class TPIC(TestCase): def test_default(self): frame = PIC() self.assertEqual(frame.encoding, 1) self.assertEqual(frame.mime, u"JPG") self.assertEqual(frame.type, PictureType.COVER_FRONT) self.assertEqual(frame.desc, u"") self.assertEqual(frame.data, b"") def test_upgrade(self): 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") class TLNK(TestCase): def test_default(self): frame = LNK() self.assertEqual(frame.frameid, u"XXX") self.assertEqual(frame.url, u"") def test_upgrade(self): url = "http://foo.bar" frame = LNK(frameid="PIC", url=url, data=b"\x00") new = LINK(frame) self.assertEqual(new.frameid, "APIC") self.assertEqual(new.url, url) self.assertEqual(new.data, b"\x00") frame = LNK(frameid="XYZ") new = LINK(frame) self.assertEqual(new.frameid, "XYZ ") class TSIGN(TestCase): def test_default(self): frame = SIGN() self.assertEqual(frame.group, 0x80) self.assertEqual(frame.sig, b"") def test_hash(self): frame = SIGN(group=1, sig=b"foo") self.assertEqual(frame.HashKey, "SIGN:1:foo") def test_pprint(self): frame = SIGN(group=1, sig=b"foo") frame._pprint() class TCRM(TestCase): def test_default(self): frame = CRM() self.assertEqual(frame.owner, u"") self.assertEqual(frame.desc, u"") self.assertEqual(frame.data, b"") class TPRIV(TestCase): def test_default(self): frame = PRIV() self.assertEqual(frame.owner, u"") self.assertEqual(frame.data, b"") def test_hash(self): 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() class TGRID(TestCase): def test_default(self): frame = GRID() self.assertEqual(frame.owner, u"") self.assertEqual(frame.group, 0x80) def test_hash(self): frame = GRID(owner="foo", group=42) self.assertEqual(frame.HashKey, "GRID:42") frame._pprint() class TENCR(TestCase): def test_default(self): frame = ENCR() self.assertEqual(frame.owner, u"") self.assertEqual(frame.method, 0x80) self.assertEqual(frame.data, b"") def test_hash(self): frame = ENCR(owner="foo", method=42, data=b"\xff") self.assertEqual(frame.HashKey, "ENCR:foo") frame._pprint() class TOWNE(TestCase): def test_default(self): frame = OWNE() self.assertEqual(frame.encoding, 1) self.assertEqual(frame.price, u"") self.assertEqual(frame.date, u"19700101") self.assertEqual(frame.seller, u"") class TCOMR(TestCase): def test_default(self): frame = COMR() self.assertEqual(frame.encoding, 1) self.assertEqual(frame.price, u"") self.assertEqual(frame.valid_until, u"19700101") self.assertEqual(frame.contact, u"") self.assertEqual(frame.format, 0) self.assertEqual(frame.seller, u"") self.assertEqual(frame.desc, u"") def test_hash(self): 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() class TBinaryFrame(TestCase): def test_default(self): frame = BinaryFrame() self.assertEqual(frame.data, b"") class TUSER(TestCase): def test_default(self): frame = USER() self.assertEqual(frame.encoding, 1) self.assertEqual(frame.lang, u"XXX") self.assertEqual(frame.text, u"") def test_hash(self): frame = USER(encoding=0, lang="foo", text="bla") self.assertEqual(frame.HashKey, "USER:foo") frame._pprint() self.assertEquals(USER(text="a").HashKey, USER(text="b").HashKey) self.assertNotEquals( USER(lang="abc").HashKey, USER(lang="def").HashKey) class TMLLT(TestCase): def test_default(self): frame = MLLT() self.assertEqual(frame.frames, 0) self.assertEqual(frame.bytes, 0) self.assertEqual(frame.milliseconds, 0) self.assertEqual(frame.bits_for_bytes, 0) self.assertEqual(frame.bits_for_milliseconds, 0) self.assertEqual(frame.data, b"") class TTIT2(TestCase): def test_hash(self): self.assertEquals(TIT2(text="a").HashKey, TIT2(text="b").HashKey) class TUFID(TestCase): def test_default(self): frame = UFID() self.assertEqual(frame.owner, u"") self.assertEqual(frame.data, b"") def test_hash(self): frame = UFID(owner="foo", data=b"\x42") self.assertEqual(frame.HashKey, "UFID:foo") frame._pprint() self.assertEquals(UFID(data=b"1").HashKey, UFID(data=b"2").HashKey) self.assertNotEquals(UFID(owner="a").HashKey, UFID(owner="b").HashKey) class TPairedTextFrame(TestCase): def test_default(self): frame = PairedTextFrame() self.assertEqual(frame.encoding, 1) self.assertEqual(frame.people, []) class TRVAD(TestCase): def test_default(self): frame = RVAD() self.assertEqual(frame.adjustments, [0, 0]) def test_hash(self): frame = RVAD() self.assertEqual(frame.HashKey, "RVAD") def test_upgrade(self): rva = RVA(adjustments=[1, 2]) self.assertEqual(RVAD(rva).adjustments, [1, 2]) class TLINK(TestCase): def test_read(self): frame = LINK() frame._readData(_24, b"XXX\x00Foo\x00") # either we can read invalid frame ids or we fail properly, atm we read # them. self.assertEqual(frame.frameid, "XXX\x00") def test_default(self): frame = LINK() self.assertEqual(frame.frameid, u"XXXX") self.assertEqual(frame.url, u"") def test_hash(self): 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:") class TAENC(TestCase): def test_default(self): frame = AENC() self.assertEqual(frame.owner, u"") self.assertEqual(frame.preview_start, 0) self.assertEqual(frame.preview_length, 0) def test_hash(self): frame = AENC( owner="foo", preview_start=1, preview_length=2, data=b"\x42") self.assertEqual(frame.HashKey, "AENC:foo") frame._pprint() class TGEOB(TestCase): def test_default(self): frame = GEOB() self.assertEqual(frame.encoding, 1) self.assertEqual(frame.mime, u"") self.assertEqual(frame.filename, u"") self.assertEqual(frame.desc, u"") self.assertEqual(frame.data, b"") def test_hash(self): frame = GEOB( encoding=0, mtime="m", filename="f", desc="d", data=b"\x42") self.assertEqual(frame.HashKey, "GEOB:d") frame._pprint() self.assertEquals(GEOB(data=b"1").HashKey, GEOB(data=b"2").HashKey) self.assertNotEquals(GEOB(desc="a").HashKey, GEOB(desc="b").HashKey) class TPOPM(TestCase): def test_default(self): frame = POPM() self.assertEqual(frame.email, u"") self.assertEqual(frame.rating, 0) self.assertFalse(hasattr(frame, "count")) def test_hash(self): frame = POPM(email="e", rating=42) self.assertEqual(frame.HashKey, "POPM:e") frame._pprint() self.assertEquals(POPM(count=1).HashKey, POPM(count=2).HashKey) self.assertNotEquals(POPM(email="a").HashKey, POPM(email="b").HashKey) class TEQU2(TestCase): def test_default(self): frame = EQU2() self.assertEqual(frame.method, 0) self.assertEqual(frame.desc, u"") self.assertEqual(frame.adjustments, []) def test_default_mutable(self): frame = EQU2() frame.adjustments.append(1) self.assertEqual(EQU2(), []) def test_hash(self): frame = EQU2(method=42, desc="d", adjustments=[(0, 0)]) self.assertEqual(frame.HashKey, "EQU2:d") frame._pprint() class TSEEK(TestCase): def test_default(self): frame = SEEK() self.assertEqual(frame.offset, 0) class TPOSS(TestCase): def test_default(self): frame = POSS() self.assertEqual(frame.format, 1) self.assertEqual(frame.position, 0) class TCOMM(TestCase): def test_default(self): frame = COMM() self.assertEqual(frame.encoding, 1) self.assertEqual(frame.lang, u"XXX") self.assertEqual(frame.desc, u"") self.assertEqual(frame.text, []) def test_hash(self): frame = COMM(encoding=0, lang="foo", desc="d") self.assertEqual(frame.HashKey, "COMM:d:foo") frame._pprint() 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_bad_unicodedecode(self): # 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 TSYLT(TestCase): def test_default(self): frame = SYLT() self.assertEqual(frame.encoding, 1) self.assertEqual(frame.lang, u"XXX") self.assertEqual(frame.format, 1) self.assertEqual(frame.type, 0) self.assertEqual(frame.desc, u"") self.assertEqual(frame.text, u"") def test_hash(self): 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_bad_sylt(self): self.assertRaises( ID3JunkFrameError, SYLT._fromData, _24, 0x0, b"\x00eng\x01description\x00foobar") self.assertRaises( ID3JunkFrameError, SYLT._fromData, _24, 0x0, b"\x00eng\x01description\x00foobar\x00\xFF\xFF\xFF") class TRVRB(TestCase): def test_default(self): frame = RVRB() self.assertEqual(frame.left, 0) self.assertEqual(frame.right, 0) self.assertEqual(frame.bounce_left, 0) self.assertEqual(frame.bounce_right, 0) self.assertEqual(frame.feedback_ltl, 0) self.assertEqual(frame.feedback_ltr, 0) self.assertEqual(frame.feedback_rtr, 0) self.assertEqual(frame.feedback_rtl, 0) self.assertEqual(frame.premix_ltr, 0) self.assertEqual(frame.premix_rtl, 0) def test_extradata(self): self.assertEqual(RVRB()._readData(_24, b'L1R1BBFFFFPP#xyz'), b'#xyz') class TRBUF(TestCase): def test_default(self): frame = RBUF() self.assertEqual(frame.size, 0) self.assertFalse(hasattr(frame, "info")) self.assertFalse(hasattr(frame, "offset")) def test_extradata(self): self.assertEqual( RBUF()._readData( _24, b'\x00\x01\x00\x01\x00\x00\x00\x00#xyz'), b'#xyz') class TUSLT(TestCase): def test_default(self): frame = USLT() self.assertEqual(frame.encoding, 1) self.assertEqual(frame.lang, u"XXX") self.assertEqual(frame.desc, u"") self.assertEqual(frame.text, u"") def test_hash(self): frame = USLT(encoding=0, lang="foo", desc="d", text="t") self.assertEqual(frame.HashKey, "USLT:d:foo") assert frame._pprint() == "d=foo=t" class TWXXX(TestCase): def test_default(self): frame = WXXX() self.assertEqual(frame.encoding, 1) self.assertEqual(frame.desc, u"") self.assertEqual(frame.url, u"") def test_hash(self): self.assert_(isinstance(WXXX(url='durl'), WXXX)) frame = WXXX(encoding=0, desc="d", url="u") self.assertEqual(frame.HashKey, "WXXX:d") frame._pprint() self.assertEquals(WXXX(text="a").HashKey, WXXX(text="b").HashKey) self.assertNotEquals(WXXX(desc="a").HashKey, WXXX(desc="b").HashKey) class TTXXX(TestCase): def test_default(self): self.assertEqual(TXXX(), TXXX(desc=u"", encoding=1, text=[])) def test_hash(self): frame = TXXX(encoding=0, desc="d", text=[]) self.assertEqual(frame.HashKey, "TXXX:d") frame._pprint() self.assertEquals(TXXX(text="a").HashKey, TXXX(text="b").HashKey) self.assertNotEquals(TXXX(desc="a").HashKey, TXXX(desc="b").HashKey) class TWCOM(TestCase): def test_hash(self): frame = WCOM(url="u") self.assertEqual(frame.HashKey, "WCOM:u") frame._pprint() class TUrlFrame(TestCase): def test_default(self): self.assertEqual(UrlFrame(), UrlFrame(url=u"")) def test_main(self): self.assertEqual(UrlFrame("url").url, "url") class TNumericTextFrame(TestCase): def test_default(self): self.assertEqual( NumericTextFrame(), NumericTextFrame(encoding=1, text=[])) def test_main(self): self.assertEqual(NumericTextFrame(text='1').text, ["1"]) self.assertEqual(+NumericTextFrame(text='1'), 1) class TNumericPartTextFrame(TestCase): def test_default(self): self.assertEqual( NumericPartTextFrame(), NumericPartTextFrame(encoding=1, text=[])) def test_main(self): self.assertEqual(NumericPartTextFrame(text='1/2').text, ["1/2"]) self.assertEqual(+NumericPartTextFrame(text='1/2'), 1) class Tread_frames_load_frame(TestCase): def test_detect_23_ints_in_24_frames(self): head = b'TIT1\x00\x00\x01\x00\x00\x00\x00' tail = b'TPE1\x00\x00\x00\x05\x00\x00\x00Yay!' tagsgood = read_frames(_24, head + b'a' * 127 + tail, Frames)[0] tagsbad = read_frames(_24, head + b'a' * 255 + tail, Frames)[0] 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 = read_frames(_24, head + b'a' * 127, Frames)[0] tagsbad = read_frames(_24, head + b'a' * 255, Frames)[0] self.assertEquals(1, len(tagsgood)) self.assertEquals(1, len(tagsbad)) self.assertEquals('a' * 127, tagsgood[0]) self.assertEquals('a' * 255, tagsbad[0]) def test_zerolength_framedata(self): tail = b'\x00' * 6 for head in b'WOAR TENC TCOP TOPE WXXX'.split(): data = head + tail self.assertEquals( 0, len(list(read_frames(_24, data, Frames)[1]))) def test_drops_truncated_frames(self): 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(read_frames(_24, data, Frames)[1])) def test_drops_nonalphanum_frames(self): 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(read_frames(_24, data, Frames)[0])) def test_frame_too_small(self): self.assertEquals([], read_frames(_24, b'012345678', Frames)[0]) self.assertEquals([], read_frames(_23, b'012345678', Frames)[0]) self.assertEquals([], read_frames(_22, b'01234', Frames_2_2)[0]) self.assertEquals( [], read_frames(_22, b'TT1' + b'\x00' * 3, Frames_2_2)[0]) def test_unknown_22_frame(self): data = b'XYZ\x00\x00\x01\x00' self.assertEquals([data], read_frames(_22, data, {})[1]) def test_22_uses_direct_ints(self): data = b'TT1\x00\x00\x83\x00' + (b'123456789abcdef' * 16) tag = read_frames(_22, data, Frames_2_2)[0][0] self.assertEquals(data[7:7 + 0x82].decode('latin1'), tag.text[0]) def test_load_write(self): artists = [s.decode('utf8') for s in [b'\xc2\xb5', b'\xe6\x97\xa5\xe6\x9c\xac']] artist = TPE1(encoding=3, text=artists) config = ID3SaveConfig() tag = read_frames(_24, save_frame(artist, config=config), Frames)[0][0] self.assertEquals('TPE1', type(tag).__name__) self.assertEquals(artist.text, tag.text) class TTPE2(TestCase): def test_unsynch(self): header = ID3Header() header.version = (2, 4, 0) header._flags = 0x80 badsync = b'\x00\xff\x00ab\x00' self.assertEquals(TPE2._fromData(header, 0, badsync), [u"\xffab"]) header._flags = 0x00 self.assertEquals(TPE2._fromData(header, 0x02, badsync), [u"\xffab"]) tag = TPE2._fromData(header, 0, badsync) self.assertEquals(tag, [u"\xff", u"ab"]) class TTPE1(TestCase): def test_badencoding(self): self.assertRaises( ID3JunkFrameError, TPE1._fromData, _24, 0, b"\x09ab") self.assertRaises(ValueError, TPE1, encoding=9, text="ab") def test_badsync(self): frame = TPE1._fromData(_24, 0x02, b"\x00\xff\xfe") self.assertEqual(frame.text, [u'\xff\xfe']) def test_noencrypt(self): self.assertRaises( NotImplementedError, TPE1._fromData, _24, 0x04, b"\x00") self.assertRaises( NotImplementedError, TPE1._fromData, _23, 0x40, b"\x00") def test_badcompress(self): self.assertRaises( ID3JunkFrameError, TPE1._fromData, _24, 0x08, b"\x00\x00\x00\x00#") self.assertRaises( ID3JunkFrameError, TPE1._fromData, _23, 0x80, b"\x00\x00\x00\x00#") def test_junkframe(self): self.assertRaises( ID3JunkFrameError, TPE1._fromData, _24, 0, b"") self.assertRaises( ID3JunkFrameError, TPE1._fromData, _24, 0, b'\x03A\xff\xfe') def test_lengthone_utf16(self): 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 tpe1 = TPE1._fromData( _24, 0, b'\x01\xff\xfeH\x00e\x00l\x00l\x00o\x00\x00') self.assertEquals(tpe1, [u'Hello']) tpe1 = TPE1._fromData( _24, 0, b'\x02\x00H\x00e\x00l\x00l\x00o\x00') self.assertEquals(tpe1, [u'Hello']) def test_utf_16_missing_bom(self): tpe1 = TPE1._fromData( _24, 0, b'\x01H\x00e\x00l\x00l\x00o\x00\x00\x00') self.assertEquals(tpe1, [u'Hello']) def test_utf_16_missing_bom_wrong_nullterm(self): tpe1 = TPE1._fromData( _24, 0, b'\x01H\x00e\x00l\x00l\x00o\x00\x00') self.assertEquals(tpe1, [u'Hello']) tpe1 = TPE1._fromData( _24, 0, b'\x01f\x00o\x00o\x00\x00\x00b\x00a\x00r\x00\x00') self.assertEquals(tpe1, [u"foo", u"bar"]) def test_zlib_bpi(self): tpe1 = TPE1(encoding=0, text="a" * (0xFFFF - 2)) data = 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): 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): 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']) class TTCON(TestCase): def _g(self, s): return TCON(text=s).genres def test_empty(self): self.assertEquals(self._g(""), []) def test_num(self): for i in range(len(GENRES)): self.assertEquals(self._g("%02d" % i), [GENRES[i]]) def test_parened_num(self): for i in range(len(GENRES)): self.assertEquals(self._g("(%02d)" % i), [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 = 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 = TCON(encoding=0, text="") gen.genres = "foo" self.assertEquals(gen.genres, ["foo"]) def test_nodoubledecode(self): gen = TCON(encoding=1, text=u"(255)genre") gen.genres = gen.genres self.assertEquals(gen.genres, [u"Unknown", u"genre"]) class TID3TimeStamp(TestCase): def test_Y(self): s = ID3TimeStamp('1234') self.assertEquals(s.year, 1234) self.assertEquals(s.text, '1234') def test_yM(self): s = ID3TimeStamp('1234-56') self.assertEquals(s.year, 1234) self.assertEquals(s.month, 56) self.assertEquals(s.text, '1234-56') def test_ymD(self): s = ID3TimeStamp('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 = ID3TimeStamp('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 = ID3TimeStamp('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 = ID3TimeStamp('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 = ID3TimeStamp('1234-56-78T12:34:56') s.month = None self.assertEquals(s.text, '1234') def test_alternate_reprs(self): s = ID3TimeStamp('1234-56.78 12:34:56') self.assertEquals(s.text, '1234-56-78 12:34:56') def test_order(self): s = ID3TimeStamp('1234') t = ID3TimeStamp('1233-12') u = ID3TimeStamp('1234-01') self.assert_(t < s < u) self.assert_(u > s > t) def test_types(self): self.assertRaises(TypeError, ID3TimeStamp, b"blah") self.assertEquals( str(ID3TimeStamp(u"2000-01-01")), u"2000-01-01") self.assertEquals( bytes(ID3TimeStamp(u"2000-01-01")), b"2000-01-01") class TFrameTest(object): FRAME = None def test_has_doc(self): self.failUnless(self.FRAME.__doc__, "%s has no docstring" % self.FRAME) def test_fake_zlib(self): header = ID3Header() header.version = (2, 4, 0) self.assertRaises(ID3JunkFrameError, self.FRAME._fromData, header, Frame.FLAG24_COMPRESS, b'\x03abcdefg') def test_no_hash(self): self.failUnlessRaises( TypeError, {}.__setitem__, self.FRAME(), None) def test_is_valid_frame_id(self): self.assertTrue(is_valid_frame_id(self.FRAME.__name__)) def test_all_specs_have_default(self): for spec in self.FRAME._framespec: self.assertTrue( spec.default is not None, msg="%r:%r" % (self.FRAME, spec.name)) @classmethod def create_frame_tests(cls): tests = {} for kind in (list(Frames.values()) + list(Frames_2_2.values())): new_type = type(cls.__name__ + kind.__name__, (cls, TestCase), {"FRAME": kind}) assert new_type.__name__ not in tests tests[new_type.__name__] = new_type for name, test_type in sorted(tests.items()): globals()[name] = test_type TFrameTest.create_frame_tests() class FrameIDValidate(TestCase): def test_valid(self): self.failUnless(is_valid_frame_id("APIC")) self.failUnless(is_valid_frame_id("TPE2")) def test_invalid(self): self.failIf(is_valid_frame_id("MP3e")) self.failIf(is_valid_frame_id("+ABC")) class TTimeStampTextFrame(TestCase): def test_default(self): self.assertEqual( TimeStampTextFrame(), TimeStampTextFrame(encoding=1, text=[])) def test_compare_to_unicode(self): frame = TimeStampTextFrame(encoding=0, text=[u'1987', u'1988']) self.failUnlessEqual(frame, str(frame)) class TTextFrame(TestCase): def test_defaults(self): self.assertEqual(TextFrame(), TextFrame(encoding=1, text=[])) def test_default_default_mutable(self): frame = TextFrame() frame.text.append("foo") self.assertEqual(TextFrame().text, []) def test_main(self): self.assertEqual(TextFrame(text='text').text, ["text"]) self.assertEqual(TextFrame(text=['a', 'b']).text, ["a", "b"]) def test_multi_value(self): frame = TextFrame( text=[u"foo", u"", u"", u"bar", u"", u""], encoding=0) config = ID3SaveConfig(3, None) data = frame._writeData(config) frame = frame._fromData(_24, 0x0, data) self.assertEqual(frame.text, [u"foo", u"", u"", u"bar", u"", u""]) frame = frame._fromData(_23, 0x0, data) self.assertEqual(frame.text, [u"foo", u"", u"", u"bar"]) frame = frame._fromData(_22, 0x0, data) self.assertEqual(frame.text, [u"foo", u"", u"", u"bar"]) def test_list_iface(self): frame = TextFrame() frame.append("a") frame.extend(["b", "c"]) self.assertEqual(frame.text, ["a", "b", "c"]) def test_zlib_latin1(self): tag = TextFrame._fromData( _24, 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): tag = TextFrame._fromData(_24, 0x01, b'\x00\x00\x00\x06\x00A test') self.assertEquals(tag.encoding, 0) self.assertEquals(tag, ['A test']) def test_utf8(self): tag = TextFrame._fromData(_23, 0x00, b'\x03this is a test') self.assertEquals(tag.encoding, 3) self.assertEquals(tag, 'this is a test') def test_zlib_utf16(self): 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 = TextFrame._fromData(_23, 0x80, data) self.assertEquals(tag.encoding, 1) self.assertEquals(tag, ['this is a/test']) tag = TextFrame._fromData(_24, 0x08, data) self.assertEquals(tag.encoding, 1) self.assertEquals(tag, ['this is a/test']) class TRVA2(TestCase): def test_default(self): frame = RVA2() self.assertEqual(frame.desc, u"") self.assertEqual(frame.channel, 1) self.assertEqual(frame.gain, 1) self.assertEqual(frame.peak, 1) def test_basic(self): r = RVA2(gain=1, channel=1, peak=1) self.assertEqual(r, r) self.assertNotEqual(r, 42) def test_hash_key(self): frame = RVA2(method=42, desc="d", channel=1, gain=1, peak=1) self.assertEqual(frame.HashKey, "RVA2:d") self.assertEquals(RVA2(gain=1).HashKey, RVA2(gain=2).HashKey) self.assertNotEquals(RVA2(desc="a").HashKey, RVA2(desc="b").HashKey) def test_pprint(self): frame = RVA2(method=42, desc="d", channel=1, gain=1, peak=1) frame._pprint() def test_wacky_truncated(self): data = b'\x01{\xf0\x10\xff\xff\x00' self.assertRaises(ID3JunkFrameError, RVA2._fromData, _24, 0x00, data) def test_bad_number_of_bits(self): data = b'\x00\x00\x01\xe6\xfc\x10{\xd7' self.assertRaises(ID3JunkFrameError, RVA2._fromData, _24, 0x00, data) class TCTOC(TestCase): def test_defaults(self): self.assertEqual(CTOC(), CTOC(element_id=u"", flags=0, child_element_ids=[], sub_frames=[])) def test_hash(self): frame = CTOC(element_id=u"foo", flags=3, child_element_ids=[u"ch0"], sub_frames=[TPE2(encoding=3, text=[u"foo"])]) self.assertEqual(frame.HashKey, "CTOC:foo") def test_pprint(self): frame = CTOC(element_id=u"foo", flags=3, child_element_ids=[u"ch0"], sub_frames=[TPE2(encoding=3, text=[u"foo"])]) self.assertEqual( frame.pprint(), "CTOC=foo flags=3 child_element_ids=ch0\n TPE2=foo") def test_write(self): frame = CTOC(element_id=u"foo", flags=3, child_element_ids=[u"ch0"], sub_frames=[TPE2(encoding=3, text=[u"f", u"b"])]) config = ID3SaveConfig(3, "/") data = (b"foo\x00\x03\x01ch0\x00TPE2\x00\x00\x00\x0b\x00\x00\x01" b"\xff\xfef\x00/\x00b\x00\x00\x00") self.assertEqual(frame._writeData(config), data) def test_eq(self): self.assertEqual(CTOC(), CTOC()) self.assertNotEqual(CTOC(), object()) class TASPI(TestCase): def test_default(self): frame = ASPI() self.assertEqual(frame.S, 0) self.assertEqual(frame.L, 0) self.assertEqual(frame.N, 0) self.assertEqual(frame.b, 0) self.assertEqual(frame.Fi, []) def test_default_default_mutable(self): frame = ASPI() frame.Fi.append(1) self.assertEqual(ASPI().Fi, []) class TCHAP(TestCase): def test_default(self): frame = CHAP() self.assertEqual(frame.element_id, u"") self.assertEqual(frame.start_time, 0) self.assertEqual(frame.end_time, 0) self.assertEqual(frame.start_offset, 0xffffffff) self.assertEqual(frame.end_offset, 0xffffffff) self.assertEqual(frame.sub_frames, ID3Tags()) def test_hash(self): frame = CHAP(element_id=u"foo", start_time=0, end_time=0, start_offset=0, end_offset=0, sub_frames=[TPE2(encoding=3, text=[u"foo"])]) self.assertEqual(frame.HashKey, "CHAP:foo") def test_pprint(self): frame = CHAP(element_id=u"foo", start_time=0, end_time=0, start_offset=0, end_offset=0, sub_frames=[TPE2(encoding=3, text=[u"foo"])]) self.assertEqual( frame.pprint(), "CHAP=foo time=0..0 offset=0..0\n TPE2=foo") def test_eq(self): self.assertEqual(CHAP(), CHAP()) self.assertNotEqual(CHAP(), object()) class TPCNT(TestCase): def test_default(self): frame = PCNT() self.assertEqual(frame.count, 0) class TAPIC(TestCase): def test_default(self): frame = APIC() self.assertEqual(frame.encoding, 1) self.assertEqual(frame.mime, u"") self.assertEqual(frame.type, 3) self.assertEqual(frame.desc, u"") self.assertEqual(frame.data, b"") def test_hash(self): frame = APIC(encoding=0, mime=u"m", type=3, desc=u"d", data=b"\x42") self.assertEqual(frame.HashKey, "APIC:d") def test_pprint(self): frame = APIC( encoding=0, mime=u"mime", type=3, desc=u"desc", data=b"\x42") self.assertEqual(frame._pprint(), u"cover front, desc (mime, 1 bytes)") def test_multi(self): self.assertEquals(APIC(data=b"1").HashKey, APIC(data=b"2").HashKey) self.assertNotEquals(APIC(desc="a").HashKey, APIC(desc="b").HashKey) def test_repr(self): frame = APIC(encoding=0, mime=u"m", type=3, desc=u"d", data=b"\x42") expected = ( "APIC(encoding=, mime='m', " "type=, desc='d', data=b'B')") self.assertEqual(repr(frame), expected) new_frame = APIC() new_frame._readData(_24, frame._writeData()) self.assertEqual(repr(new_frame), expected) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test__id3specs.py0000664000175000017500000003007000000000000020122 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- from tests import TestCase from mutagen.id3._specs import SpecError, Latin1TextListSpec, ID3FramesSpec, \ ASPIIndexSpec, ByteSpec, EncodingSpec, StringSpec, BinaryDataSpec, \ EncodedTextSpec, VolumePeakSpec, VolumeAdjustmentSpec, CTOCFlagsSpec, \ Spec, SynchronizedTextSpec, TimeStampSpec, FrameIDSpec, RVASpec from mutagen.id3._frames import Frame from mutagen.id3._tags import ID3Header, ID3Tags, ID3SaveConfig from mutagen.id3 import TIT3, ASPI, CTOCFlags, ID3TimeStamp class TSynchronizedTextSpec(TestCase): def test_write(self): 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(None, f, s.write(None, f, values)), (values, b"")) data = s.write(None, f, [(u"A", 100)]) self.assertEquals(data, b"\xff\xfeA\x00\x00\x00\x00\x00\x00d") # utf-16be f.encoding = 2 self.assertEqual( s.read(None, f, s.write(None, f, values)), (values, b"")) self.assertEquals( s.write(None, f, [(u"A", 100)]), b"\x00A\x00\x00\x00\x00\x00d") # utf-8 f.encoding = 3 self.assertEqual( s.read(None, f, s.write(None, f, values)), (values, b"")) self.assertEquals( s.write(None, f, [(u"A", 100)]), b"A\x00\x00\x00\x00d") class TTimeStampSpec(TestCase): def test_read(self): s = TimeStampSpec('name') f = Frame() f.encoding = 0 header = ID3Header() header.version = (2, 4, 0) self.assertEquals( (ID3TimeStamp('ab'), b'fg'), s.read(header, f, b'ab\x00fg')) self.assertEquals( (ID3TimeStamp('1234'), b''), s.read(header, f, b'1234\x00')) def test_write(self): s = TimeStampSpec('name') f = Frame() f.encoding = 0 self.assertEquals(b'1234\x00', s.write(None, f, ID3TimeStamp('1234'))) self.assertRaises(AttributeError, s.write, None, f, None) class TEncodedTextSpec(TestCase): def test_read(self): s = EncodedTextSpec('name') f = Frame() f.encoding = 0 header = ID3Header() header.version = (2, 4, 0) self.assertEquals((u'abcd', b'fg'), s.read(header, f, b'abcd\x00fg')) def test_write(self): s = EncodedTextSpec('name') f = Frame() f.encoding = 0 self.assertEquals(b'abcdefg\x00', s.write(None, f, u'abcdefg')) self.assertRaises(AttributeError, s.write, None, f, None) class TEncodingSpec(TestCase): def test_read(self): s = EncodingSpec('name') self.assertEquals((3, b'abcdefg'), s.read(None, None, b'\x03abcdefg')) self.assertRaises(SpecError, s.read, None, None, b'\x04abcdefg') def test_write(self): s = EncodingSpec('name') self.assertEquals(b'\x00', s.write(None, None, 0)) self.assertRaises(TypeError, s.write, None, None, b'abc') self.assertRaises(TypeError, s.write, None, None, None) def test_validate(self): s = EncodingSpec('name') self.assertRaises(TypeError, s.validate, None, None) class TASPIIndexSpec(TestCase): def test_read(self): frame = ASPI(b=16, N=2) s = ASPIIndexSpec('name', []) self.assertRaises(SpecError, s.read, None, frame, b'') self.assertEqual( s.read(None, frame, b'\x01\x00\x00\x01'), ([256, 1], b"")) frame = ASPI(b=42) self.assertRaises(SpecError, s.read, None, frame, b'') class TVolumeAdjustmentSpec(TestCase): def test_validate(self): s = VolumeAdjustmentSpec('gain', 0) self.assertRaises(ValueError, s.validate, None, 65) def test_read(self): s = VolumeAdjustmentSpec('gain', 0) self.assertEquals((0.0, b''), s.read(None, None, b'\x00\x00')) self.assertEquals((2.0, b''), s.read(None, None, b'\x04\x00')) self.assertEquals((-2.0, b''), s.read(None, None, b'\xfc\x00')) def test_write(self): s = VolumeAdjustmentSpec('gain', 0) self.assertEquals(b'\x00\x00', s.write(None, None, 0.0)) self.assertEquals(b'\x04\x00', s.write(None, None, 2.0)) self.assertEquals(b'\xfc\x00', s.write(None, None, -2.0)) class TByteSpec(TestCase): def test_validate(self): s = ByteSpec('byte') self.assertRaises(ValueError, s.validate, None, 1000) def test_read(self): s = ByteSpec('name') self.assertEquals((97, b'bcdefg'), s.read(None, None, b'abcdefg')) def test_write(self): s = ByteSpec('name') self.assertEquals(b'a', s.write(None, None, 97)) self.assertRaises(TypeError, s.write, None, None, b'abc') self.assertRaises(TypeError, s.write, None, None, None) class TVolumePeakSpec(TestCase): def test_validate(self): s = VolumePeakSpec('peak', 0) self.assertRaises(ValueError, s.validate, None, 2) class TStringSpec(TestCase): def test_validate(self): s = StringSpec('byte', 3) 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") self.assertRaises(TypeError, s.validate, None, None) self.assertRaises(TypeError, s.validate, None, b"ABC") self.assertRaises(ValueError, s.validate, None, u"\xf6\xe4\xfc") def test_read(self): s = StringSpec('name', 3) self.assertEquals(('abc', b'defg'), s.read(None, None, b'abcdefg')) self.assertRaises(SpecError, s.read, None, None, b'\xff') def test_write(self): s = StringSpec('name', 3) self.assertEquals(b'abc', s.write(None, None, 'abcdefg')) self.assertEquals(b'\x00\x00\x00', s.write(None, None, '\x00')) self.assertEquals(b'a\x00\x00', s.write(None, None, 'a')) class TBinaryDataSpec(TestCase): def test_validate(self): s = BinaryDataSpec('name') self.assertRaises(TypeError, s.validate, None, None) self.assertEqual(s.validate(None, b"abc"), b"abc") self.assertRaises(TypeError, s.validate, None, "abc") def test_read(self): s = BinaryDataSpec('name') self.assertEquals((b'abcdefg', b''), s.read(None, None, b'abcdefg')) def test_write(self): s = BinaryDataSpec('name') self.assertEquals(b'43', s.write(None, None, 43)) self.assertEquals(b'abc', s.write(None, None, b'abc')) class TSpec(TestCase): def test_no_hash(self): self.failUnlessRaises( TypeError, {}.__setitem__, Spec("foo", None), None) class TRVASpec(TestCase): def test_read(self): spec = RVASpec("name", False) val, rest = spec.read( None, None, b"\x03\x10\xc7\xc7\xc7\xc7\x00\x00\x00\x00\x00\x00\x00\x00") self.assertEqual(rest, b"") self.assertEqual(val, [51143, 51143, 0, 0, 0, 0]) def test_read_stereo_only(self): spec = RVASpec("name", True) val, rest = spec.read( None, None, b"\x03\x10\xc7\xc7\xc7\xc7\x00\x00\x00\x00\x00\x00\x00\x00") self.assertEqual(rest, b"\x00\x00\x00\x00") self.assertEqual(val, [51143, 51143, 0, 0]) def test_write(self): spec = RVASpec("name", False) data = spec.write(None, None, [0, 1, 2, 3, -4, -5]) self.assertEqual( data, b"\x03\x10\x00\x00\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05") def test_write_stereo_only(self): spec = RVASpec("name", True) self.assertRaises( SpecError, spec.write, None, None, [0, 0, 0, 0, 0, 0]) def test_validate(self): spec = RVASpec("name", False) self.assertRaises(ValueError, spec.validate, None, []) self.assertEqual(spec.validate(None, [1, 2]), [1, 2]) class TFrameIDSpec(TestCase): def test_read(self): spec = FrameIDSpec("name", 3) self.assertEqual(spec.read(None, None, b"FOOX"), (u"FOO", b"X")) def test_validate(self): spec = FrameIDSpec("name", 3) self.assertRaises(ValueError, spec.validate, None, u"123") self.assertRaises(ValueError, spec.validate, None, u"TXXX") self.assertEqual(spec.validate(None, u"TXX"), u"TXX") spec = FrameIDSpec("name", 4) self.assertEqual(spec.validate(None, u"TXXX"), u"TXXX") class TCTOCFlagsSpec(TestCase): def test_read(self): spec = CTOCFlagsSpec("name") v, r = spec.read(None, None, b"\x03") self.assertEqual(r, b"") self.assertEqual(v, 3) self.assertTrue(isinstance(v, CTOCFlags)) def test_write(self): spec = CTOCFlagsSpec("name") self.assertEqual(spec.write(None, None, CTOCFlags.ORDERED), b"\x01") def test_validate(self): spec = CTOCFlagsSpec("name") self.assertEqual(spec.validate(None, 3), 3) self.assertTrue(isinstance(spec.validate(None, 3), CTOCFlags)) self.assertEqual(spec.validate(None, None), None) class TID3FramesSpec(TestCase): def test_read_empty(self): header = ID3Header() header.version = (2, 4, 0) spec = ID3FramesSpec("name") value, data = spec.read(header, None, b"") self.assertEqual(data, b"") self.assertTrue(isinstance(value, ID3Tags)) def test_read_tit3(self): header = ID3Header() header.version = (2, 4, 0) spec = ID3FramesSpec("name") value, data = spec.read(header, None, b"TIT3" + b"\x00\x00\x00\x03" + b"\x00\x00" + b"\x03" + b"F\x00") self.assertTrue(isinstance(value, ID3Tags)) self.assertEqual(data, b"") frames = value.getall("TIT3") self.assertEqual(len(frames), 1) self.assertEqual(frames[0].encoding, 3) self.assertEqual(frames[0].text, [u"F"]) def test_write_empty(self): header = ID3Header() header.version = (2, 4, 0) spec = ID3FramesSpec("name") config = ID3SaveConfig() tags = ID3Tags() self.assertEqual(spec.write(config, None, tags), b"") def test_write_tit3(self): spec = ID3FramesSpec("name") config = ID3SaveConfig() tags = ID3Tags() tags.add(TIT3(encoding=3, text=[u"F", u"B"])) self.assertEqual(spec.write(config, None, tags), b"TIT3" + b"\x00\x00\x00\x05" + b"\x00\x00" + b"\x03" + b"F\x00" + b"B\x00") def test_write_tit3_v23(self): spec = ID3FramesSpec("name") config = ID3SaveConfig(3, "/") tags = ID3Tags() tags.add(TIT3(encoding=3, text=[u"F", u"B"])) self.assertEqual(spec.write(config, None, tags), b"TIT3" + b"\x00\x00\x00\x0B" + b"\x00\x00" + b"\x01" + b"\xff\xfeF\x00/\x00B\x00\x00\x00") def test_validate(self): header = ID3Header() header.version = (2, 4, 0) spec = ID3FramesSpec("name") self.assertRaises(TypeError, spec.validate, None, None) self.assertTrue(isinstance(spec.validate(None, []), ID3Tags)) v = spec.validate(None, [TIT3(encoding=3, text=[u"foo"])]) self.assertEqual(v.getall("TIT3")[0].text, [u"foo"]) class TLatin1TextListSpec(TestCase): def test_read(self): spec = Latin1TextListSpec("name") self.assertEqual(spec.read(None, None, b"\x00xxx"), ([], b"xxx")) self.assertEqual( spec.read(None, None, b"\x01foo\x00"), ([u"foo"], b"")) self.assertEqual( spec.read(None, None, b"\x01\x00"), ([u""], b"")) self.assertEqual( spec.read(None, None, b"\x02f\x00o\x00"), ([u"f", u"o"], b"")) def test_write(self): spec = Latin1TextListSpec("name") self.assertEqual(spec.write(None, None, []), b"\x00") self.assertEqual(spec.write(None, None, [u""]), b"\x01\x00") def test_validate(self): spec = Latin1TextListSpec("name") self.assertRaises(TypeError, spec.validate, None, object()) self.assertRaises(TypeError, spec.validate, None, None) self.assertEqual(spec.validate(None, [u"foo"]), [u"foo"]) self.assertEqual(spec.validate(None, []), []) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test__id3util.py0000664000175000017500000001170100000000000017762 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import struct from tests import TestCase from mutagen.id3._util import BitPaddedInt, unsynch class BitPaddedIntTest(TestCase): def test_negative(self): self.assertRaises(ValueError, BitPaddedInt, -1) 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): self.assertEquals(BitPaddedInt(238).as_str(), BitPaddedInt(struct.pack('>L', 238)).as_str()) def test_varwidth(self): self.assertEquals(len(BitPaddedInt.to_str(100)), 4) self.assertEquals(len(BitPaddedInt.to_str(100, width=-1)), 4) self.assertEquals(len(BitPaddedInt.to_str(2 ** 32, width=-1)), 5) def test_minwidth(self): self.assertEquals( len(BitPaddedInt.to_str(100, width=-1, minwidth=6)), 6) def test_inval_input(self): self.assertRaises(TypeError, BitPaddedInt, None) def test_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') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test__iff.py0000664000175000017500000000033100000000000017146 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- from mutagen._iff import is_valid_chunk_id def test_is_valid_chunk_id(): assert not is_valid_chunk_id("") assert is_valid_chunk_id("QUUX") assert not is_valid_chunk_id("FOOBAR") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test__riff.py0000664000175000017500000001402500000000000017335 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from mutagen._riff import RiffFile, RiffChunk from tests import TestCase, DATA_DIR, get_temp_copy class TRiffFile(TestCase): has_tags = os.path.join(DATA_DIR, 'silence-2s-PCM-44100-16-ID3v23.wav') no_tags = os.path.join(DATA_DIR, 'silence-2s-PCM-16000-08-notags.wav') def setUp(self): self.file_1 = open(self.has_tags, 'rb') self.riff_1 = RiffFile(self.file_1) self.file_2 = open(self.no_tags, 'rb') self.riff_2 = RiffFile(self.file_2) self.tmp_1_name = get_temp_copy(self.has_tags) self.file_1_tmp = open(self.tmp_1_name, 'rb+') self.riff_1_tmp = RiffFile(self.file_1_tmp) self.tmp_2_name = get_temp_copy(self.no_tags) self.file_2_tmp = open(self.tmp_2_name, 'rb+') self.riff_2_tmp = RiffFile(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'fmt' in self.riff_1) self.failUnless(u'data' in self.riff_1) self.failUnless(u'id3' in self.riff_1) self.failUnless(u'fmt' in self.riff_2) self.failUnless(u'data' in self.riff_2) def test_is_chunks(self): self.failUnless(isinstance(self.riff_1[u'fmt'], RiffChunk)) self.failUnless(isinstance(self.riff_1[u'data'], RiffChunk)) self.failUnless(isinstance(self.riff_1[u'id3'], RiffChunk)) def test_chunk_size(self): self.failUnlessEqual(self.riff_1[u'data'].size, 352808) self.failUnlessEqual(self.riff_1[u'id3'].size, 376) self.failUnlessEqual(self.riff_2[u'data'].size, 64008) def test_chunk_data_size(self): self.failUnlessEqual(self.riff_1[u'data'].data_size, 352800) self.failUnlessEqual(self.riff_1[u'id3'].data_size, 368) self.failUnlessEqual(self.riff_2[u'data'].data_size, 64000) def test_RIFF_chunk_resize(self): self.riff_1_tmp[u'data'].resize(17000) self.failUnlessEqual( RiffFile(self.file_1_tmp)[u'data'].data_size, 17000) self.riff_2_tmp[u'data'].resize(0) self.failUnlessEqual(RiffFile(self.file_2_tmp)[u'data'].data_size, 0) def test_insert_chunk(self): self.riff_2_tmp.insert_chunk(u'id3') new_riff = RiffFile(self.file_2_tmp) self.failUnless(u'id3' in new_riff) self.failUnless(isinstance(new_riff[u'id3'], RiffChunk)) self.failUnlessEqual(new_riff[u'id3'].size, 8) self.failUnlessEqual(new_riff[u'id3'].data_size, 0) def test_insert_padded_chunks(self): padded = self.riff_2_tmp.insert_chunk(u'TST1') unpadded = self.riff_2_tmp.insert_chunk(u'TST2') # The second chunk needs no padding unpadded.resize(4) self.failUnlessEqual(4, unpadded.data_size) self.failUnlessEqual(0, unpadded.padding()) self.failUnlessEqual(12, unpadded.size) # Resize the first chunk so it needs padding padded.resize(3) self.failUnlessEqual(3, padded.data_size) self.failUnlessEqual(1, padded.padding()) self.failUnlessEqual(12, padded.size) self.failUnlessEqual(padded.offset + padded.size, unpadded.offset) # Verify the padding byte gets written correctly self.file_2_tmp.seek(padded.data_offset) self.file_2_tmp.write(b'ABCD') padded.write(b'ABC') self.file_2_tmp.seek(padded.data_offset) self.failUnlessEqual(b'ABC\x00', self.file_2_tmp.read(4)) # Verify the second chunk got not overwritten self.file_2_tmp.seek(unpadded.offset) self.failUnlessEqual(b'TST2', self.file_2_tmp.read(4)) def test_delete_padded_chunks(self): riff_file = self.riff_2_tmp self.failUnlessEqual(riff_file.root.size, 64044) riff_file.insert_chunk(u'TST') # Resize to odd length, should insert 1 padding byte riff_file[u'TST'].resize(3) # Insert another chunk after the first one self.failUnlessEqual(riff_file.root.size, 64056) riff_file.insert_chunk(u'TST2') riff_file[u'TST2'].resize(2) self.failUnlessEqual(riff_file.root.size, 64066) self.failUnlessEqual(riff_file[u'TST'].size, 12) self.failUnlessEqual(riff_file[u'TST'].data_size, 3) self.failUnlessEqual(riff_file[u'TST'].data_offset, 64052) self.failUnlessEqual(riff_file[u'TST2'].size, 10) self.failUnlessEqual(riff_file[u'TST2'].data_size, 2) self.failUnlessEqual(riff_file[u'TST2'].data_offset, 64064) # Delete the odd chunk riff_file.delete_chunk(u'TST') self.failUnlessEqual(riff_file.root.size, 64054) self.failUnlessEqual(riff_file[u'TST2'].size, 10) self.failUnlessEqual(riff_file[u'TST2'].data_size, 2) self.failUnlessEqual(riff_file[u'TST2'].data_offset, 64052) # Reloading the file should give the same results new_riff_file = RiffFile(self.file_2_tmp) self.failUnlessEqual(new_riff_file.root.size, riff_file.root.size) self.failUnlessEqual(new_riff_file[u'TST2'].size, riff_file[u'TST2'].size) self.failUnlessEqual(new_riff_file[u'TST2'].data_size, riff_file[u'TST2'].data_size) self.failUnlessEqual(new_riff_file[u'TST2'].data_offset, riff_file[u'TST2'].data_offset) def test_read_list_info(self): riff = self.riff_1_tmp info = riff[u'LIST'] self.failUnlessEqual(info.name, 'INFO') info_tags = {} for chunk in info.subchunks(): info_tags[chunk.id] = chunk.read().decode().strip('\0') self.failUnlessEqual(info_tags['IPRD'], 'Quod Libet Test Data') self.failUnlessEqual(info_tags['IART'], 'piman, jzig') self.failUnlessEqual(info_tags['IGNR'], 'Silence') self.failUnlessEqual(info_tags['INAM'], 'Silence') self.failUnlessEqual(info_tags['ITRK'], '02/10') self.failUnlessEqual(info_tags['ICRD'], '2004') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1595055359.0 mutagen-1.45.1/tests/test__util.py0000664000175000017500000010074700000000000017373 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- from mutagen._util import DictMixin, cdata, insert_bytes, delete_bytes, \ decode_terminated, dict_match, enum, get_size, BitReader, BitReaderError, \ resize_bytes, seek_end, verify_fileobj, fileobj_name, \ read_full, flags, resize_file, move_bytes, encode_endian, loadfile, \ intround, verify_filename from tests import TestCase, get_temp_empty import os import random import tempfile import errno import builtins from io import BytesIO try: import fcntl except ImportError: fcntl = None import pytest def test_intround(): assert intround(2.5) == 2 assert intround(2.6) == 3 assert intround(2.4) == 2 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) def test_iter(self): self.failUnlessEqual(list(iter(self.fdict)), ["foo"]) def test_clear(self): self.fdict.clear() self.rdict.clear() self.failIf(self.fdict) def test_keys(self): self.failUnlessEqual(list(self.fdict.keys()), list(self.rdict.keys())) self.failUnlessEqual( list(self.fdict.keys()), list(self.rdict.keys())) def test_values(self): self.failUnlessEqual( list(self.fdict.values()), list(self.rdict.values())) self.failUnlessEqual( list(self.fdict.values()), list(self.rdict.values())) def test_items(self): self.failUnlessEqual( list(self.fdict.items()), list(self.rdict.items())) self.failUnlessEqual( list(self.fdict.items()), list(self.rdict.items())) 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) assert cdata.int8_max == 2 ** 7 - 1 assert cdata.int8_min == - 2 ** 7 assert cdata.int8_max == cdata.char_max assert cdata.int8_min == cdata.char_min 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) assert cdata.uint8_max == 2 ** 8 - 1 assert cdata.uint8_min == 0 assert cdata.uint8_max == cdata.uchar_max assert cdata.uint8_min == cdata.uchar_min 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 Tresize_file(TestCase): def get_named_file(self, content): filename = get_temp_empty() h = open(filename, "wb+") h.write(content) h.seek(0) return h def test_resize(self): with self.get_named_file(b"") as h: resize_file(h, 0) self.assertEqual(os.path.getsize(h.name), 0) self.assertRaises(ValueError, resize_file, h, -1) resize_file(h, 1) self.assertEqual(os.path.getsize(h.name), 1) h.seek(0) self.assertEqual(h.read(), b"\x00") resize_file(h, 2 ** 17) self.assertEqual(os.path.getsize(h.name), 2 ** 17 + 1) h.seek(0) self.assertEqual(h.read(), b"\x00" * (2 ** 17 + 1)) def test_resize_content(self): with self.get_named_file(b"abc") as h: self.assertRaises(ValueError, resize_file, h, -4) resize_file(h, -1) h.seek(0) self.assertEqual(h.read(), b"ab") resize_file(h, 2) h.seek(0) self.assertEqual(h.read(), b"ab\x00\x00") def test_resize_dev_full(self): def raise_no_space(*args): raise IOError(errno.ENOSPC, os.strerror(errno.ENOSPC)) # fail on first write h = BytesIO(b"abc") h.write = raise_no_space self.assertRaises(IOError, resize_file, h, 1) h.seek(0, 2) self.assertEqual(h.tell(), 3) # fail on flush h = BytesIO(b"abc") h.flush = raise_no_space self.assertRaises(IOError, resize_file, h, 1) h.seek(0, 2) self.assertEqual(h.tell(), 3) class TMoveMixin(TestCase): MOVE = staticmethod(move_bytes) def file(self, contents): 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_basic(self): with self.file(b"abc123") as h: self.MOVE(h, 0, 1, 4) self.assertEqual(self.read(h), b"bc1223") with self.file(b"abc123") as h: self.MOVE(h, 1, 0, 4) self.assertEqual(self.read(h), b"aabc13") def test_invalid_params(self): with self.file(b"foo") as o: self.assertRaises(ValueError, self.MOVE, o, -1, 0, 0) self.assertRaises(ValueError, self.MOVE, o, 0, -1, 0) self.assertRaises(ValueError, self.MOVE, o, 0, 0, -1) def test_outside_file(self): with self.file(b"foo") as o: self.assertRaises(ValueError, self.MOVE, o, 0, 0, 4) self.assertRaises(ValueError, self.MOVE, o, 0, 1, 3) self.assertRaises(ValueError, self.MOVE, o, 1, 0, 3) def test_ok(self): with self.file(b"foo") as o: self.MOVE(o, 0, 1, 2) self.MOVE(o, 1, 0, 2) class FileHandling(TestCase): def file(self, contents): 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): with self.file(b'abcd') as o: resize_bytes(o, 2, 1, 1) self.assertEqual(self.read(o), b"abd") def test_resize_increase(self): with self.file(b'abcd') as o: resize_bytes(o, 2, 4, 1) self.assertEqual(self.read(o), b"abcd\x00d") def test_resize_nothing(self): with self.file(b'abcd') as o: resize_bytes(o, 2, 2, 1) self.assertEqual(self.read(o), b"abcd") def test_insert_into_empty(self): with self.file(b'') as o: insert_bytes(o, 8, 0) self.assertEqual(b'\x00' * 8, self.read(o)) def test_insert_before_one(self): with self.file(b'a') as o: insert_bytes(o, 8, 0) self.assertEqual(b'a' + b'\x00' * 7 + b'a', self.read(o)) def test_insert_after_one(self): with self.file(b'a') as o: insert_bytes(o, 8, 1) self.assertEqual(b'a' + b'\x00' * 8, self.read(o)) def test_insert_after_file(self): with self.file(b'a') as o: self.assertRaises(ValueError, insert_bytes, o, 1, 2) def test_smaller_than_file_middle(self): with self.file(b'abcdefghij') as o: insert_bytes(o, 4, 4) self.assertEqual(b'abcdefghefghij', self.read(o)) def test_smaller_than_file_to_end(self): with self.file(b'abcdefghij') as o: insert_bytes(o, 4, 6) self.assertEqual(b'abcdefghijghij', self.read(o)) def test_smaller_than_file_across_end(self): with self.file(b'abcdefghij') as o: insert_bytes(o, 4, 8) self.assertEqual(b'abcdefghij\x00\x00ij', self.read(o)) def test_smaller_than_file_at_end(self): with self.file(b'abcdefghij') as o: insert_bytes(o, 3, 10) self.assertEqual(b'abcdefghij\x00\x00\x00', self.read(o)) def test_smaller_than_file_at_beginning(self): with self.file(b'abcdefghij') as o: insert_bytes(o, 3, 0) self.assertEqual(b'abcabcdefghij', self.read(o)) def test_zero(self): with self.file(b'abcdefghij') as o: insert_bytes(o, 0, 1) self.assertEqual(b'abcdefghij', self.read(o)) def test_negative(self): with self.file(b'abcdefghij') as o: self.assertRaises(ValueError, insert_bytes, o, 8, -1) def test_delete_one(self): with self.file(b'a') as o: delete_bytes(o, 1, 0) self.assertEqual(b'', self.read(o)) def test_delete_first_of_two(self): with self.file(b'ab') as o: delete_bytes(o, 1, 0) self.assertEqual(b'b', self.read(o)) def test_delete_second_of_two(self): with self.file(b'ab') as o: delete_bytes(o, 1, 1) self.assertEqual(b'a', self.read(o)) def test_delete_third_of_two(self): with self.file(b'ab') as o: self.assertRaises(ValueError, delete_bytes, o, 1, 2) def test_delete_middle(self): with self.file(b'abcdefg') as o: delete_bytes(o, 3, 2) self.assertEqual(b'abfg', self.read(o)) def test_delete_across_end(self): with self.file(b'abcdefg') as o: self.assertRaises(ValueError, delete_bytes, o, 4, 8) def test_delete_zero(self): with self.file(b'abcdefg') as o: delete_bytes(o, 0, 3) self.assertEqual(b'abcdefg', self.read(o)) def test_delete_negative(self): with self.file(b'abcdefg') as o: self.assertRaises(ValueError, 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. data = u''.join(map(str, range(12574))) # 51760 bytes data = data.encode("ascii") with self.file(data) as o: 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. data = u''.join(map(str, range(12574))) # 51760 bytes data = data.encode("ascii") with self.file(data[:6106 + 79] + data[79:]) as o: delete_bytes(o, 6106, 79) self.failUnless(data == self.read(o)) # Generate a bunch of random insertions, apply them, delete them, # and make sure everything is still correct. # # The num_runs and num_changes values are tuned to take about 10s # on my laptop, or about 30 seconds since we we have 3 variations # on insert/delete_bytes brokenness. If I ever get a faster # laptop, it's probably a good idea to increase them. :) def test_many_changes(self, num_runs=5, num_changes=300, min_change_size=500, max_change_size=1000, min_buffer_size=1, max_buffer_size=2000): self.failUnless(min_buffer_size < min_change_size and max_buffer_size > max_change_size and min_change_size < max_change_size and min_buffer_size < max_buffer_size, "Given testing parameters make this test useless") for j in range(num_runs): data = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ" * 1024 with self.file(data) as fobj: filesize = len(data) # Generate the list of changes to apply changes = [] for i in range(num_changes): change_size = random.randrange( min_change_size, max_change_size) change_offset = random.randrange(0, filesize) filesize += change_size changes.append((change_offset, change_size)) # Apply the changes, and make sure they all took. for offset, size in changes: buffer_size = random.randrange( min_buffer_size, max_buffer_size) insert_bytes(fobj, size, offset, BUFFER_SIZE=buffer_size) fobj.seek(0) self.failIfEqual(fobj.read(len(data)), data) fobj.seek(0, 2) self.failUnlessEqual(fobj.tell(), filesize) # Then, undo them. changes.reverse() for offset, size in changes: buffer_size = random.randrange( min_buffer_size, max_buffer_size) delete_bytes(fobj, size, offset, BUFFER_SIZE=buffer_size) fobj.seek(0) self.failUnless(fobj.read() == data) 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 Tflags(TestCase): def test_enum(self): @flags class Foo(object): FOO = 1 BAR = 2 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)), "") self.assertEqual(str(Foo(42)), "Foo.BAR | 40") self.assertEqual(int(Foo(42)), 42) self.assertEqual(str(Foo(1)), "Foo.FOO") self.assertEqual(int(Foo(1)), 1) self.assertEqual(str(Foo(0)), "0") self.assertTrue(isinstance(str(Foo.FOO), str)) self.assertTrue(isinstance(repr(Foo.FOO), str)) class Tverify_fileobj(TestCase): def test_verify_fileobj_fail(self): self.assertRaises(ValueError, verify_fileobj, object()) with tempfile.TemporaryFile(mode="rb") as h: self.assertRaises(ValueError, verify_fileobj, h, writable=True) def test_verify_fileobj(self): with tempfile.TemporaryFile(mode="rb") as h: verify_fileobj(h) with tempfile.TemporaryFile(mode="rb+") as h: verify_fileobj(h, writable=True) class Tfileobj_name(TestCase): def test_fileobj_name_other_type(self): class Foo(object): name = 123 self.assertEqual(fileobj_name(Foo()), "123") def test_fileobj_name(self): with tempfile.TemporaryFile(mode="rb") as h: self.assertEqual(fileobj_name(h), str(h.name)) class Tseek_end(TestCase): def file(self, contents): temp = tempfile.TemporaryFile() temp.write(contents) temp.flush() temp.seek(0) return temp def test_seek_end(self): with self.file(b"foo") as f: seek_end(f, 2) self.assertEqual(f.tell(), 1) seek_end(f, 3) self.assertEqual(f.tell(), 0) seek_end(f, 4) self.assertEqual(f.tell(), 0) seek_end(f, 0) self.assertEqual(f.tell(), 3) self.assertRaises(ValueError, seek_end, f, -1) def test_seek_end_pos(self): with self.file(b"foo") as f: f.seek(10) seek_end(f, 10) self.assertEqual(f.tell(), 0) class Tloadfile(TestCase): def test_handle_readwrite_notsup(self): @loadfile(method=False, writable=True) def file_func(filething): fileobj = filething.fileobj assert fileobj.read(3) == b"foo" fileobj.seek(0, 2) fileobj.write(b"bar") # first a normal test filename = get_temp_empty() try: with open(filename, "wb") as h: h.write(b"foo") file_func(filename) with open(filename, "rb") as h: assert h.read() == b"foobar" finally: os.unlink(filename) # now we mock open to return raise EOPNOTSUPP in case of mixed mode. # things should still work since we edit the file in memory raised = [] old_open = open def mock_open(name, mode, *args): if "+" in mode: raised.append(True) raise IOError(errno.EOPNOTSUPP, "nope") return old_open(name, mode, *args) builtins.open = mock_open try: filename = get_temp_empty() try: with open(filename, "wb") as h: h.write(b"foo") file_func(filename) with open(filename, "rb") as h: assert h.read() == b"foobar" finally: os.unlink(filename) finally: builtins.open = old_open assert raised def test_filename_from_fspath(self): class FilePath(object): def __init__(self, filename): self.filename = filename def __fspath__(self): return self.filename @loadfile(method=False, writable=True) def file_func(filething): fileobj = filething.fileobj assert fileobj.read(3) == b"foo" fileobj.seek(0, 2) fileobj.write(b"bar") filename = get_temp_empty() try: with open(filename, "wb") as h: h.write(b"foo") file_func(FilePath(filename)) with open(filename, "rb") as h: assert h.read() == b"foobar" finally: os.unlink(filename) with pytest.raises(TypeError, match=r'.*__fspath__.*'): file_func(FilePath(42)) class Tread_full(TestCase): def test_read_full(self): fileobj = BytesIO() self.assertRaises(ValueError, read_full, fileobj, -3) self.assertRaises(IOError, read_full, fileobj, 3) class Tget_size(TestCase): def test_get_size(self): f = BytesIO(b"foo") f.seek(1, 0) self.assertEqual(f.tell(), 1) self.assertEqual(get_size(f), 3) self.assertEqual(f.tell(), 1) class Tencode_endian(TestCase): def test_other(self): assert encode_endian(u"\xe4", "latin-1") == b"\xe4" assert encode_endian(u"\xe4", "utf-8") == b"\xc3\xa4" with self.assertRaises(LookupError): encode_endian(u"", "nopenope") with self.assertRaises(UnicodeEncodeError): assert encode_endian(u"\u2714", "latin-1") assert encode_endian(u"\u2714", "latin-1", "replace") == b"?" def test_utf_16(self): assert encode_endian(u"\xe4", "utf-16", le=True) == b"\xff\xfe\xe4\x00" assert encode_endian(u"\xe4", "utf-16-le") == b"\xe4\x00" assert encode_endian( u"\xe4", "utf-16", le=False) == b"\xfe\xff\x00\xe4" assert encode_endian(u"\xe4", "utf-16-be") == b"\x00\xe4" def test_utf_32(self): assert encode_endian(u"\xe4", "utf-32", le=True) == \ b"\xff\xfe\x00\x00\xe4\x00\x00\x00" assert encode_endian(u"\xe4", "utf-32-le") == b"\xe4\x00\x00\x00" assert encode_endian( u"\xe4", "utf-32", le=False) == b"\x00\x00\xfe\xff\x00\x00\x00\xe4" assert encode_endian(u"\xe4", "utf-32-be") == b"\x00\x00\x00\xe4" 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") self.assertRaises( ValueError, decode_terminated, b"\xff\xfea\x00", "utf-16") # 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 range(64): fo = BytesIO(data) r = BitReader(fo) v = r.bits(i) << (64 - i) | r.bits(64 - i) self.assertEqual(v, ref) def test_bits_null(self): r = BitReader(BytesIO(b"")) self.assertEqual(r.bits(0), 0) def test_bits_error(self): r = BitReader(BytesIO(b"")) self.assertRaises(ValueError, r.bits, -1) def test_bytes_error(self): r = BitReader(BytesIO(b"")) self.assertRaises(ValueError, r.bytes, -1) def test_skip_error(self): r = BitReader(BytesIO(b"")) self.assertRaises(ValueError, r.skip, -1) def test_read_too_much(self): r = BitReader(BytesIO(b"")) self.assertEqual(r.bits(0), 0) self.assertRaises(BitReaderError, r.bits, 1) def test_skip(self): r = BitReader(BytesIO(b"\xEF")) r.skip(4) self.assertEqual(r.bits(4), 0xf) def test_skip_more(self): r = BitReader(BytesIO(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(BytesIO(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(BytesIO(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(BytesIO(b"\xAB\xCD\xEF")) r.skip(4) self.assertEqual(r.bytes(2), b"\xBC\xDE") def test_get_position(self): r = BitReader(BytesIO(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(BytesIO(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(BytesIO(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()) class Tverify_filename(TestCase): def test_verify_filename_fail(self): self.assertRaises(ValueError, verify_filename, object()) def test_verify_filename(self): class FilePath(object): def __init__(self, filename): self.filename = filename def __fspath__(self): return self.filename verify_filename(FilePath("foo")) verify_filename("foo") verify_filename(b"foo") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test__vorbis.py0000664000175000017500000002160000000000000017710 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- from tests import TestCase from mutagen._vorbis import VComment, VCommentDict, istag, error 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")) 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(error, VComment, b"foo") def test_unset_framing_bit(self): self.failUnlessRaises( error, 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")) self.failUnlessRaises(ValueError, 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(error, 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], str)) 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(error, 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") 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/test_aac.py0000644000175000017500000000566400000000000017003 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from mutagen.id3 import ID3, TIT1 from mutagen.aac import AAC, AACError from tests import TestCase, DATA_DIR, get_temp_copy class TADTS(TestCase): def setUp(self): original = os.path.join(DATA_DIR, "empty.aac") self.filename = get_temp_copy(original) 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") self.filename = get_temp_copy(original) 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()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574016271.0 mutagen-1.45.1/tests/test_ac3.py0000644000175000017500000000270600000000000016717 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from mutagen.ac3 import AC3, AC3Error from tests import TestCase, DATA_DIR class TAC3(TestCase): def setUp(self): self.ac3 = AC3(os.path.join(DATA_DIR, "silence-44-s.ac3")) self.eac3 = AC3(os.path.join(DATA_DIR, "silence-44-s.eac3")) def test_channels(self): self.failUnlessEqual(self.ac3.info.channels, 2) self.failUnlessEqual(self.eac3.info.channels, 2) def test_bitrate(self): self.failUnlessEqual(self.ac3.info.bitrate, 192000) self.failUnlessAlmostEqual(self.eac3.info.bitrate, 192000, delta=500) def test_sample_rate(self): self.failUnlessEqual(self.ac3.info.sample_rate, 44100) self.failUnlessEqual(self.eac3.info.sample_rate, 44100) def test_length(self): self.failUnlessAlmostEqual(self.ac3.info.length, 3.70, delta=0.009) self.failUnlessAlmostEqual(self.eac3.info.length, 3.70, delta=0.009) def test_type(self): self.failUnlessEqual(self.ac3.info.codec, "ac-3") self.failUnlessEqual(self.eac3.info.codec, "ec-3") def test_not_my_file(self): self.failUnlessRaises( AC3Error, AC3, os.path.join(DATA_DIR, "empty.ogg")) self.failUnlessRaises( AC3Error, AC3, os.path.join(DATA_DIR, "silence-44-s.mp3")) def test_pprint(self): self.assertTrue("ac-3" in self.ac3.pprint()) self.assertTrue("ec-3" in self.eac3.pprint()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_aiff.py0000664000175000017500000003216500000000000017162 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from io import BytesIO from mutagen.aiff import AIFF, AIFFInfo, delete, AIFFFile, AIFFChunk from mutagen.aiff import error as AIFFError, read_float from mutagen._iff import error as IffError from tests import TestCase, DATA_DIR, get_temp_copy import pytest 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): self.filename_1 = get_temp_copy(self.has_tags) self.filename_2 = get_temp_copy(self.no_tags) 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_read_float(self): assert read_float(b'@\x0b\xfa\x00\x00\x00\x00\x00\x00\x00') == 8000.0 with pytest.raises(OverflowError): read_float(b"\xfa\x00\x00\xfa\x00\x00\x00\x00\x00\x00") with pytest.raises(OverflowError): read_float(b"\x7f\xff\x00\xfa\x00\x00\x00\x00\x00\x00") 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_bits_per_sample(self): self.failUnlessEqual(self.aiff_1.info.bits_per_sample, 16) self.failUnlessEqual(self.aiff_2.info.bits_per_sample, 16) self.failUnlessEqual(self.aiff_3.info.bits_per_sample, 16) self.failUnlessEqual(self.aiff_4.info.bits_per_sample, 16) self.failUnlessEqual(self.aiff_5.info.bits_per_sample, 16) def test_sample_size(self): for test in [self.aiff_1, self.aiff_2, self.aiff_3, self.aiff_4, self.aiff_5]: info = test.info self.failUnlessEqual(info.sample_size, info.bits_per_sample) def test_notaiff(self): self.failUnlessRaises( IffError, 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 = AIFFFile(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(IffError, 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 = BytesIO(b"") self.failUnlessRaises(IffError, AIFFInfo, fileobj) class TAIFFFile(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 = AIFFFile(self.file_1) self.file_2 = open(self.no_tags, 'rb') self.iff_2 = AIFFFile(self.file_2) self.tmp_1_name = get_temp_copy(self.has_tags) self.file_1_tmp = open(self.tmp_1_name, 'rb+') self.iff_1_tmp = AIFFFile(self.file_1_tmp) self.tmp_2_name = get_temp_copy(self.no_tags) self.file_2_tmp = open(self.tmp_2_name, 'rb+') self.iff_2_tmp = AIFFFile(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'], AIFFChunk)) self.failUnless(isinstance(self.iff_1[u'COMM'], AIFFChunk)) self.failUnless(isinstance(self.iff_1[u'SSND'], AIFFChunk)) self.failUnless(isinstance(self.iff_1[u'ID3'], AIFFChunk)) 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( AIFFFile(self.file_1_tmp)[u'FORM'].data_size, 17000) self.iff_2_tmp[u'FORM'].resize(4) self.failUnlessEqual(AIFFFile(self.file_2_tmp)[u'FORM'].data_size, 4) 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(AIFFFile(self.file_1_tmp)[u'ID3'].data_size, 128) self.failUnlessEqual( AIFFFile(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 AIFFFile(self.file_1_tmp)) self.failUnlessEqual(AIFFFile(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 AIFFFile(self.file_2_tmp)) self.failUnlessEqual(AIFFFile(self.file_2_tmp)[u'FORM'].size, 38) def test_insert_chunk(self): self.iff_2_tmp.insert_chunk(u'ID3') new_iff = AIFFFile(self.file_2_tmp) self.failUnless(u'ID3' in new_iff) self.failUnless(isinstance(new_iff[u'ID3'], AIFFChunk)) 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) def test_insert_padded_chunks(self): padded = self.iff_2_tmp.insert_chunk(u'TST1') unpadded = self.iff_2_tmp.insert_chunk(u'TST2') # The second chunk needs no padding unpadded.resize(4) self.failUnlessEqual(4, unpadded.data_size) self.failUnlessEqual(0, unpadded.padding()) self.failUnlessEqual(12, unpadded.size) # Resize the first chunk so it needs padding padded.resize(3) self.failUnlessEqual(3, padded.data_size) self.failUnlessEqual(1, padded.padding()) self.failUnlessEqual(12, padded.size) self.failUnlessEqual(padded.offset + padded.size, unpadded.offset) # Verify the padding byte gets written correctly self.file_2_tmp.seek(padded.data_offset) self.file_2_tmp.write(b'ABCD') padded.write(b'ABC') self.file_2_tmp.seek(padded.data_offset) self.failUnlessEqual(b'ABC\x00', self.file_2_tmp.read(4)) # Verify the second chunk got not overwritten self.file_2_tmp.seek(unpadded.offset) self.failUnlessEqual(b'TST2', self.file_2_tmp.read(4)) def test_delete_padded_chunks(self): iff_file = self.iff_2_tmp iff_file.insert_chunk(u'TST') # Resize to odd length, should insert 1 padding byte iff_file[u'TST'].resize(3) # Insert another chunk after the first one iff_file.insert_chunk(u'TST2') iff_file[u'TST2'].resize(2) self.failUnlessEqual(iff_file[u'FORM'].size, 16076) self.failUnlessEqual(iff_file[u'FORM'].data_size, 16068) self.failUnlessEqual(iff_file[u'TST'].size, 12) self.failUnlessEqual(iff_file[u'TST'].data_size, 3) self.failUnlessEqual(iff_file[u'TST'].data_offset, 16062) self.failUnlessEqual(iff_file[u'TST2'].size, 10) self.failUnlessEqual(iff_file[u'TST2'].data_size, 2) self.failUnlessEqual(iff_file[u'TST2'].data_offset, 16074) # Delete the odd chunk iff_file.delete_chunk(u'TST') self.failUnlessEqual(iff_file[u'FORM'].size, 16064) self.failUnlessEqual(iff_file[u'FORM'].data_size, 16056) self.failUnlessEqual(iff_file[u'TST2'].size, 10) self.failUnlessEqual(iff_file[u'TST2'].data_size, 2) self.failUnlessEqual(iff_file[u'TST2'].data_offset, 16062) # Reloading the file should give the same results new_iff_file = AIFFFile(self.file_2_tmp) self.failUnlessEqual(new_iff_file[u'FORM'].size, iff_file[u'FORM'].size) self.failUnlessEqual(new_iff_file[u'TST2'].size, iff_file[u'TST2'].size) self.failUnlessEqual(new_iff_file[u'TST2'].data_size, iff_file[u'TST2'].data_size) self.failUnlessEqual(new_iff_file[u'TST2'].data_offset, iff_file[u'TST2'].data_offset) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_apev2.py0000664000175000017500000003262500000000000017273 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from tests import TestCase, DATA_DIR, get_temp_copy, get_temp_empty import mutagen.apev2 from mutagen import MutagenError 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") 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)) def test_py3(self): self.assertRaises(TypeError, is_valid_apev2_key, b"abc") class TAPEInvalidItemCount(TestCase): # https://github.com/quodlibet/mutagen/issues/145 def test_load(self): x = mutagen.apev2.APEv2( os.path.join(DATA_DIR, "145-invalid-item-count.apev2")) self.failUnlessEqual(len(x.keys()), 17) class TAPEWriter(TestCase): offset = 0 def setUp(self): self.sample_new = get_temp_copy(SAMPLE) self.broken_new = get_temp_copy(BROKEN) 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(self.sample_new) self.just_tag = get_temp_empty() tag.save(self.just_tag) self.tag_at_start = get_temp_empty() tag.save(self.tag_at_start) with open(self.tag_at_start, "ab") as fileobj: fileobj.write(b"tag garbage" * 1000) self.tag = mutagen.apev2.APEv2(self.sample_new) def tearDown(self): os.unlink(self.sample_new) os.unlink(self.broken_new) os.unlink(self.just_tag) os.unlink(self.tag_at_start) def test_changed(self): size = os.path.getsize(self.sample_new) self.tag.save() self.failUnlessEqual( os.path.getsize(self.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(self.broken_new) self.failUnlessEqual( os.path.getsize(OLD), os.path.getsize(self.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(self.sample_new), os.path.getsize(SAMPLE) + os.path.getsize(self.just_tag)) def test_delete(self): mutagen.apev2.delete(self.just_tag) tag = mutagen.apev2.APEv2(self.sample_new) tag.delete() self.failUnlessEqual(os.path.getsize(self.just_tag), self.offset) self.failUnlessEqual(os.path.getsize(SAMPLE) + self.offset, os.path.getsize(self.sample_new)) self.failIf(tag) def test_empty(self): self.failUnlessRaises( APEv2Error, mutagen.apev2.APEv2, os.path.join(DATA_DIR, "emptyfile.mp3")) def test_tag_at_start(self): tag = mutagen.apev2.APEv2(self.tag_at_start) self.failUnlessEqual(tag["album"], "Mutagen tests") def test_tag_at_start_write(self): filename = self.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(self.just_tag), os.path.getsize(filename) - (len("tag garbage") * 1000)) def test_tag_at_start_delete(self): filename = self.tag_at_start tag = mutagen.apev2.APEv2(filename) tag.delete() self.failUnlessRaises(APEv2Error, mutagen.apev2.APEv2, filename) self.failUnlessEqual( os.path.getsize(filename), len("tag garbage") * 1000) def test_case_preservation(self): mutagen.apev2.delete(self.just_tag) tag = mutagen.apev2.APEv2(self.sample_new) tag["FoObaR"] = "Quux" tag.save() tag = mutagen.apev2.APEv2(self.sample_new) self.failUnless("FoObaR" in tag.keys()) self.failIf("foobar" in tag.keys()) def test_unicode_key(self): # https://github.com/quodlibet/mutagen/issues/123 tag = mutagen.apev2.APEv2(self.sample_new) tag["abc"] = u'\xf6\xe4\xfc' tag[u"cba"] = "abc" tag.save() def test_save_sort_is_deterministic(self): tag = mutagen.apev2.APEv2(self.sample_new) tag["cba"] = "my cba value" tag["abc"] = "my abc value" tag.save() with open(self.sample_new, 'rb') as fobj: content = fobj.read() self.assertTrue(content.index(b"abc") < content.index(b"cba")) class TAPEv2ThenID3v1Writer(TAPEWriter): offset = 128 def setUp(self): super(TAPEv2ThenID3v1Writer, self).setUp() with open(self.sample_new, "ab+") as f: f.write(b"TAG" + b"\x00" * 125) with open(self.broken_new, "ab+") as f: f.write(b"TAG" + b"\x00" * 125) with open(self.just_tag, "ab+") as f: f.write(b"TAG" + b"\x00" * 125) def test_tag_at_start_write(self): pass class TAPEv2(TestCase): def setUp(self): self.filename = get_temp_copy(OLD) self.audio = APEv2(self.filename) def tearDown(self): os.unlink(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(MutagenError, mutagen.apev2.APEv2, "dne") def test_no_tag(self): self.failUnlessRaises(MutagenError, 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(zip(self.audio.keys(), self.audio.values()))) def test_key_type(self): key = self.audio.keys()[0] self.assertTrue(isinstance(key, str)) 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()) 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( os.path.join(DATA_DIR, "apev2-lyricsv2.mp3")) 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(str(self.TV(u"foo")), u"foo") def test_list(self): self.failUnlessEqual(self.sample, list(self.value)) def test_setitem_list(self): self.value[2] = self.sample[2] = 'quux' self.test_list() self.test_getitem() self.value[2] = self.sample[2] = 'baz' def test_getitem(self): for i in range(len(self.value)): self.failUnlessEqual(self.sample[i], self.value[i]) def test_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") self.assertRaises(TypeError, self.value.insert, 2, b"abc") def test_types(self): 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(str(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) 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() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_asf.py0000664000175000017500000005566600000000000017041 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os import warnings from io import BytesIO 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, \ HeaderObject from mutagen.asf import ASFUnicodeAttribute, ASFError, ASFByteArrayAttribute, \ ASFBoolAttribute, ASFDWordAttribute, ASFQWordAttribute, ASFWordAttribute, \ ASFGUIDAttribute from tests import TestCase, DATA_DIR, get_temp_copy 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(), str)) class TASF(TestCase): def setUp(self): self.filename = get_temp_copy(self.original) 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 zip(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): 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__ 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): self.assertRaises(TypeError, ASFUnicodeAttribute, b"\xff") self.assertRaises(ASFError, ASFUnicodeAttribute, data=b"\x00") self.assertEqual(ASFUnicodeAttribute(u"foo").value, u"foo") assert ASFUnicodeAttribute(data=b"") == u"" def test_ASFUnicodeAttribute_dunder(self): attr = ASFUnicodeAttribute(u"foo") self.assertEqual(bytes(attr), b"f\x00o\x00o\x00") self.assertEqual(str(attr), u"foo") self.assertEqual(repr(attr), "ASFUnicodeAttribute('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(str(attr), u"[binary data (1 bytes)]") self.assertEqual(repr(attr), r"ASFByteArrayAttribute(b'\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") self.assertEqual(str(attr), u"b'\\xff'") self.assertEqual(repr(attr), "ASFGUIDAttribute(b'\\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(str(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(str(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(str(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(str(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): self.filename = get_temp_copy(self.original) 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 TASFObjects(TestCase): filename = os.path.join(DATA_DIR, "silence-1.wma") def test_invalid_header(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") asf = ASF() fileobj = BytesIO( b"0&\xb2u\x8ef\xcf\x11\xa6\xd9\x00\xaa\x00b\xcel\x19\xbf\x01\x00" b"\x00\x00\x00\x00\x07\x00\x00\x00\x01\x02") self.assertRaises( ASFHeaderError, HeaderObject.parse_full, asf, fileobj) class TASFAttrDest(TestCase): original = os.path.join(DATA_DIR, "silence-1.wma") def setUp(self): self.filename = get_temp_copy(self.original) 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_str(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): self.filename = get_temp_copy(self.original) 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): # https://github.com/quodlibet/mutagen/issues/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): # https://github.com/quodlibet/mutagen/issues/81#issuecomment-207014936 original = os.path.join(DATA_DIR, "silence-1.wma") def setUp(self): self.filename = get_temp_copy(self.original) 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_dsdiff.py0000664000175000017500000000520400000000000017506 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from mutagen.dsdiff import DSDIFF, IffError from tests import TestCase, DATA_DIR, get_temp_copy class TDSDIFF(TestCase): silence_1 = os.path.join(DATA_DIR, '2822400-1ch-0s-silence.dff') silence_2 = os.path.join(DATA_DIR, '5644800-2ch-s01-silence.dff') silence_dst = os.path.join(DATA_DIR, '5644800-2ch-s01-silence-dst.dff') def setUp(self): self.dff_1 = DSDIFF(self.silence_1) self.dff_2 = DSDIFF(self.silence_2) self.dff_dst = DSDIFF(self.silence_dst) self.dff_id3 = DSDIFF(get_temp_copy(self.silence_dst)) self.dff_no_id3 = DSDIFF(get_temp_copy(self.silence_2)) def test_channels(self): self.failUnlessEqual(self.dff_1.info.channels, 1) self.failUnlessEqual(self.dff_2.info.channels, 2) self.failUnlessEqual(self.dff_dst.info.channels, 2) def test_length(self): self.failUnlessEqual(self.dff_1.info.length, 0) self.failUnlessEqual(self.dff_2.info.length, 0.01) self.failUnlessEqual(self.dff_dst.info.length, 0) def test_sampling_frequency(self): self.failUnlessEqual(self.dff_1.info.sample_rate, 2822400) self.failUnlessEqual(self.dff_2.info.sample_rate, 5644800) self.failUnlessEqual(self.dff_dst.info.sample_rate, 5644800) def test_bits_per_sample(self): self.failUnlessEqual(self.dff_1.info.bits_per_sample, 1) def test_bitrate(self): self.failUnlessEqual(self.dff_1.info.bitrate, 2822400) self.failUnlessEqual(self.dff_2.info.bitrate, 11289600) self.failUnlessEqual(self.dff_dst.info.bitrate, 0) def test_notdsf(self): self.failUnlessRaises(IffError, DSDIFF, os.path.join( DATA_DIR, '2822400-1ch-0s-silence.dsf')) def test_pprint(self): self.failUnless(self.dff_1.pprint()) def test_mime(self): self.failUnless("audio/x-dff" in self.dff_1.mime) def test_update_tags(self): from mutagen.id3 import TIT1 tags = self.dff_id3.tags tags.add(TIT1(encoding=3, text="foobar")) tags.save() new = DSDIFF(self.dff_id3.filename) self.failUnlessEqual(new["TIT1"], ["foobar"]) def test_delete_tags(self): self.dff_id3.tags.delete() new = DSDIFF(self.dff_id3.filename) self.failUnlessEqual(new.tags, None) def test_save_tags(self): from mutagen.id3 import TIT1 self.dff_no_id3.add_tags() tags = self.dff_no_id3.tags tags.add(TIT1(encoding=3, text="foobar")) tags.save(self.dff_no_id3.filename) new = DSDIFF(self.dff_no_id3.filename) self.failUnlessEqual(new["TIT1"], ["foobar"]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/test_dsf.py0000644000175000017500000000774500000000000017035 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from mutagen.dsf import DSF, DSFFile, delete from mutagen.dsf import error as DSFError from tests import TestCase, DATA_DIR, get_temp_copy class TDSF(TestCase): silence_1 = os.path.join(DATA_DIR, '2822400-1ch-0s-silence.dsf') silence_2 = os.path.join(DATA_DIR, '5644800-2ch-s01-silence.dsf') has_tags = os.path.join(DATA_DIR, 'with-id3.dsf') no_tags = os.path.join(DATA_DIR, 'without-id3.dsf') def setUp(self): self.filename_1 = get_temp_copy(self.has_tags) self.filename_2 = get_temp_copy(self.no_tags) self.dsf_tmp_id3 = DSF(self.filename_1) self.dsf_tmp_no_id3 = DSF(self.filename_2) self.dsf_1 = DSF(self.silence_1) self.dsf_2 = DSF(self.silence_2) def test_channels(self): self.failUnlessEqual(self.dsf_1.info.channels, 1) self.failUnlessEqual(self.dsf_2.info.channels, 2) def test_length(self): self.failUnlessEqual(self.dsf_1.info.length, 0) self.failUnlessEqual(self.dsf_2.info.length, 0.01) def test_sampling_frequency(self): self.failUnlessEqual(self.dsf_1.info.sample_rate, 2822400) self.failUnlessEqual(self.dsf_2.info.sample_rate, 5644800) def test_bits_per_sample(self): self.failUnlessEqual(self.dsf_1.info.bits_per_sample, 1) def test_notdsf(self): self.failUnlessRaises( DSFError, DSF, os.path.join(DATA_DIR, 'empty.ofr')) def test_pprint(self): self.failUnless(self.dsf_tmp_id3.pprint()) def test_delete(self): self.dsf_tmp_id3.delete() self.failIf(self.dsf_tmp_id3.tags) self.failUnless(DSF(self.filename_1).tags is None) def test_module_delete(self): delete(self.filename_1) self.failUnless(DSF(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.dsf_tmp_id3.tags = None self.failUnless(self.dsf_tmp_id3.pprint()) def test_save_no_tags(self): self.dsf_tmp_id3.tags = None self.dsf_tmp_id3.save() self.assertTrue(self.dsf_tmp_id3.tags is None) def test_add_tags_already_there(self): self.failUnless(self.dsf_tmp_id3.tags) self.failUnlessRaises(Exception, self.dsf_tmp_id3.add_tags) def test_mime(self): self.failUnless("audio/dsf" in self.dsf_tmp_id3.mime) def test_loaded_tags(self): self.failUnless(self.dsf_tmp_id3["TIT2"] == "DSF title") def test_roundtrip(self): self.failUnlessEqual(self.dsf_tmp_id3["TIT2"], ["DSF title"]) self.dsf_tmp_id3.save() new = DSF(self.dsf_tmp_id3.filename) self.failUnlessEqual(new["TIT2"], ["DSF title"]) def test_save_tags(self): from mutagen.id3 import TIT2 tags = self.dsf_tmp_id3.tags tags.add(TIT2(encoding=3, text="foobar")) tags.save() new = DSF(self.dsf_tmp_id3.filename) self.failUnlessEqual(new["TIT2"], ["foobar"]) def test_corrupt_tag(self): with open(self.filename_1, "r+b") as h: chunk = DSFFile(h).dsd_chunk h.seek(chunk.offset_metdata_chunk) h.seek(4, 1) h.write(b"\xff\xff") self.assertRaises(DSFError, DSF, self.filename_1) def test_padding(self): DSF(self.filename_1).save() self.assertEqual(DSF(self.filename_1).tags._padding, 1024) DSF(self.filename_1).save() self.assertEqual(DSF(self.filename_1).tags._padding, 1024) tags = DSF(self.filename_1) tags.save(padding=lambda x: 1) self.assertEqual(DSF(self.filename_1).tags._padding, 1) tags = DSF(self.filename_1) tags.save(padding=lambda x: 100) self.assertEqual(DSF(self.filename_1).tags._padding, 100) tags = DSF(self.filename_1) self.assertRaises(DSFError, tags.save, padding=lambda x: -1) def tearDown(self): os.unlink(self.filename_1) os.unlink(self.filename_2) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_easyid3.py0000664000175000017500000003707600000000000017624 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os import pickle from mutagen import MutagenError from mutagen.id3 import ID3FileType, ID3, RVA2, CHAP, TDRC, CTOC from mutagen.easyid3 import EasyID3, error as ID3Error from tests import TestCase, DATA_DIR, get_temp_copy class TEasyID3(TestCase): def setUp(self): self.filename = get_temp_copy(os.path.join(DATA_DIR, 'emptyfile.mp3')) self.id3 = EasyID3() self.realid3 = self.id3._EasyID3__id3 def tearDown(self): os.unlink(self.filename) def test_size_attr(self): assert self.id3.size == self.realid3.size def test_load_filename(self): self.id3.save(self.filename) self.id3.load(self.filename) assert self.id3.filename == self.filename path = os.path.join(DATA_DIR, 'silence-44-s.mp3') new = EasyID3(path) assert new.filename == path def test_txxx_latin_first_then_non_latin(self): self.id3["performer"] = [u"foo"] self.id3["performer"] = [u"\u0243"] self.id3.save(self.filename) new = EasyID3(self.filename) self.assertEqual(new["performer"], [u"\u0243"]) 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_save_23(self): self.id3.save(self.filename, v2_version=3) self.assertEqual(ID3(self.filename).version, (2, 3, 0)) self.id3.save(self.filename, v2_version=4) self.assertEqual(ID3(self.filename).version, (2, 4, 0)) def test_save_date_v23(self): self.id3["date"] = "2004" assert self.realid3.getall("TDRC")[0] == u"2004" self.id3.save(self.filename, v2_version=3) assert self.realid3.getall("TDRC")[0] == u"2004" assert not self.realid3.getall("TYER") new = ID3(self.filename, translate=False) assert new.version == (2, 3, 0) assert new.getall("TYER")[0] == u"2004" def test_save_v23_error_restore(self): self.id3["date"] = "2004" with self.assertRaises(MutagenError): self.id3.save("", v2_version=3) assert self.id3["date"] == ["2004"] def test_save_v23_recurse_restore(self): self.realid3.add(CHAP(sub_frames=[TDRC(text="2006")])) self.realid3.add(CTOC(sub_frames=[TDRC(text="2006")])) self.id3.save(self.filename, v2_version=3) for frame_id in ["CHAP", "CTOC"]: chap = self.realid3.getall(frame_id)[0] assert chap.sub_frames.getall("TDRC")[0] == "2006" new = ID3(self.filename, translate=False) assert new.version == (2, 3, 0) chap = new.getall(frame_id)[0] assert not chap.sub_frames.getall("TDRC") assert chap.sub_frames.getall("TYER")[0] == "2006" 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) 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(MutagenError, 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.realid3) 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): # https://github.com/quodlibet/mutagen/issues/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.realid3) 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.realid3.getall("RVA2")) def test_gain_bad_key(self): self.failIf("replaygain_foo_gain" in self.id3) self.failIf(self.realid3.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.realid3.getall("RVA2")) def test_peak_bad_key(self): self.failIf("replaygain_foo_peak" in self.id3) self.failIf(self.realid3.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.realid3.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_gain_peak_capitalization(self): frame = RVA2(desc=u"Foo", gain=1.0, peak=1.0, channel=0) self.assertFalse(len(self.realid3)) self.realid3.add(frame) self.assertTrue("replaygain_Foo_peak" in self.id3) self.assertTrue("replaygain_Foo_peak" in self.id3.keys()) self.assertTrue("replaygain_Foo_gain" in self.id3) self.assertTrue("replaygain_Foo_gain" in self.id3.keys()) self.id3["replaygain_Foo_gain"] = ["0.5"] self.id3["replaygain_Foo_peak"] = ["0.25"] frames = self.realid3.getall("RVA2") self.assertEqual(len(frames), 1) self.assertEqual(frames[0].desc, u"Foo") self.assertEqual(frames[0].gain, 0.5) self.assertEqual(frames[0].peak, 0.25) def test_case_insensitive(self): self.id3["date"] = [u"2004"] self.assertEqual(self.id3["DATE"], [u"2004"]) del self.id3["DaTe"] self.assertEqual(len(self.id3), 0) self.id3["asin"] = [u"foo"] self.assertEqual(self.id3["Asin"], [u"foo"]) del self.id3["AsIn"] self.assertEqual(len(self.id3), 0) def test_pickle(self): # https://github.com/quodlibet/mutagen/issues/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 test_text_tags(self): for tag in ["albumartist", "performer"]: self.id3[tag] = u"foo" self.id3.save(self.filename) id3 = EasyID3(self.filename) self.failUnlessEqual(id3[tag], [u"foo"]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/test_easymp4.py0000644000175000017500000001223600000000000017632 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from mutagen import MutagenError from mutagen.easymp4 import EasyMP4, error as MP4Error from tests import TestCase, DATA_DIR, get_temp_copy class TEasyMP4(TestCase): def setUp(self): self.filename = get_temp_copy(os.path.join(DATA_DIR, 'has-tags.m4a')) self.mp4 = EasyMP4(self.filename) self.mp4.delete() def tearDown(self): os.unlink(self.filename) def test_no_tags(self): audio = EasyMP4(os.path.join(DATA_DIR, 'no-tags.m4a')) assert audio.tags is None audio.add_tags() assert audio.tags is not None def test_padding(self): assert self.mp4._padding == 1634 self.mp4.save(padding=lambda x: 42) assert EasyMP4(self.mp4.filename)._padding == 42 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(MutagenError, 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") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/test_encoding.py0000644000175000017500000000312700000000000020035 0ustar00lazkalazka00000000000000# -*- 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 as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. 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"), os.path.join(root, "venv")] 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_flac.py0000664000175000017500000005436200000000000017165 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os import subprocess import pytest from mutagen import MutagenError from mutagen.id3 import ID3, TIT2, ID3NoHeaderError from mutagen.flac import to_int_be, Padding, VCFLACDict, MetadataBlock, error from mutagen.flac import StreamInfo, SeekTable, CueSheet, FLAC, delete, Picture from tests import TestCase, DATA_DIR, get_temp_copy 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_bitrate(self): assert self.i.bitrate == 0 def test_invalid(self): # https://github.com/quodlibet/mutagen/issues/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): self.NEW = get_temp_copy(self.SAMPLE) self.flac = FLAC(self.NEW) def tearDown(self): os.unlink(self.NEW) def test_zero_samples(self): # write back zero sample count and load again self.flac.info.total_samples = 0 self.flac.save() new = FLAC(self.flac.filename) assert new.info.total_samples == 0 assert new.info.bitrate == 0 assert new.info.length == 0.0 def test_bitrate(self): assert self.flac.info.bitrate == 101430 old_file_size = os.path.getsize(self.flac.filename) self.flac.save(padding=lambda x: 9999) new_flac = FLAC(self.flac.filename) assert os.path.getsize(new_flac.filename) > old_file_size assert new_flac.info.bitrate == 101430 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_delete_change_reload(self): self.flac.delete() self.flac.tags["FOO"] = ["BAR"] self.flac.save() assert FLAC(self.flac.filename)["FOO"] == ["BAR"] # same with delete failing due to IO etc. with pytest.raises(MutagenError): self.flac.delete(os.devnull) self.flac.tags["FOO"] = ["QUUX"] self.flac.save() assert FLAC(self.flac.filename)["FOO"] == ["QUUX"] 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() with open(self.SAMPLE, "rb") as a: with open(self.NEW, "rb") as b: self.failUnlessEqual(a.read(), b.read()) def test_write_changetitle(self): f = FLAC(self.NEW) self.assertRaises( TypeError, f.__setitem__, b'title', b"A New Title") def test_write_changetitle_unicode_value(self): f = FLAC(self.NEW) self.assertRaises( TypeError, f.__setitem__, b'title', u"A Unicode Title \u2022") def test_write_changetitle_unicode_key(self): f = FLAC(self.NEW) f[u"title"] = b"A New Title" self.assertRaises(ValueError, f.save) 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 # https://github.com/quodlibet/mutagen/issues/112 self.assertRaises(error, 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(error, FLAC, self.NEW) def test_missing_streaminfo(self): self.flac.metadata_blocks.pop(0) self.flac.save() self.failUnlessRaises(error, FLAC, self.NEW) def test_load_invalid_flac(self): self.failUnlessRaises( error, FLAC, os.path.join(DATA_DIR, "xing.mp3")) def test_save_invalid_flac(self): self.failUnlessRaises( error, 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-existent FLAC files""" filename = os.path.join(DATA_DIR, "doesntexist.flac") self.assertRaises(MutagenError, 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): self.NEW = get_temp_copy(self.TOO_SHORT) 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"]) with open(self.NEW, "rb") as h: data = h.read(1024) self.failIf(b"Tunng" in data) class TFLACBadDuplicateVorbisComment(TestCase): def setUp(self): self.filename = get_temp_copy( os.path.join(DATA_DIR, "silence-44-s.flac")) # add a second vorbis comment block to the file right after the first some_tags = VCFLACDict() some_tags["DUPLICATE"] = ["SECOND"] f = FLAC(self.filename) f.tags["DUPLICATE"] = ["FIRST"] assert f.tags is f.metadata_blocks[2] f.metadata_blocks.insert(3, some_tags) f.save() def tearDown(self): os.unlink(self.filename) def test_load_multiple(self): # on load always use the first one, like metaflac f = FLAC(self.filename) assert f["DUPLICATE"] == ["FIRST"] assert f.metadata_blocks[2] is f.tags assert f.metadata_blocks[3]["DUPLICATE"] == ["SECOND"] # save in the same order f.save() f = FLAC(self.filename) assert f["DUPLICATE"] == ["FIRST"] def test_delete_multiple(self): # on delete we delete both f = FLAC(self.filename) f.delete() assert len(f.tags) == 0 f = FLAC(self.filename) assert f.tags is None def test_delete_multiple_fail(self): f = FLAC(self.filename) with pytest.raises(MutagenError): f.delete(os.devnull) f.save() # if delete failed we shouldn't see a difference f = FLAC(self.filename) assert f.metadata_blocks[2] is f.tags assert f.metadata_blocks[3].code == f.tags.code class TFLACBadBlockSizeOverflow(TestCase): def setUp(self): self.filename = get_temp_copy( os.path.join(DATA_DIR, "silence-44-s.flac")) 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() have_flac = True try: call_flac() except OSError: have_flac = False print("WARNING: Skipping FLAC reference tests.") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_id3.py0000664000175000017500000012627000000000000016735 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from io import BytesIO from mutagen import id3 from mutagen import MutagenError from mutagen.apev2 import APEv2 from mutagen.id3 import ID3, Frames, ID3UnsupportedVersionError, TIT2, \ CHAP, CTOC, TT1, TCON, COMM, TORY, PIC, MakeID3v1, TRCK, TYER, TDRC, \ TDAT, TIME, LNK, IPLS, TPE1, BinaryFrame, TIT3, POPM, APIC, CRM, \ TALB, TPE2, TSOT, TDEN, TIPL, ParseID3v1, Encoding, ID3Tags, RVAD, \ ID3NoHeaderError, Frames_2_2 from mutagen.id3._util import BitPaddedInt, error as ID3Error from mutagen.id3._tags import determine_bpi, ID3Header, \ save_frame, ID3SaveConfig from mutagen.id3._id3v1 import find_id3v1 from tests import TestCase, DATA_DIR, get_temp_copy, get_temp_empty def test_id3_module_exports_all_frames(): for key in Frames: assert getattr(id3, key) is Frames[key] for key in Frames_2_2: assert getattr(id3, key) is Frames_2_2[key] class TID3Read(TestCase): empty = os.path.join(DATA_DIR, 'emptyfile.mp3') silence = os.path.join(DATA_DIR, 'silence-44-s.mp3') silence_v1 = os.path.join(DATA_DIR, 'silence-44-s-v1.mp3') unsynch = os.path.join(DATA_DIR, 'id3v23_unsynch.id3') v22 = os.path.join(DATA_DIR, "id3v22-test.mp3") bad_tyer = os.path.join(DATA_DIR, 'bad-TYER-frame.mp3') v1v2_combined = os.path.join(DATA_DIR, "id3v1v2-combined.mp3") def test_PIC_in_23(self): filename = get_temp_empty(".mp3") 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) def test_bad_tyer(self): audio = ID3(self.bad_tyer) self.failIf("TYER" in audio) self.failUnless("TIT2" in audio) 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_to_v23(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"]]) def test_tags(self): tags = ID3(self.v22) self.failUnless(tags["TRCK"].text == ["3/11"]) self.failUnless(tags["TPE1"].text == ["Anais Mitchell"]) def test_load_v1(self): tags = ID3(self.silence_v1) self.assertEquals(tags["TALB"], "Quod Libet Test Data") with self.assertRaises(ID3NoHeaderError): tags = ID3(self.silence_v1, load_v1=False) def test_load_v1_v2(self): tags = ID3(self.v1v2_combined) # From ID3v2 self.assertEquals(tags["TPE1"].text, ["Anais Mitchell"]) # From ID3v1 self.assertEquals(tags["TALB"].text, ["Hymns for the Exiled"]) tags = ID3(self.v1v2_combined, load_v1=False) self.assertEquals(tags["TPE1"].text, ["Anais Mitchell"]) with self.assertRaises(KeyError): tags["TALB"] def test_load_v1_v2_no_translate(self): tags = ID3(self.v1v2_combined, v2_version=4, translate=False) assert tags.version == (2, 4, 0) assert str(tags["TDRC"].text[0]) == "1337" tags = ID3(self.v1v2_combined, v2_version=3, translate=False) assert tags.version == (2, 4, 0) assert str(tags["TDRC"].text[0]) == "1337" def test_load_v1_v2_tcon_translate(self): tags = ID3() tags.add(TCON(text=["12"])) v1_data = MakeID3v1(tags) filename = get_temp_copy(self.empty) try: tags = ID3() tags.save(filename=filename, v1=0) with open(filename, "ab") as h: h.write(v1_data) tags = ID3(filename, load_v1=True) assert tags["TCON"][0] == "Other" tags = ID3(filename, load_v1=False) assert "TCON" not in tags finally: os.unlink(filename) def test_load_v1_v2_precedence(self): tags = ID3(self.v1v2_combined) self.assertEquals(tags["TRCK"].text, ["3/11"]) # i.e. not 123 # ID3v2 has TYER=2004 (which isn't a valid v2.4 frame), # ID3v1 has TDRC=1337. self.assertEquals(str(tags["TDRC"].text[0]), "1337") with self.assertRaises(KeyError): tags["TYER"] tags = ID3(self.v1v2_combined, v2_version=3) # With v2_version=3, the ID3v2 tag should still have precedence self.assertEquals(str(tags["TYER"].text[0]), "2004") with self.assertRaises(KeyError): tags["TDRC"] def test_load_v1_comment(self): # Tags with different HashKeys but equal FrameIDs (like COMM) # should be kept separate tags = ID3(self.v1v2_combined) comments = tags.getall("COMM") # From ID3v2 self.failUnless("Waterbug Records, www.anaismitchell.com" in comments) # From ID3v1 self.failUnless("v1 comment" in comments) def test_load_v1_known_frames_override(self): class MyCOMM(COMM): @property def FrameID(self): # We want to replace the existing COMM, so override # the FrameID return COMM.__name__ frames = dict(id3.Frames) frames["COMM"] = MyCOMM tags = ID3(self.v1v2_combined, known_frames=frames) comments = tags.getall("COMM") self.failUnless(len(comments) > 0) for comm in comments: self.assertIsInstance(comm, MyCOMM) def test_empty_file(self): self.assertRaises(ID3Error, ID3, filename=self.empty) def test_nonexistent_file(self): name = os.path.join(DATA_DIR, 'does', 'not', 'exist') self.assertRaises(MutagenError, ID3, name) def test_read_padding(self): self.assertEqual(ID3(self.silence)._padding, 1142) self.assertEqual(ID3(self.unsynch)._padding, 0) def test_load_v23_unsynch(self): id3 = ID3(self.unsynch) self.assertEquals(id3["TPE1"], ["Nina Simone"]) def test_bad_extended_header_flags(self): # Files with bad extended header flags failed to read tags. # Ensure the extended header is turned off, and the frames are # read. id3 = ID3(os.path.join(DATA_DIR, 'issue_21.id3')) self.failIf(id3.f_extended) self.failUnless("TIT2" in id3) self.failUnless("TALB" in id3) self.failUnlessEqual(id3["TIT2"].text, [u"Punk To Funk"]) def test_no_known_frames(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_23_multiframe_hack(self): # loaded_frame is no longer used in mutagen, but this makes # sure that old code keeps working (used in quod libet <=3.6) 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_chap_subframes(self): id3 = ID3() id3.version = (2, 3) id3.add(CHAP(element_id="foo", start_time=0, end_time=0, start_offset=0, end_offset=0, sub_frames=[TYER(encoding=0, text="2006")])) id3.update_to_v24() chap = id3.getall("CHAP:foo")[0] self.assertEqual(chap.sub_frames.getall("TDRC")[0], u"2006") self.assertFalse(chap.sub_frames.getall("TYER")) id3.update_to_v23() self.assertEqual(chap.sub_frames.getall("TYER")[0], u"2006") def test_ctoc_subframes(self): id3 = ID3() id3.version = (2, 3) id3.add(CTOC(sub_frames=[TYER(encoding=0, text="2006")])) id3.update_to_v24() ctoc = id3.getall("CTOC")[0] self.assertEqual(ctoc.sub_frames.getall("TDRC")[0], u"2006") self.assertFalse(ctoc.sub_frames.getall("TYER")) id3.update_to_v23() self.assertEqual(ctoc.sub_frames.getall("TYER")[0], u"2006") def test_pic(self): 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): id3 = ID3() id3.version = (2, 2) id3.add(LNK(frameid="PIC", url="http://foo.bar")) id3.update_to_v24() self.assertTrue(id3.getall("LINK")) def test_tyer(self): 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): 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): 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_multiple_tyer_tdat_time(self): id3 = ID3() id3.version = (2, 3) id3.add(TYER(text=['2000', '2001', '2002', '19xx', 'foo'])) id3.add(TDAT(text=['0102', '0304', '1111bar'])) id3.add(TIME(text=['1220', '1111quux', '1111'])) id3.update_to_v24() assert [str(t) for t in id3['TDRC']] == \ ['2000-02-01 12:20:00', '2001-04-03', '2002'] def test_tory(self): 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): 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_time_dropped(self): id3 = ID3() id3.version = (2, 3) id3.add(TIME(encoding=0, text=["1155"])) id3.update_to_v24() self.assertFalse(id3.getall("TIME")) def test_rvad_dropped(self): id3 = ID3() id3.version = (2, 3) id3.add(RVAD()) id3.update_to_v24() self.assertFalse(id3.getall("RVAD")) class TID3Header(TestCase): silence = os.path.join(DATA_DIR, 'silence-44-s.mp3') empty = os.path.join(DATA_DIR, 'emptyfile.mp3') def test_header_empty(self): with open(self.empty, 'rb') as fileobj: self.assertRaises(ID3Error, ID3Header, fileobj) def test_header_silence(self): with open(self.silence, 'rb') as fileobj: header = ID3Header(fileobj) self.assertEquals(header.version, (2, 3, 0)) self.assertEquals(header.size, 1314) def test_header_2_4_invalid_flags(self): fileobj = BytesIO(b'ID3\x04\x00\x1f\x00\x00\x00\x00') self.assertRaises(ID3Error, ID3Header, fileobj) def test_header_2_4_unsynch_size(self): fileobj = BytesIO(b'ID3\x04\x00\x10\x00\x00\x00\xFF') self.assertRaises(ID3Error, ID3Header, fileobj) def test_header_2_4_allow_footer(self): fileobj = BytesIO(b'ID3\x04\x00\x10\x00\x00\x00\x00') self.assertTrue(ID3Header(fileobj).f_footer) def test_header_2_3_invalid_flags(self): fileobj = BytesIO(b'ID3\x03\x00\x1f\x00\x00\x00\x00') self.assertRaises(ID3Error, ID3Header, fileobj) fileobj = BytesIO(b'ID3\x03\x00\x0f\x00\x00\x00\x00') self.assertRaises(ID3Error, ID3Header, fileobj) def test_header_2_2(self): fileobj = BytesIO(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 = BytesIO(b'ID3\x01\x00\x00\x00\x00\x00\x00') self.assertRaises(ID3UnsupportedVersionError, ID3Header, fileobj) def test_header_too_small(self): fileobj = BytesIO(b'ID3\x01\x00\x00\x00\x00\x00') self.assertRaises(ID3Error, ID3Header, fileobj) def test_header_2_4_extended(self): fileobj = BytesIO( 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 = BytesIO( 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 = BytesIO( 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 = BytesIO(b'ID3\x04\x00\x40\x00\x00\x00\x00TIT9') self.failUnlessRaises(ID3Error, ID3Header, fileobj) def test_header_2_3_extended(self): fileobj = BytesIO( 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_23(self): id3 = ID3(self.silence) self.assertEqual(id3.version, (2, 3, 0)) 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']) class TID3Tags(TestCase): silence = os.path.join(DATA_DIR, 'silence-44-s.mp3') def setUp(self): self.frames = [ TIT2(text=["1"]), TIT2(text=["2"]), TIT2(text=["3"]), TIT2(text=["4"])] self.i = ID3Tags() self.i["BLAH"] = self.frames[0] self.i["QUUX"] = self.frames[1] self.i["FOOB:ar"] = self.frames[2] self.i["FOOB:az"] = self.frames[3] def test_apic_duplicate_hash(self): id3 = ID3Tags() for i in range(10): apic = APIC(encoding=0, mime=u"b", type=3, desc=u"", data=b"a") id3._add(apic, False) self.assertEqual(len(id3), 10) for key, value in id3.items(): self.assertEqual(key, value.HashKey) def test_text_duplicate_frame_different_encoding(self): id3 = ID3Tags() frame = TPE2(encoding=Encoding.LATIN1, text=[u"foo"]) id3._add(frame, False) assert id3.getall("TPE2")[0].encoding == Encoding.LATIN1 frame = TPE2(encoding=Encoding.LATIN1, text=[u"bar"]) id3._add(frame, False) assert id3.getall("TPE2")[0].encoding == Encoding.LATIN1 frame = TPE2(encoding=Encoding.UTF8, text=[u"baz\u0400"]) id3._add(frame, False) assert id3.getall("TPE2")[0].encoding == Encoding.UTF8 frames = id3.getall("TPE2") assert len(frames) == 1 assert len(frames[0].text) == 3 def test_add_CRM(self): id3 = ID3Tags() self.assertRaises(TypeError, id3.add, CRM()) def test_read__ignore_CRM(self): tags = ID3Tags() header = ID3Header() header.version = ID3Header._V22 framedata = CRM(owner="foo", desc="bar", data=b"bla")._writeData() datasize = BitPaddedInt.to_str(len(framedata), width=3, bits=8) tags._read(header, b"CRM" + datasize + framedata) self.assertEqual(len(tags), 0) def test_update_v22_add(self): id3 = ID3Tags() 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_getnormal(self): self.assertEquals(self.i.getall("BLAH"), [self.frames[0]]) self.assertEquals(self.i.getall("QUUX"), [self.frames[1]]) self.assertEquals(self.i.getall("FOOB:ar"), [self.frames[2]]) self.assertEquals(self.i.getall("FOOB:az"), [self.frames[3]]) def test_getlist(self): self.assertTrue( self.i.getall("FOOB") in [[self.frames[2], self.frames[3]], [self.frames[3], self.frames[2]]]) 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"), [self.frames[3]]) 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(TIT2): HashKey = "" t = TEST() t.HashKey = "FOOB:ar" 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(TIT2): HashKey = "" t = TEST() t.HashKey = "FOOB:ar" 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]]) def test_set_wrong_type(self): id3 = ID3Tags() self.assertRaises(TypeError, id3.__setitem__, "FOO", object()) class ID3v1Tags(TestCase): def setUp(self): self.filename = os.path.join(DATA_DIR, 'silence-44-s-v1.mp3') self.id3 = ID3(self.filename) 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): 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): 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): 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): 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): 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): 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): self.failUnless(ParseID3v1(b"") is None) def test_invalid_track(self): tag = {} tag["TRCK"] = TRCK(encoding=0, text="not a number") v1tag = MakeID3v1(tag) self.failIf("TRCK" in ParseID3v1(v1tag)) def test_v1_genre(self): tag = {} tag["TCON"] = TCON(encoding=0, text="Pop") v1tag = MakeID3v1(tag) self.failUnlessEqual(ParseID3v1(v1tag)["TCON"].genres, ["Pop"]) class TestWriteID3v1(TestCase): def setUp(self): self.filename = get_temp_copy( os.path.join(DATA_DIR, "silence-44-s.mp3")) self.audio = ID3(self.filename) def failIfV1(self): with open(self.filename, "rb") as fileobj: fileobj.seek(-128, 2) self.failIf(fileobj.read(3) == b"TAG") def failUnlessV1(self): with open(self.filename, "rb") as fileobj: 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 Issue97_UpgradeUnknown23(TestCase): def setUp(self): self.filename = get_temp_copy( os.path.join(DATA_DIR, "97-unknown-23-update.mp3")) def tearDown(self): os.unlink(self.filename) def test_unknown(self): 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") # save as 2.3 unknown.save(v2_version=3) # load again with support for TIT2, all should be there again new = ID3(self.filename) self.failUnlessEqual(new["TIT2"].text, orig["TIT2"].text) self.failUnlessEqual(new["TPE1"].text, orig["TPE1"].text) def test_unknown_invalid(self): frame = BinaryFrame(data=b"\xff" * 50) f = ID3(self.filename) self.assertEqual(f.version, ID3Header._V23) config = ID3SaveConfig(3, None) f.unknown_frames = [save_frame(frame, b"NOPE", config)] f.save() f = ID3(self.filename) self.assertFalse(f.unknown_frames) class TID3Write(TestCase): def setUp(self): self.filename = get_temp_copy( os.path.join(DATA_DIR, 'silence-44-s.mp3')) def tearDown(self): try: os.unlink(self.filename) except OSError: pass def test_corrupt_header_too_small(self): with open(self.filename, "r+b") as h: h.truncate(5) self.assertRaises(id3.error, ID3, self.filename) def test_corrupt_tag_too_small(self): with open(self.filename, "r+b") as h: h.truncate(50) self.assertRaises(id3.error, ID3, self.filename) def test_corrupt_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) def test_padding_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_padding_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) 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 test_update_to_v23_on_load(self): audio = ID3(self.filename) audio.add(TSOT(text=["Ha"], encoding=3)) 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): audio = ID3(self.filename) self.assertRaises(ValueError, audio.save, v2_version=5) self.assertRaises(ValueError, ID3, self.filename, v2_version=5) def test_save(self): audio = ID3(self.filename) strings = ["one", "two", "three"] audio.add(TPE1(text=strings, encoding=3)) audio.save(v2_version=3) frame = 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 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 audio = ID3(self.filename) dates = ["2013", "2014"] frame = TDEN(text=dates, encoding=3) audio.add(frame) tipl_frame = TIPL(people=[("a", "b"), ("c", "d")], encoding=2) audio.add(tipl_frame) 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) def test_wrong_encoding(self): t = ID3(self.filename) t.add(TIT2(encoding=Encoding.LATIN1, text=[u"\u0243"])) self.assertRaises(MutagenError, t.save) def test_toemptyfile(self): t = ID3(self.filename) os.unlink(self.filename) open(self.filename, "wb").close() t.save(self.filename) def test_tononfile(self): t = ID3(self.filename) os.unlink(self.filename) t.save(self.filename) def test_1bfile(self): t = ID3(self.filename) os.unlink(self.filename) with open(self.filename, "wb") as f: f.write(b"!") t.save(self.filename) self.assert_(os.path.getsize(self.filename) > 1) with open(self.filename, "rb") as h: self.assertEquals(h.read()[-1], b"!"[0]) def test_unknown_chap(self): # add ctoc id3 = ID3(self.filename) id3.add(CTOC(element_id="foo", flags=3, child_element_ids=["ch0"], sub_frames=[TIT2(encoding=3, text=["bla"])])) id3.save() # pretend we don't know ctoc and save id3 = ID3(self.filename, known_frames={"CTOC": CTOC}) ctoc = id3.getall("CTOC")[0] self.assertFalse(ctoc.sub_frames) self.assertTrue(ctoc.sub_frames.unknown_frames) id3.save() # make sure we wrote all sub frames back id3 = ID3(self.filename) self.assertEqual( id3.getall("CTOC")[0].sub_frames.getall("TIT2")[0].text, ["bla"]) def test_same(self): ID3(self.filename).save() id3 = ID3(self.filename) self.assertEquals(id3["TALB"], "Quod Libet Test Data") self.assertEquals(id3["TCON"], "Silence") self.assertEquals(id3["TIT2"], "Silence") self.assertEquals(id3["TPE1"], ["piman", "jzig"]) def test_same_v23(self): id3 = ID3(self.filename, v2_version=3) id3.save(v2_version=3) id3 = ID3(self.filename) 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"], "piman/jzig") def test_addframe(self): f = ID3(self.filename) self.assert_("TIT3" not in f) f["TIT3"] = TIT3(encoding=0, text="A subtitle!") f.save() id3 = ID3(self.filename) self.assertEquals(id3["TIT3"], "A subtitle!") def test_changeframe(self): f = ID3(self.filename) self.assertEquals(f["TIT2"], "Silence") f["TIT2"].text = [u"The sound of silence."] f.save() id3 = ID3(self.filename) self.assertEquals(id3["TIT2"], "The sound of silence.") def test_replaceframe(self): f = ID3(self.filename) self.assertEquals(f["TPE1"], [u'piman', u'jzig']) f["TPE1"] = TPE1(encoding=0, text=u"jzig\x00piman") f.save() id3 = ID3(self.filename) self.assertEquals(id3["TPE1"], ["jzig", "piman"]) def test_compressibly_large(self): f = ID3(self.filename) self.assert_("TPE2" not in f) f["TPE2"] = TPE2(encoding=0, text="Ab" * 1025) f.save() id3 = ID3(self.filename) self.assertEquals(id3["TPE2"], "Ab" * 1025) def test_nofile_silencetag(self): id3 = ID3(self.filename) os.unlink(self.filename) id3.save(self.filename) with open(self.filename, 'rb') as h: self.assertEquals(b'ID3', h.read(3)) self.test_same() def test_emptyfile_silencetag(self): id3 = ID3(self.filename) with open(self.filename, 'wb') as h: h.truncate() id3.save(self.filename) with open(self.filename, 'rb') as h: self.assertEquals(b'ID3', h.read(3)) self.test_same() def test_empty_plustag_minustag_empty(self): id3 = ID3(self.filename) with open(self.filename, 'wb') as h: h.truncate() id3.save() id3.delete() self.failIf(id3) with open(self.filename, 'rb') as h: self.assertEquals(h.read(10), b'') def test_delete_invalid_zero(self): with open(self.filename, 'wb') as f: f.write(b'ID3\x04\x00\x00\x00\x00\x00\x00abc') ID3(self.filename).delete() with open(self.filename, 'rb') as f: self.assertEquals(f.read(10), b'abc') def test_frame_order(self): f = ID3(self.filename) 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() with open(self.filename, 'rb') as h: data = h.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 test_apic_last(self): # https://github.com/quodlibet/mutagen/issues/278 f = ID3(self.filename) f.add(TYER(text=[u"2016"])) f.add(APIC(data=b"x" * 500)) f.save() with open(self.filename, 'rb') as h: data = h.read() assert data.find(b"TYER") < data.find(b"APIC") class WriteForEyeD3(TestCase): def setUp(self): self.silence = os.path.join(DATA_DIR, 'silence-44-s.mp3') self.newsilence = get_temp_copy(self.silence) # remove ID3v1 tag with open(self.newsilence, "rb+") as f: f.seek(-128, 2) f.truncate() def tearDown(self): os.unlink(self.newsilence) 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") self.assertEquals(len(id3.frames["TPE1"]), 1) self.assertEquals(id3.frames["TPE1"][0].text, "piman/jzig") def test_addframe(self): 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.") class BadPOPM(TestCase): def setUp(self): self.filename = get_temp_copy( os.path.join(DATA_DIR, 'bad-POPM-frame.mp3')) def tearDown(self): os.unlink(self.filename) def test_read_popm_long_counter(self): f = ID3(self.filename) 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): f = ID3(self.filename) f.add(POPM(email="foo@example.com", rating=125, count=2 ** 32 + 1)) f.save() f = ID3(self.filename) 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): 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): 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(BytesIO(data)) self.assertEqual(offset, -125) self.assertEqual(frames, tag) def test_none(self): s = MakeID3v1(dict()) self.failUnlessEqual(len(s), 128) tag = ParseID3v1(s) self.failIf("TDRC" in tag) def test_empty(self): s = MakeID3v1(dict(TDRC="")) self.failUnlessEqual(len(s), 128) tag = ParseID3v1(s) self.failIf("TDRC" in tag) def test_short(self): s = MakeID3v1(dict(TDRC="1")) self.failUnlessEqual(len(s), 128) tag = ParseID3v1(s) self.failUnlessEqual(tag["TDRC"], "0001") def test_long(self): s = MakeID3v1(dict(TDRC="123456789")) self.failUnlessEqual(len(s), 128) tag = ParseID3v1(s) self.failUnlessEqual(tag["TDRC"], "1234") class TID3Trailing(TestCase): def test_audacious_trailing_id3(self): # https://github.com/quodlibet/mutagen/issues/78 # tagged with audacious 3.2.4, both are id3v2 at the end despite the # spec saying it should be before other tags. # Audacious changed it to write in the beginning with 3.4 or 3.5 # Now with Audacious 3.7, re-saving the files results in the id3v3 # tag moved to the front and the id3v1/apev2 tags left as is at the # end. path = os.path.join(DATA_DIR, 'audacious-trailing-id32-id31.mp3') self.assertRaises(ID3NoHeaderError, ID3, path) path = os.path.join(DATA_DIR, 'audacious-trailing-id32-apev2.mp3') self.assertRaises(ID3NoHeaderError, ID3, path) 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): # 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) try: import eyeD3 except ImportError: print("WARNING: Skipping eyeD3 tests.") del WriteForEyeD3 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_m4a.py0000664000175000017500000000175500000000000016737 0ustar00lazkalazka00000000000000# -*- 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 # flake8 with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/test_monkeysaudio.py0000644000175000017500000000373200000000000020760 0ustar00lazkalazka00000000000000# -*- 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_bits_per_sample(self): assert self.mac399.info.bits_per_sample == 16 assert self.mac396.info.bits_per_sample == 16 assert self.mac390.info.bits_per_sample == 16 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()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_mp3.py0000664000175000017500000004270400000000000016754 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from io import BytesIO from tests import TestCase, DATA_DIR, get_temp_copy from mutagen.mp3 import MP3, error as MP3Error, delete, MPEGInfo, EasyMP3, \ BitrateMode, iter_sync from mutagen.mp3._util import XingHeader, XingHeaderError, VBRIHeader, \ VBRIHeaderError, LAMEHeader, LAMEError from mutagen.id3 import ID3 class TMP3Util(TestCase): def test_find_sync(self): def get_syncs(fileobj, max_read): start = fileobj.tell() pos = [] for i in iter_sync(fileobj, max_read): pos.append(fileobj.tell() - start) return pos self.assertEqual(get_syncs(BytesIO(b"abc"), 100), []) self.assertEqual(get_syncs(BytesIO(b""), 100), []) self.assertEqual(get_syncs(BytesIO(b"a\xff\xe0"), 1), []) self.assertEqual(get_syncs(BytesIO(b"a\xff\xc0\xff\xe0"), 100), [3]) self.assertEqual( get_syncs(BytesIO(b"a\xff\xe0\xff\xe0\xff\xe0"), 100), [1, 3, 5]) for i in range(400): fileobj = BytesIO(b"\x00" * i + b"\xff\xe0") self.assertEqual(get_syncs(fileobj, 100 + i), [i]) 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') lame_broken_short = os.path.join(DATA_DIR, 'lame397v9short.mp3') def setUp(self): self.filename = get_temp_copy( os.path.join(DATA_DIR, "silence-44-s.mp3")) 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_lame_broken_short(self): # lame <=3.97 wrote broken files f = MP3(self.lame_broken_short) assert f.info.encoder_info == "LAME 3.97.0" assert f.info.encoder_settings == "-V 9" assert f.info.length == 0.0 assert f.info.bitrate == 40000 assert f.info.bitrate_mode == 2 assert f.info.sample_rate == 24000 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, str)) 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, str)) 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.assertAlmostEqual(self.mp3.info.length, 3.77, 2) self.assertAlmostEqual(self.mp3_2.info.length, 3.77, 2) self.assertAlmostEqual(self.mp3_3.info.length, 3.68475, 4) self.assertAlmostEqual(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, 17783) self.failUnlessEqual(self.mp3_4.info.bitrate, 8900) def test_notmp3(self): self.failUnlessRaises( MP3Error, MP3, os.path.join(DATA_DIR, 'empty.ofr')) self.failUnlessRaises( MP3Error, MP3, os.path.join(DATA_DIR, 'emptyfile.mp3')) def test_too_short(self): self.failUnlessRaises( MP3Error, MP3, os.path.join(DATA_DIR, 'too-short.mp3')) 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) self.assertTrue(u"sketchy" in notmp3.info.pprint()) 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, str)) 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") with open(filename, "rb") as h: fileobj = BytesIO(h.read(20)) self.failUnlessRaises(MP3Error, MPEGInfo, fileobj) def test_empty(self): fileobj = BytesIO(b"") self.failUnlessRaises(MP3Error, MPEGInfo, fileobj) def test_xing_unknown_framecount(self): frame = ( b'\xff\xfb\xe4\x0c\x00\x0f\xf0\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\x00Info\x00\x00\x00\x02\x00\xb4V@\x00\xb4R\x80\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' ) fileobj = BytesIO(frame) info = MPEGInfo(fileobj) assert info.bitrate == 320000 assert info.length > 0 class TEasyMP3(TestCase): def setUp(self): self.filename = get_temp_copy( os.path.join(DATA_DIR, "silence-44-s.mp3")) 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): # https://github.com/quodlibet/mutagen/issues/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 with open(self.filename, "rb") as h: nonid3 = MPEGInfo(h) self.failUnlessEqual(easy.length, noneasy.length) self.failUnlessEqual(noneasy.length, nonid3.length) def tearDown(self): os.unlink(self.filename) 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 = BytesIO(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(BytesIO(data.replace(b'Info', b'Xing'))) def test_invalid(self): self.assertRaises(XingHeaderError, XingHeader, BytesIO(b"")) self.assertRaises(XingHeaderError, XingHeader, BytesIO(b"Xing")) self.assertRaises(XingHeaderError, XingHeader, BytesIO(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 = BytesIO(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, BytesIO(b"")) self.assertRaises(VBRIHeaderError, VBRIHeader, BytesIO(b"VBRI")) self.assertRaises(VBRIHeaderError, VBRIHeader, BytesIO(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 = BytesIO(data + b"\x00" * (20 - len(data))) return tuple(LAMEHeader.parse_version(data)[1:]) 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)) self.assertEqual(parse(b"LAME3.100"), (u"3.100.0+", True)) def test_invalid(self): def parse(data): data = BytesIO(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.9999") 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_desc, u"3.99.1+") self.assertTrue(xing.lame_header) self.assertEqual(xing.lame_header.track_gain_adjustment, 6.0) assert xing.get_encoder_settings() == u"-V 2" def test_settings(self): with open(os.path.join(DATA_DIR, "lame.mp3"), "rb") as h: h.seek(36, 0) xing = XingHeader(h) header = xing.lame_header def s(major, minor, **kwargs): old = vars(header) for key, value in kwargs.items(): assert hasattr(header, key) setattr(header, key, value) r = header.guess_settings(major, minor) header.__dict__.update(old) return r assert s(3, 99) == "-V 2" assert s(3, 98) == "-V 2" assert s(3, 97) == "-V 2 --vbr-new" assert s(3, 96) == "-V 2 --vbr-new" assert s(3, 95) == "-V 2 --vbr-new" assert s(3, 94) == "-V 2 --vbr-new" assert s(3, 93) == "-V 2 --vbr-new" assert s(3, 92) == "-V 2 --vbr-new" assert s(3, 91) == "-V 2 --vbr-new" assert s(3, 90) == "-V 2 --vbr-new" assert s(3, 89) == "" assert s(3, 91, vbr_method=2) == "--alt-preset 32" assert s(3, 91, vbr_method=2, bitrate=255) == "--alt-preset 255+" assert s(3, 99, vbr_method=2, preset_used=128) == "--preset 128" assert s(3, 99, vbr_method=2, preset_used=0, bitrate=48) == "--abr 48" assert \ s(3, 94, vbr_method=2, preset_used=0, bitrate=255) == "--abr 255+" assert s(3, 99, vbr_method=3) == "-V 2 --vbr-old" assert s(3, 94, vbr_method=3) == "-V 2" assert s(3, 99, vbr_method=1, preset_used=1003) == "--preset insane" assert s(3, 93, vbr_method=3, preset_used=1001) == "--preset standard" assert s(3, 93, vbr_method=3, preset_used=1002) == "--preset extreme" assert s(3, 93, vbr_method=3, preset_used=1004) == \ "--preset fast standard" assert s(3, 93, vbr_method=3, preset_used=1005) == \ "--preset fast extreme" assert s(3, 93, vbr_method=3, preset_used=1006) == "--preset medium" assert s(3, 93, vbr_method=3, preset_used=1007) == \ "--preset fast medium" assert s(3, 92, vbr_method=3) == "-V 2" assert s(3, 92, vbr_method=1, preset_used=0, bitrate=254) == "-b 254" assert s(3, 92, vbr_method=1, preset_used=0, bitrate=255) == "-b 255+" def skey(major, minor, args): keys = ["vbr_quality", "quality", "vbr_method", "lowpass_filter", "ath_type"] return s(major, minor, **dict(zip(keys, args))) assert skey(3, 91, (1, 2, 4, 19500, 3)) == "--preset r3mix" assert skey(3, 91, (2, 2, 3, 19000, 4)) == "--alt-preset standard" assert skey(3, 91, (2, 2, 3, 19500, 2)) == "--alt-preset extreme" def test_length(self): mp3 = MP3(os.path.join(DATA_DIR, "lame.mp3")) self.assertAlmostEqual(mp3.info.length, 0.06160, 4) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_mp4.py0000664000175000017500000012633500000000000016760 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os import struct import subprocess from io import BytesIO import pytest from tests import TestCase, DATA_DIR, get_temp_copy from mutagen.mp4 import (MP4, Atom, Atoms, MP4Tags, MP4Info, delete, MP4Cover, MP4MetadataError, MP4FreeForm, error, AtomDataType, AtomError, _item_sort_key, MP4StreamInfoError) 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 = BytesIO(b"\x00\x00\x00\x08atom") atom = Atom(fileobj) self.failUnlessRaises(KeyError, atom.__getitem__, "test") def test_length_1(self): fileobj = BytesIO(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 = BytesIO(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 = BytesIO(b"\x00\x00\x00\x02atom") self.assertRaises(AtomError, Atom, fileobj) def test_truncated(self): self.assertRaises(AtomError, Atom, BytesIO(b"\x00")) self.assertRaises(AtomError, Atom, BytesIO(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 = BytesIO(struct.pack(">I4s", 0, b"whee")) self.assertRaises(AtomError, Atom, data, level=1) def test_length_0(self): fileobj = BytesIO(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 = BytesIO(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 = BytesIO(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 = BytesIO(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): with open(self.filename, "rb") as h: self.atoms = Atoms(h) 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 = BytesIO(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( error, 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 = BytesIO(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 = BytesIO(moov) atoms = Atoms(fileobj) info = MP4Info(atoms, fileobj) self.failUnlessEqual(info.length, 8) def test_no_tracks(self): moov = Atom.render(b"moov", b"") fileobj = BytesIO(moov) atoms = Atoms(fileobj) with self.assertRaises(MP4StreamInfoError): MP4Info(atoms, fileobj) 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 = BytesIO(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://github.com/quodlibet/mutagen/issues/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_integer_min_size(self): render_int = MP4Tags()._MP4Tags__render_integer data = render_int('stik', [42], 1) tags = self.wrap_ilst(data) assert tags['stik'] == [42] assert len(render_int('stik', [42], 2)) == len(data) + 1 assert len(render_int('stik', [42], 4)) == len(data) + 3 assert len(render_int('stik', [42], 8)) == len(data) + 7 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_parse_tmpo(self): for d, v in [(b"\x01", 1), (b"\x01\x02", 258), (b"\x01\x02\x03", 66051), (b"\x01\x02\x03\x04", 16909060), (b"\x01\x02\x03\x04\x05\x06\x07\x08", 72623859790382856)]: data = Atom.render( b"data", b"\x00\x00\x00\x15" + b"\x00\x00\x00\x00" + d) tmpo = Atom.render(b"tmpo", data) tags = self.wrap_ilst(tmpo) assert tags["tmpo"][0] == v 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") filename = get_temp_copy(original) 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["trkn"] = [(1, 2), (3, 4)] tags.pprint() def test_freeform_data(self): # https://github.com/quodlibet/mutagen/issues/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): self.filename = get_temp_copy(self.original) 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_set_inval(self): self.assertRaises(TypeError, self.audio.__setitem__, "\xa9nam", 42) 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 TypeError: pass 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( TypeError, self.audio.__setitem__, '\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', []) self.set_key('tmpo', [0]) self.set_key('tmpo', [cdata.int16_min]) self.set_key('tmpo', [cdata.int32_min]) self.set_key('tmpo', [cdata.int64_min]) self.set_key('tmpo', [cdata.int16_max]) self.set_key('tmpo', [cdata.int32_max]) self.set_key('tmpo', [cdata.int64_max]) def test_various_int(self): keys = [ "stik", "hdvd", "rtng", "plID", "cnID", "geID", "atID", "sfID", "cmID", "akID", "tvsn", "tves", ] for key in keys: self.set_key(key, []) self.set_key(key, [0]) self.set_key(key, [1]) self.set_key(key, [cdata.int64_max]) def test_movements(self): self.set_key('shwm', [1]) self.set_key('\xa9mvc', [42]) self.set_key('\xa9mvi', [24]) self.set_key('\xa9mvn', [u"movement"]) self.set_key('\xa9wrk', [u"work"]) def test_tempos(self): self.set_key('tmpo', [160, 200], faad=False) def test_tempo_invalid(self): for badvalue in [ [cdata.int64_max + 1], [cdata.int64_min - 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(), str)) def test_pprint_binary(self): self.audio["covr"] = [b"\x00\xa9garbage"] 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 zip(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): self.filename = get_temp_copy(self.original) 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_no_audio_tracks(self): data = Atom.render(b"moov", Atom.render(b"udta", b"")) fileobj = BytesIO(data) audio = MP4(fileobj) assert audio.info assert audio.pprint() info = audio.info assert isinstance(info.bitrate, int) assert isinstance(info.length, float) assert isinstance(info.channels, int) assert isinstance(info.sample_rate, int) assert isinstance(info.bits_per_sample, int) assert isinstance(info.codec, str) assert isinstance(info.codec_description, str) 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=lambda kv: _item_sort_key(*kv)) 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") 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") 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 = BytesIO(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 = BytesIO(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 = BytesIO(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 = BytesIO(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, str)) self.assertTrue(isinstance(entry.codec_description, str)) 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 = BytesIO(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 = BytesIO(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, str)) self.assertTrue(isinstance(entry.codec_description, str)) 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 = BytesIO(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, str)) self.assertTrue(isinstance(entry.codec_description, str)) def test_error(self): fileobj = BytesIO(b"\x00" * 20) atom = Atom(fileobj) self.assertRaises(ASEntryError, AudioSampleEntry, atom, fileobj) def test_weird_descriptor_size(): path = os.path.join(DATA_DIR, "ep7.m4b") t = MP4(path) assert t.info.length == pytest.approx(2.02, 2) assert t.info.bitrate == 125591 assert t.info.sample_rate == 44100 assert t.info.codec_description == "AAC LC" path = os.path.join(DATA_DIR, "ep9.m4b") t = MP4(path) assert t.info.length == pytest.approx(2.02, 2) assert t.info.bitrate == 61591 assert t.info.sample_rate == 44100 assert t.info.codec_description == "AAC LC" class TMP4Chapters(TMP4): original = os.path.join(DATA_DIR, "nero-chapters.m4b") def test_has_chapters(self): self.failUnless(hasattr(self.audio, 'chapters')) chapters = self.audio.chapters self.failUnlessEqual(len(chapters), 112) for i, c in enumerate(chapters): self.failUnlessEqual(c.title, str(i + 1).zfill(3)) 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.") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_musepack.py0000664000175000017500000001055700000000000020066 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from io import BytesIO from mutagen.id3 import ID3, TIT2 from mutagen.musepack import Musepack, MusepackInfo, MusepackHeaderError from tests import TestCase, DATA_DIR, get_temp_copy 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, BytesIO(b"MP+" + b"\x00" * 32)) self.failUnlessRaises( MusepackHeaderError, MusepackInfo, BytesIO(b"MP+" + b"\x00" * 100)) self.failUnlessRaises( MusepackHeaderError, MusepackInfo, BytesIO(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://github.com/quodlibet/mutagen/issues/198 data = (b"MPCKSH\x10\x95 Q\xa2\x08\x81\xb8\xc9T\x00\x1e\x1b" b"\x00RG\x0c\x01A\xcdY\x06?\x80Z\x06EI") fileobj = BytesIO(data) info = MusepackInfo(fileobj) self.assertEqual(info.channels, 2) self.assertEqual(info.samples, 3024084) class TMusepackWithID3(TestCase): def setUp(self): self.filename = get_temp_copy(os.path.join(DATA_DIR, "click.mpc")) def tearDown(self): os.unlink(self.filename) def test_ignore_id3(self): id3 = ID3() id3.add(TIT2(encoding=0, text='id3 title')) id3.save(self.filename) f = Musepack(self.filename) f['title'] = 'apev2 title' f.save() id3 = ID3(self.filename) self.failUnlessEqual(id3['TIT2'], 'id3 title') f = Musepack(self.filename) self.failUnlessEqual(f['title'], 'apev2 title') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_ogg.py0000664000175000017500000006634700000000000017042 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os import random import subprocess from io import BytesIO from tests import TestCase, DATA_DIR, get_temp_copy from mutagen.ogg import OggPage, error as OggError from mutagen._util import cdata from mutagen import _util 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 range(len(pages)): pages[i].sequence = i for page in pages: page.serial = 1 self.pages = pages def test_to_packets_empty_page(self): pages = [OggPage(), OggPage()] for i in range(len(pages)): pages[i].sequence = i assert OggPage.to_packets(pages, True) == [] assert OggPage.to_packets(pages, False) == [] pages = [OggPage(), OggPage(), OggPage()] pages[0].packets = [b"foo"] pages[0].complete = False pages[1].continued = True pages[1].complete = False pages[2].packets = [b"bar"] pages[2].continued = True for i in range(len(pages)): pages[i].sequence = i assert OggPage.to_packets(pages, True) == [b'foobar'] 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 range(3)] self.failUnlessEqual([page.sequence for page in pages], [10, 11, 12]) fileobj.seek(0) OggPage.renumber(fileobj, 1, 20) fileobj.seek(0) pages = [OggPage(fileobj) for i in range(3)] self.failUnlessEqual([page.sequence for page in pages], [20, 21, 22]) def test_renumber_extradata(self): fileobj = 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 range(3)] self.failUnlessEqual([page.sequence for page in pages], [10, 11, 12]) # And the garbage that caused the error should be okay too. self.failUnlessEqual(fileobj.read(), b"left over data") def test_renumber_reread(self): try: filename = get_temp_copy( os.path.join(DATA_DIR, "multipagecomment.ogg")) with open(filename, "rb+") as fileobj: OggPage.renumber(fileobj, 1002429366, 20) with open(filename, "rb+") as fileobj: OggPage.renumber(fileobj, 1002429366, 0) finally: os.unlink(filename) def test_renumber_muxed(self): pages = [OggPage() for i in range(10)] for seq, page in enumerate(pages[0:1] + pages[2:]): page.serial = 0 page.sequence = seq pages[1].serial = 2 pages[1].sequence = 100 data = BytesIO(b"".join([page.write() for page in pages])) OggPage.renumber(data, 0, 20) data.seek(0) pages = [OggPage(data) for i in range(10)] self.failUnlessEqual(pages[1].serial, 2) self.failUnlessEqual(pages[1].sequence, 100) pages.pop(1) self.failUnlessEqual( [page.sequence for page in pages], list(range(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 try: for i in range(10): num_packets = random.randrange(2, 100) lengths = [random.randrange(10, 10000) for i in range(num_packets)] packets = list(map(random_file.read, lengths)) self.failUnlessEqual( packets, OggPage.to_packets(OggPage.from_packets(packets))) finally: random_file.close() 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 range(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_none_finishing(self): page = OggPage() page.position = -1 data = BytesIO(page.write()) assert OggPage.find_last(data, page.serial, finishing=True) is None def test_find_last_none_finishing_mux(self): page1 = OggPage() page1.last = True page1.position = -1 page2 = OggPage() page2.serial = page1.serial + 1 pages = [page1, page2] data = BytesIO(b"".join([page.write() for page in pages])) assert OggPage.find_last(data, page1.serial, finishing=True) is None assert OggPage.find_last(data, page2.serial, finishing=True) == page2 def test_find_last_last_empty(self): # https://github.com/quodlibet/mutagen/issues/308 pages = [OggPage() for i in range(10)] for i, page in enumerate(pages): page.sequence = i page.position = i pages[-1].last = True pages[-1].position = -1 data = BytesIO(b"".join([page.write() for page in pages])) page = OggPage.find_last(data, pages[-1].serial, finishing=True) assert page is not None assert page.position == 8 page = OggPage.find_last(data, pages[-1].serial, finishing=False) assert page is not None assert page.position == -1 def test_find_last_single_muxed(self): page1 = OggPage() page1.last = True page2 = OggPage() page2.serial = page1.serial + 1 pages = [page1, page2] data = BytesIO(b"".join([page.write() for page in pages])) assert OggPage.find_last(data, page2.serial).serial == page2.serial def test_find_last_really_last(self): pages = [OggPage() for i in range(10)] pages[-1].last = True for i, page in enumerate(pages): page.sequence = i data = 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 range(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 range(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. # https://github.com/quodlibet/mutagen/issues/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(OggError, self.Kind, os.path.join(DATA_DIR, 'xing.mp3')) def test_invalid_delete(self): self.failUnlessRaises(OggError, self.audio.delete, os.path.join(DATA_DIR, 'xing.mp3')) def test_invalid_save(self): self.failUnlessRaises(OggError, 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") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_oggflac.py0000664000175000017500000000645100000000000017656 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from io import BytesIO from mutagen.oggflac import OggFLAC, OggFLACStreamInfo, delete, error from mutagen.ogg import OggPage, error as OggError from tests import TestCase, DATA_DIR, get_temp_copy 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): self.filename = get_temp_copy(os.path.join(DATA_DIR, "empty.oggflac")) 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): with open(self.filename, "rb") as h: page = OggPage(h).write() page = page.replace(b"fLaC", b"!fLa", 1) self.failUnlessRaises(error, OggFLACStreamInfo, BytesIO(page)) def test_streaminfo_too_short(self): with open(self.filename, "rb") as h: page = OggPage(h).write() self.failUnlessRaises(OggError, OggFLACStreamInfo, BytesIO(page[:10])) def test_streaminfo_bad_version(self): with open(self.filename, "rb") as h: page = OggPage(h).write() page = page.replace(b"\x01\x00", b"\x02\x00", 1) self.failUnlessRaises(error, OggFLACStreamInfo, BytesIO(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(error, type(self.audio), fn) self.failUnlessRaises(error, self.audio.save, fn) self.failUnlessRaises(error, 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")) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_oggopus.py0000664000175000017500000000505300000000000017734 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from io import BytesIO from mutagen.oggopus import OggOpus, OggOpusInfo, delete, error from mutagen.ogg import OggPage from tests import TestCase, DATA_DIR, get_temp_copy from tests.test_ogg import TOggFileTypeMixin class TOggOpus(TestCase, TOggFileTypeMixin): Kind = OggOpus def setUp(self): self.filename = get_temp_copy(os.path.join(DATA_DIR, "example.opus")) 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): with open(self.filename, "rb") as h: page = OggPage(h) page.first = False self.failUnlessRaises(error, OggOpusInfo, BytesIO(page.write())) def test_unsupported_version(self): with open(self.filename, "rb") as h: page = OggPage(h) 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(error, 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_oggspeex.py0000664000175000017500000000413400000000000020071 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os import shutil from io import BytesIO from mutagen.ogg import OggPage from mutagen.oggspeex import OggSpeex, OggSpeexInfo, delete, error from tests import TestCase, DATA_DIR, get_temp_copy from tests.test_ogg import TOggFileTypeMixin class TOggSpeex(TestCase, TOggFileTypeMixin): Kind = OggSpeex def setUp(self): self.filename = get_temp_copy(os.path.join(DATA_DIR, "empty.spx")) 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): with open(self.filename, "rb") as h: page = OggPage(h) page.first = False self.failUnlessRaises(error, OggSpeexInfo, BytesIO(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(error, type(self.audio), fn) self.failUnlessRaises(error, self.audio.save, fn) self.failUnlessRaises(error, 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_oggtheora.py0000664000175000017500000000446700000000000020240 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from io import BytesIO from mutagen.oggtheora import OggTheora, OggTheoraInfo, delete, error from mutagen.ogg import OggPage from tests import TestCase, DATA_DIR, get_temp_copy from tests.test_ogg import TOggFileTypeMixin class TOggTheora(TestCase, TOggFileTypeMixin): Kind = OggTheora def setUp(self): self.filename = get_temp_copy( os.path.join(DATA_DIR, "sample.oggtheora")) 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): with open(self.filename, "rb") as h: page = OggPage(h) packet = page.packets[0] packet = packet[:7] + b"\x03\x00" + packet[9:] page.packets = [packet] fileobj = BytesIO(page.write()) self.failUnlessRaises(error, OggTheoraInfo, fileobj) def test_theora_not_first_page(self): with open(self.filename, "rb") as h: page = OggPage(h) page.first = False fileobj = BytesIO(page.write()) self.failUnlessRaises(error, 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(error, type(self.audio), fn) self.failUnlessRaises(error, self.audio.save, fn) self.failUnlessRaises(error, 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_oggvorbis.py0000664000175000017500000001455400000000000020260 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os import shutil from io import BytesIO from mutagen.ogg import OggPage from mutagen.oggvorbis import OggVorbis, OggVorbisInfo, delete, error from tests import TestCase, DATA_DIR, get_temp_copy from tests.test_ogg import TOggFileTypeMixin class TOggVorbis(TestCase, TOggFileTypeMixin): Kind = OggVorbis def setUp(self): self.filename = get_temp_copy(os.path.join(DATA_DIR, "empty.ogg")) 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): with open(self.filename, "rb") as h: page = OggPage(h) page.first = False self.failUnlessRaises(error, OggVorbisInfo, BytesIO(page.write())) def test_avg_bitrate(self): with open(self.filename, "rb") as h: page = OggPage(h) 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(BytesIO(page.write())) self.failUnlessEqual(info.bitrate, 32768) def test_overestimated_bitrate(self): with open(self.filename, "rb") as h: page = OggPage(h) 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(BytesIO(page.write())) self.failUnlessEqual(info.bitrate, 65536) def test_underestimated_bitrate(self): with open(self.filename, "rb") as h: page = OggPage(h) 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(BytesIO(page.write())) self.failUnlessEqual(info.bitrate, 65536) def test_negative_bitrate(self): with open(self.filename, "rb") as h: page = OggPage(h) 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(BytesIO(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(error, type(self.audio), fn) self.failUnlessRaises(error, self.audio.save, fn) self.failUnlessRaises(error, 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574016271.0 mutagen-1.45.1/tests/test_optimfrog.py0000644000175000017500000000451600000000000020260 0ustar00lazkalazka00000000000000# -*- 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")) self.ofr_5100 = OptimFROG( os.path.join(DATA_DIR, "silence-2s-44100-16.ofr")) self.ofs_5100 = OptimFROG( os.path.join(DATA_DIR, "silence-2s-44100-16.ofs")) def test_channels(self): self.failUnlessEqual(self.ofr.info.channels, 2) self.failUnlessEqual(self.ofs.info.channels, 2) self.failUnlessEqual(self.ofr_5100.info.channels, 2) self.failUnlessEqual(self.ofs_5100.info.channels, 2) def test_sample_rate(self): self.failUnlessEqual(self.ofr.info.sample_rate, 44100) self.failUnlessEqual(self.ofs.info.sample_rate, 44100) self.failUnlessEqual(self.ofr_5100.info.sample_rate, 44100) self.failUnlessEqual(self.ofs_5100.info.sample_rate, 44100) def test_bits_per_sample(self): self.failUnlessEqual(self.ofr.info.bits_per_sample, 16) self.failUnlessEqual(self.ofs.info.bits_per_sample, 16) self.failUnlessEqual(self.ofr_5100.info.bits_per_sample, 16) self.failUnlessEqual(self.ofs_5100.info.bits_per_sample, 16) def test_length(self): self.failUnlessAlmostEqual(self.ofr.info.length, 3.68, 2) self.failUnlessAlmostEqual(self.ofs.info.length, 3.68, 2) self.failUnlessAlmostEqual(self.ofr_5100.info.length, 2.0, 2) self.failUnlessAlmostEqual(self.ofs_5100.info.length, 2.0, 2) def test_encoder_info(self): self.failUnlessEqual(self.ofr.info.encoder_info, "4.520") self.failUnlessEqual(self.ofs.info.encoder_info, "4.520") self.failUnlessEqual(self.ofr_5100.info.encoder_info, "5.100") self.failUnlessEqual(self.ofs_5100.info.encoder_info, "5.100") 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()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/test_smf.py0000644000175000017500000000124600000000000017034 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from mutagen.smf import SMF, SMFError from tests import TestCase, DATA_DIR class TSMF(TestCase): def setUp(self): self.audio = SMF(os.path.join(DATA_DIR, "sample.mid")) def test_length(self): self.failUnlessAlmostEqual(self.audio.info.length, 127.997, 2) def test_not_my_file(self): self.failUnlessRaises( SMFError, SMF, os.path.join(DATA_DIR, "empty.ogg")) def test_pprint(self): self.audio.pprint() self.audio.info.pprint() def test_mime(self): self.assertTrue("audio/x-midi" in self.audio.mime) self.assertTrue("audio/midi" in self.audio.mime) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574016271.0 mutagen-1.45.1/tests/test_tak.py0000644000175000017500000000331700000000000017027 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from mutagen.tak import TAK, TAKHeaderError from tests import TestCase, DATA_DIR class TTAK(TestCase): def setUp(self): self.tak_no_tags = TAK(os.path.join(DATA_DIR, "silence-44-s.tak")) self.tak_tags = TAK(os.path.join(DATA_DIR, "has-tags.tak")) def test_channels(self): self.failUnlessEqual(self.tak_no_tags.info.channels, 2) self.failUnlessEqual(self.tak_tags.info.channels, 2) def test_length(self): self.failUnlessAlmostEqual(self.tak_no_tags.info.length, 3.68, delta=0.009) self.failUnlessAlmostEqual(self.tak_tags.info.length, 0.08, delta=0.009) def test_sample_rate(self): self.failUnlessEqual(self.tak_no_tags.info.sample_rate, 44100) self.failUnlessEqual(self.tak_tags.info.sample_rate, 44100) def test_bits_per_sample(self): self.failUnlessEqual(self.tak_no_tags.info.bits_per_sample, 16) self.failUnlessAlmostEqual(self.tak_tags.info.bits_per_sample, 16) def test_encoder_info(self): self.failUnlessEqual(self.tak_no_tags.info.encoder_info, "TAK 2.3.0") self.failUnlessEqual(self.tak_tags.info.encoder_info, "TAK 2.3.0") def test_not_my_file(self): self.failUnlessRaises( TAKHeaderError, TAK, os.path.join(DATA_DIR, "empty.ogg")) self.failUnlessRaises( TAKHeaderError, TAK, os.path.join(DATA_DIR, "click.mpc")) def test_mime(self): self.failUnless("audio/x-tak" in self.tak_no_tags.mime) def test_pprint(self): self.failUnless(self.tak_no_tags.pprint()) self.failUnless(self.tak_tags.pprint()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_tools.py0000664000175000017500000000251500000000000017411 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import sys import importlib from io import StringIO from mutagen._senf import fsnative from tests import TestCase def get_var(tool_name, entry="main"): mod = importlib.import_module( "mutagen._tools.%s" % tool_name.replace("-", "_")) return getattr(mod, entry) class _TTools(TestCase): TOOL_NAME = None def setUp(self): self.assertTrue(isinstance(self.TOOL_NAME, str)) 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(isinstance(arg, fsnative)) 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() 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/test_tools_mid3cp.py0000644000175000017500000001401500000000000020644 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- # Copyright 2014 Ben Ockmore # # 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. """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 import mutagen.id3 from mutagen.id3 import ID3, ParseID3v1 from mutagen._senf 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_merge(self): id3 = ID3(self.filename) id3.delete() id3.add(mutagen.id3.TALB(text=[u"foo"])) id3.save(v2_version=3) target = ID3() target.add(mutagen.id3.TPE1(text=[u"bar", u"quux"])) target.save(self.blank_file, v2_version=4) res, out, err = self.call2( self.filename, self.blank_file, fsn(u"--merge")) assert not any([res, out, err]) result = ID3(self.blank_file) assert result.version == (2, 4, 0) assert result.getall("TALB")[0].text == [u"foo"] assert result.getall("TPE1")[0].text == [u"bar", u"quux"] def test_merge_dst_no_tag(self): id3 = ID3(self.filename) id3.delete() id3.save(v2_version=3) with open(self.blank_file, "wb") as h: h.write(b"SOMEDATA") res, out, err = self.call2( self.filename, self.blank_file, fsn(u"--merge")) assert not any([res, out, err]) result = ID3(self.blank_file) assert result.version == (2, 3, 0) 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.assertEqual(copied_id3.version, (2, 3, 0)) # XXX: the v2.3 frame contains duplicate TPE1 frames which get merged # when saving to v2.3 again frame = copied_id3["TPE1"] frame.text = frame.text[0].split("/") 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)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_tools_mid3iconv.py0000664000175000017500000000542500000000000021367 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from mutagen.id3 import ID3 from mutagen._senf import fsnative as fsn from tests.test_tools import _TTools from tests import DATA_DIR, get_temp_copy 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() self.filename = get_temp_copy( os.path.join(DATA_DIR, fsn(u'silence-44-s.mp3'))) 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(str(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(str(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)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_tools_mid3v2.py0000664000175000017500000002652100000000000020600 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from tempfile import mkstemp import shutil import locale import mutagen from mutagen.id3 import ID3 from mutagen._senf import fsnative as fsn 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 isinstance(self.filename, fsn) 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_txxx_multiple(self): res, out = self.call( fsn(u"--TXXX"), fsn(u"A:B"), fsn(u"--TXXX"), fsn(u"C:D"), self.filename) self.failUnlessEqual((res, out), (0, "")) f = ID3(self.filename) assert len(f.getall("TXXX")) == 2 def test_wcom(self): res, out = self.call(fsn(u"--WCOM"), fsn(u"foo"), self.filename) self.failUnlessEqual((res, out), (0, "")) f = ID3(self.filename) frames = f.getall("WCOM") assert len(frames) == 1 assert frames[0].url == "foo" def test_wcom_multiple(self): res, out = self.call( fsn(u"--WCOM"), fsn(u"foo"), fsn(u"--WCOM"), fsn(u"bar"), self.filename) self.failUnlessEqual((res, out), (0, "")) f = ID3(self.filename) frames = f.getall("WCOM") assert len(frames) == 1 assert frames[0].url == "bar" def test_wxxx(self): res, out = self.call(fsn(u"--WXXX"), fsn(u"foobar"), self.filename) self.failUnlessEqual((res, out), (0, "")) f = ID3(self.filename) frames = f.getall("WXXX") assert len(frames) == 1 assert frames[0].url == "foobar" def test_wxxx_escape(self): res, out = self.call( fsn(u"-e"), fsn(u"--WXXX"), fsn(u"http\\://example.com/"), self.filename) self.failUnlessEqual((res, out), (0, "")) f = ID3(self.filename) frames = f.getall("WXXX") assert frames[0].url == "http://example.com/" def test_wxxx_multiple(self): res, out = self.call( fsn(u"--WXXX"), fsn(u"A:B"), fsn(u"--WXXX"), fsn(u"C:D"), self.filename) self.failUnlessEqual((res, out), (0, "")) f = ID3(self.filename) frames = sorted(f.getall("WXXX"), key=lambda f: f.HashKey) assert len(frames) == 2 assert frames[0].url == "B" assert frames[0].desc == "A" assert frames[1].url == "D" assert frames[1].desc == "C" def test_ufid(self): res, out, err = self.call2( fsn(u"--UFID"), fsn(u"foo:bar"), self.filename) self.assertEqual((res, out, err), (0, "", "")) f = ID3(self.filename) frame = f.getall("UFID:foo")[0] self.assertEqual(frame.owner, u"foo") self.assertEqual(frame.data, b"bar") 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_USLT(self): res, out = self.call(fsn(u"--USLT"), fsn(u"Y:foo"), self.filename) assert (res, out) == (0, "") f = ID3(self.filename) frame = f.getall("USLT:Y")[0] assert frame.desc == "Y" assert frame.text == "foo" assert frame.lang == "eng" res, out = self.call(fsn(u"--USLT"), fsn(u"Z:bar:ger"), self.filename) assert (res, out) == (0, "") f = ID3(self.filename) frame = f.getall("USLT:Z")[0] assert frame.desc == "Z" assert frame.text == "bar" assert frame.lang == "ger" res, out = self.call(fsn(u"--USLT"), fsn(u"X"), self.filename) assert (res, out) == (0, "") f = ID3(self.filename) frame = f.getall("USLT:")[0] assert frame.desc == "" assert frame.text == "X" assert frame.lang == "eng" def test_apic(self): image_path = os.path.join(DATA_DIR, "image.jpg") image_path = os.path.relpath(image_path) res, out, err = self.call2( fsn(u"--APIC"), image_path + fsn(u":fooAPIC:3:image/jpeg"), self.filename) self.failUnlessEqual((res, out, err), (0, "", "")) with open(image_path, "rb") as h: data = h.read() f = ID3(self.filename) frame = f.getall("APIC:fooAPIC")[0] self.assertEqual(frame.desc, u"fooAPIC") self.assertEqual(frame.mime, "image/jpeg") self.assertEqual(frame.data, data) res, out = self.call(fsn(u"--list"), self.filename) self.assertEqual(res, 0) self.assertTrue("fooAPIC" in out) def test_encoding_with_escape(self): text = u'\xe4\xf6\xfc' res, out = self.call(fsn(u"-e"), fsn(u"-a"), text, self.filename) self.failUnlessEqual((res, out), (0, "")) f = ID3(self.filename) 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 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": 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"öäü")) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_tools_moggsplit.py0000664000175000017500000000231500000000000021474 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from mutagen._senf import fsnative as fsn from tests.test_tools import _TTools from tests import DATA_DIR, get_temp_copy class TMOggSPlit(_TTools): TOOL_NAME = u"moggsplit" def setUp(self): super(TMOggSPlit, self).setUp() self.filename = get_temp_copy( os.path.join(DATA_DIR, fsn(u'multipagecomment.ogg'))) # append the second file with open(self.filename, "ab") as first: to_append = os.path.join( DATA_DIR, fsn(u'multipage-setup.ogg')) with open(to_append, "rb") as second: first.write(second.read()) 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(str(stream)) + fsn(u".ogg")) self.failUnless(os.path.exists(stream_path)) os.unlink(stream_path) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/test_tools_mutagen_inspect.py0000644000175000017500000000120200000000000022644 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os import glob from tests.test_tools import _TTools from mutagen._senf 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/test_tools_mutagen_pony.py0000644000175000017500000000057600000000000022201 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from tests.test_tools import _TTools from mutagen._senf import fsnative class TMutagenPony(_TTools): TOOL_NAME = u"mutagen-pony" def test_basic(self): base = os.path.join(fsnative(u'tests'), fsnative(u'data')) res, out = self.call(base) self.failIf(res) self.failUnless("Report for %s" % base in out) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_tools_util.py0000664000175000017500000000311600000000000020444 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- from mutagen._tools._util import split_escape from tests import TestCase 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, str) for p in parts)) parts = split_escape(u"", u":") self.assertEqual(parts, [u""]) self.assertTrue(all(isinstance(p, str) for p in parts)) parts = split_escape(u":", u":") self.assertEqual(parts, [u"", u""]) self.assertTrue(all(isinstance(p, str) for p in parts)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/test_trueaudio.py0000644000175000017500000000264700000000000020256 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from mutagen.trueaudio import TrueAudio, delete, error from mutagen.id3 import TIT1 from tests import TestCase, DATA_DIR, get_temp_copy 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(error, 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): filename = get_temp_copy(self.audio.filename) try: 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) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_wave.py0000664000175000017500000002370000000000000017212 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from mutagen.wave import WAVE, InvalidChunk from tests import TestCase, DATA_DIR, get_temp_copy class TWave(TestCase): def setUp(self): fn_wav_pcm_2s_16000_08_id3v23 = \ os.path.join(DATA_DIR, "silence-2s-PCM-16000-08-ID3v23.wav") self.wav_pcm_2s_16000_08_ID3v23 = \ WAVE(fn_wav_pcm_2s_16000_08_id3v23) self.tmp_fn_pcm_2s_16000_08_ID3v23 = \ get_temp_copy(fn_wav_pcm_2s_16000_08_id3v23) self.tmp_wav_pcm_2s_16000_08_ID3v23 = \ WAVE(self.tmp_fn_pcm_2s_16000_08_ID3v23) self.fn_wav_pcm_2s_16000_08_notags = \ os.path.join(DATA_DIR, "silence-2s-PCM-16000-08-notags.wav") self.wav_pcm_2s_16000_08_notags = \ WAVE(self.fn_wav_pcm_2s_16000_08_notags) self.tmp_fn_pcm_2s_16000_08_notag = \ get_temp_copy(self.fn_wav_pcm_2s_16000_08_notags) self.tmp_wav_pcm_2s_16000_08_notag = \ WAVE(self.tmp_fn_pcm_2s_16000_08_notag) fn_wav_pcm_2s_44100_16_id3v23 = \ os.path.join(DATA_DIR, "silence-2s-PCM-44100-16-ID3v23.wav") self.wav_pcm_2s_44100_16_ID3v23 = WAVE(fn_wav_pcm_2s_44100_16_id3v23) def test_channels(self): self.failUnlessEqual(self.wav_pcm_2s_16000_08_ID3v23.info.channels, 2) self.failUnlessEqual(self.wav_pcm_2s_44100_16_ID3v23.info.channels, 2) def test_sample_rate(self): self.failUnlessEqual(self.wav_pcm_2s_16000_08_ID3v23.info.sample_rate, 16000) self.failUnlessEqual(self.wav_pcm_2s_44100_16_ID3v23.info.sample_rate, 44100) def test_number_of_samples(self): self.failUnlessEqual(self.wav_pcm_2s_16000_08_ID3v23. info._number_of_samples, 32000) self.failUnlessEqual(self.wav_pcm_2s_44100_16_ID3v23. info._number_of_samples, 88200) def test_bits_per_sample(self): self.failUnlessEqual(self.wav_pcm_2s_16000_08_ID3v23. info.bits_per_sample, 8) self.failUnlessEqual(self.wav_pcm_2s_44100_16_ID3v23. info.bits_per_sample, 16) def test_bitrate(self): self.failUnlessEqual(self.wav_pcm_2s_16000_08_ID3v23. info.bitrate, 64000) self.failUnlessEqual(self.wav_pcm_2s_44100_16_ID3v23. info.bitrate, 352800) def test_length(self): self.failUnlessAlmostEqual(self.wav_pcm_2s_16000_08_ID3v23.info.length, 2.0, 2) self.failUnlessAlmostEqual(self.wav_pcm_2s_44100_16_ID3v23.info.length, 2.0, 2) def test_not_my_file(self): self.failUnlessRaises( InvalidChunk, WAVE, os.path.join(DATA_DIR, "empty.ogg")) def test_pprint(self): self.wav_pcm_2s_44100_16_ID3v23.pprint() def test_mime(self): self.failUnless("audio/wav" in self.wav_pcm_2s_44100_16_ID3v23.mime) self.failUnless("audio/wave" in self.wav_pcm_2s_44100_16_ID3v23.mime) def test_id3_tags(self): id3 = self.wav_pcm_2s_44100_16_ID3v23.tags self.assertEquals(id3["TALB"], "Quod Libet Test Data") self.assertEquals(id3["TCON"], "Silence") self.assertEquals(id3["TIT2"], "Silence") self.assertEquals(id3["TPE1"], ["piman / jzig"]) def test_id3_tags_uppercase_chunk(self): id3 = self.wav_pcm_2s_16000_08_ID3v23 self.assertEquals(id3["TALB"], "Quod Libet Test Data") self.assertEquals(id3["TCON"], "Silence") self.assertEquals(id3["TIT2"], "Silence") self.assertEquals(id3["TPE1"], ["piman / jzig"]) def test_delete(self): self.tmp_wav_pcm_2s_16000_08_ID3v23.delete() self.failIf(self.tmp_wav_pcm_2s_16000_08_ID3v23.tags) self.failUnless(WAVE(self.tmp_fn_pcm_2s_16000_08_ID3v23).tags is None) def test_save_no_tags(self): self.tmp_wav_pcm_2s_16000_08_ID3v23.tags = None self.tmp_wav_pcm_2s_16000_08_ID3v23.save() self.assertTrue(self.tmp_wav_pcm_2s_16000_08_ID3v23.tags is None) def test_add_tags_already_there(self): self.failUnless(self.tmp_wav_pcm_2s_16000_08_ID3v23.tags) self.failUnlessRaises(Exception, self.tmp_wav_pcm_2s_16000_08_ID3v23.add_tags) def test_roundtrip(self): self.failUnlessEqual(self.tmp_wav_pcm_2s_16000_08_ID3v23["TIT2"], ["Silence"]) self.tmp_wav_pcm_2s_16000_08_ID3v23.save() new = WAVE(self.tmp_wav_pcm_2s_16000_08_ID3v23.filename) self.failUnlessEqual(new["TIT2"], ["Silence"]) def test_save_tags(self): from mutagen.id3 import TIT1 tags = self.tmp_wav_pcm_2s_16000_08_ID3v23.tags tags.add(TIT1(encoding=3, text="foobar")) tags.save() new = WAVE(self.tmp_wav_pcm_2s_16000_08_ID3v23.filename) self.failUnlessEqual(new["TIT1"], ["foobar"]) """" Simulate the way Picard writes and update tags """ def test_picard_lifecycle(self): path_tmp_wav_file = \ get_temp_copy(self.fn_wav_pcm_2s_16000_08_notags) from mutagen.id3 import ID3 wav = WAVE(path_tmp_wav_file) id3 = wav.tags """" Picard WaveFile._get_tags: """ self.assertIsNone(id3, "Ensure ID3-tag-header does not exist") """" Picard WaveFile._get_tags: initialize tags """ wav.add_tags() id3 = wav.tags self.assertIsInstance(id3, ID3) """ ID3v2.3 separator """ separator = '/' """ Initialize Picard like metadata tags """ self.__init_id3_tags(id3, major=3) """ Write the Picard like metadata to the empty WAVE-file """ id3.save(path_tmp_wav_file, v23_sep=separator) """ Tags (metadata) have been added; now load the file again """ wav = WAVE(path_tmp_wav_file) id3 = wav.tags self.assertIsInstance(id3, ID3) self.assertEquals(id3["TRCK"], "1/10") self.assertEquals(id3["TPOS"], "1/1") self.assertEquals(id3["TXXX:MusicBrainz Release Group Id"], "e00305af-1c72-469b-9a7c-6dc665ca9adc") self.assertEquals(id3["TXXX:MusicBrainz Album Artist Id"], [ "3fe817fc-966e-4ece-b00a-76be43e7e73c", "984f8239-8fe1-4683-9c54-10ffb14439e9"]) self.assertEquals(id3["TXXX:CATALOGNUMBER"], ["PRAR931391"]) self.assertEquals(id3["TSRC"], ["NLB931100460", "USMH51100098"]) @staticmethod def __init_id3_tags(id3, major=3): """ Attributes: id3 ID3 Tag object major ID3 major version, e.g.: 3 for ID3v2.3 """ from mutagen.id3 import TRCK, TPOS, TXXX, TPUB, TALB, UFID, TPE2, \ TSO2, TMED, TIT2, TPE1, TSRC, IPLS, TORY, TDAT, TYER id3.add(TRCK(encoding=major, text="1/10")) id3.add(TPOS(encoding=major, text="1/1")) id3.add(TXXX(encoding=major, desc="MusicBrainz Release Group Id", text="e00305af-1c72-469b-9a7c-6dc665ca9adc")) id3.add(TXXX(encoding=major, desc="originalyear", text="2011")) id3.add(TXXX(encoding=major, desc="MusicBrainz Album Type", text="album")) id3.add(TXXX(encoding=major, desc="MusicBrainz Album Id", text="e7050302-74e6-42e4-aba0-09efd5d431d8")) id3.add(TPUB(encoding=major, text="J&R Adventures")) id3.add(TXXX(encoding=major, desc="CATALOGNUMBER", text="PRAR931391")) id3.add(TALB(encoding=major, text="Don\'t Explain")) id3.add(TXXX(encoding=major, desc="MusicBrainz Album Status", text="official")) id3.add(TXXX(encoding=major, desc="SCRIPT", text="Latn")) id3.add(TXXX(encoding=major, desc="MusicBrainz Album Release Country", text="US")) id3.add(TXXX(encoding=major, desc="BARCODE", text="804879313915")) id3.add(TXXX(encoding=major, desc="MusicBrainz Album Artist Id", text=[ "3fe817fc-966e-4ece-b00a-76be43e7e73c", "984f8239-8fe1-4683-9c54-10ffb14439e9"])) id3.add(TPE2(encoding=major, text="Beth Hart & Joe Bonamassa")) id3.add(TSO2(encoding=major, text="Hart, Beth & Bonamassa, Joe")) id3.add(TXXX(encoding=major, desc="ASIN", text="B005NPEUB2")) id3.add(TMED(encoding=major, text="CD")) id3.add(UFID(encoding=major, owner="http://musicbrainz.org", data=b"f151cb94-c909-46a8-ad99-fb77391abfb8")) id3.add(TIT2(encoding=major, text="Sinner's Prayer")) id3.add(TXXX(encoding=major, desc="MusicBrainz Artist Id", text=[ "3fe817fc-966e-4ece-b00a-76be43e7e73c", "984f8239-8fe1-4683-9c54-10ffb14439e9"])) id3.add(TPE1(encoding=major, text=["Beth Hart & Joe Bonamassa"])) id3.add(TXXX(encoding=major, desc="Artists", text=["Beth Hart", "Joe Bonamassa"])) id3.add(TSRC(encoding=major, text=["NLB931100460", "USMH51100098"])) id3.add(TXXX(encoding=major, desc="MusicBrainz Release Track Id", text="d062f484-253c-374b-85f7-89aab45551c7")) id3.add(IPLS(encoding=major, people=[ ["engineer", "James McCullagh"], ["engineer", "Jared Kvitka"], ["arranger", "Jeff Bova"], ["producer", "Roy Weisman"], ["piano", "Beth Hart"], ["guitar", "Blondie Chaplin"], ["guitar", "Joe Bonamassa"], ["percussion", "Anton Fig"], ["drums", "Anton Fig"], ["keyboard", "Arlan Schierbaum"], ["bass guitar", "Carmine Rojas"], ["orchestra", "The Bovaland Orchestra"], ["vocals", "Beth Hart"], ["vocals", "Joe Bonamassa"]])), id3.add(TORY(encoding=major, text="2011")) id3.add(TYER(encoding=major, text="2011")) id3.add(TDAT(encoding=major, text="2709")) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/test_wavpack.py0000664000175000017500000000467300000000000017714 0ustar00lazkalazka00000000000000# -*- coding: utf-8 -*- import os from mutagen.wavpack import WavPack, error as WavPackError 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_bits_per_sample(self): self.failUnlessEqual(self.audio.info.bits_per_sample, 16) def test_length(self): self.failUnlessAlmostEqual(self.audio.info.length, 3.68, 2) def test_not_my_file(self): self.failUnlessRaises( WavPackError, 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_bits_per_sample(self): self.failUnlessEqual(self.audio.info.bits_per_sample, 16) 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) class TWavPackDSD(TestCase): def setUp(self): self.audio = WavPack(os.path.join(DATA_DIR, "dsd.wv")) def test_version(self): self.failUnlessEqual(self.audio.info.version, 0x410) def test_channels(self): self.failUnlessEqual(self.audio.info.channels, 2) def test_sample_rate(self): self.failUnlessEqual(self.audio.info.sample_rate, 352800) def test_bits_per_sample(self): self.failUnlessEqual(self.audio.info.bits_per_sample, 1) def test_length(self): self.failUnlessAlmostEqual(self.audio.info.length, 0.01, 3) def test_pprint(self): self.audio.pprint() def test_mime(self): self.failUnless("audio/x-wavpack" in self.audio.mime)

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,^a././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/multipagecomment.ogg0000644000175000017500000041101600000000000021617 0ustar00lazkalazka00000000000000OggSۿ;ZfvorbisDOggSۿ;3bvorbisXiph.Org libVorbis I 20050304dbig=foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoOggSۿ;YobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoOggSۿ;obarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoOggSۿ;obarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoOggSۿ;obarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoOggSۿ;d@obarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoOggSۿ;,"obarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoOggSۿ;obarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoOggSۿ; vobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoOggSۿ; 9?obarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoOggSۿ; 'wobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoOggSۿ; ʒobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoOggSۿ; obarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoOggSۿ;\obarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoOggSۿ;߈obarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarwbigger=quuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuOggSۿ;xbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquOggSۿ;\uxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazqOggSۿ;Ȋ uuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazOggSۿ;dquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbaOggSۿ;%zquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbOggSۿ;|azquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxOggSۿ;bazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuOggSۿ;xbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquOggSۿ;"uxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazqOggSۿ;4OuuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazquuxbazOggSۿ;Ϭ>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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/multiplexed.spx0000644000175000017500000005743600000000000020653 0ustar00lazkalazka00000000000000OggS'ǑrPSpeex 1.1.12PDOggSd/da pretty dumb stream.OggS'[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 < ; < = ; = < ; = < = <! =" ;# =$ <0 ;1 <3 =4 <5 ;6 <: =; ;< == <? =@ <A =B ;C =D <F =G ;H >I ;J =K ;L =M ;N <X >Y <Z ;[ <^ =_ :` =a <b =c ;d <f =g <i =j ;k <n =o ;q <r =s <w =x <y =z ;{ <~ = ; < = < = ; < = < = ; < = : = < = ; < = ; = < > ; < = < ; < = < = < ; < ; = < = ; = < = < ; < > ; < = < ; < ; < = : = < = : = < ; < = < = ; = ; < = < ; = < = ; < ; = < = <# =$ <( =) ;* <+ ;, =- </ ;0 =1 <3 ;4 =5 <6 ;7 =8 ;9 <= => <@ ;A =B <L =M ;N <U >V <W ;X <Y =Z <[ ;\ <^ ;_ <b =c <d ;e <h =i <j ;k <m =n <r =s ;t <x ;y =z <~ = < = ; < > ; < = ; < = ; < = < = < = < = < = < > ; < ; < > < ; < = ; = < = ; < = ; < > ; < = ; = < = ; < = ; < = ; < = ; < = ; < = ; = ; = < = ; < = < = ; <# =$ <& =' ;( <* =+ ;, =- ;. <5 =7 ;8 <9 =: ;; =< <? =@ <F =G <O ;P =Q <S =T <X =Y <\ =] <e =f <i =j ;k <l =m ;n <r =s <u ;v =w <z ;{ <| =} < = < = ; < ; < = ; > ; < = < ; < = < = ; > ; < = < ; = ; = < = < ; = < ; < = : = < = ; < = ; < = ; < = < ; < = < = < ; = ; = < = < = < ; < = < = < = ; < = < = < ; = ; < = < : < = ;! <( ;) =* <+ =, ;- </ =0 <2 =3 <4 =5 ;7 =8 <; ;< <A =B <D ;E <J =K <Q =R :S =T <X >Y ;Z <\ =] <_ =` <g =h <j =k <n =o <u =v <x =y <| =} ;~ = ; < = < = < ; < ; > ; < = ; < = ; < > ; < ; < = < = < = < ; < = ; < = < = ; = < = ; < > ; = < = < ; = ; = < = ; = ; < = ; < = < = < ; < = < = ; = < = < = ; < = < = ; < = < =! ;" ># ;$ <& =' <) =* <+ =, ;- =. <3 =4 <8 =9 ;: <C ;D =E ;F <G =H <J =K ;L <R >S ;T =U <Z =[ ;\ <a =b ;c <d =e ;f <h =i ;k <l >m ;n =o ;p <r =s ;u =v <x =y ;z <} =~ < = < = < = < ; < = ; < = ; < = ; < = < = ; < = < = ; < = < = ; < = ; = < = ; < ; = < ; < = < ; = ; < = ; <VstszstcodccfbjaV/`?`O_6_^o^]]\%[ZZYYXXYWz/U?U/OT_SoSERRRQPP0O4ON"NM</L?KOJ_JoIGGQFJEoCCSB-A%@ ?j>/=Y?K N ^ n' ~ } T ) ;  .F > N ^ n ~' W 5 F . R   . > N ^ np ~X ې r N 8  Q   .Ӿ >ҩ Nѥ ^0 nb ~Y ̙ & " ? ʷ \)ȱ.t>*N^ěn~ìka Ag.>vN^n~Rjΰްdp^.B>uN^Un~mj'Σyޡ.>N"^n~Z^dΖ4ޖ#.>dN^n~ΊފNM.>N^n~ ~}}|{zmy~.x>x:Nw ^v=nuD~srrrrpoonn1mV.lW>kNj^inh~gffedcbaa `_._>^N]^]n\6~[\[YZaYW:VGU*T}SIRQ{.P>PNO=^NjnM"~L\KJJJJIGFFID.D>CNB^AcnA~@?>>1> =9=.;;L:M8.8>7N7^7n6&~54c43100g/8.-,.,>+7N*^*Pn*;~(((-'&&&%%/$1#."Y>!N!0^ n7~u~sJdw.t>dN^n~CT  @. m> N ^ gn [~  DJ. >M]]m}v @6U' -y=mMW]m0}e * - = M ] m } ^ ~ w ߏ   ! !!-܅!=ۢ!M5!]c!mT!}!b!־!~!M!a!Ҙ!!Ѡ" р""-"=H"MΓ"]"m"}ˣ"ʬ"ɛ"<"""[""3# # #-Þ#=;#M#])#mb#}.#;#x###ͻ#ݺP##$ q$g$-4$=\$MG$]W$m$} $$$ $$ͱ$ݱ2$$U% n%J%-%=%Mx%]%m%}%%%%R%ͤ%ݤ1%%E& &M&-)&=&M&]&ms&}&H&&&&͜K&ݛ&J&' ''-'= 'Mo']'m'} 'q'u'}''͏'ݏW'}'( ((-F(=(M(]^(m(}(((b((̓(݃-( (%) r)G)-h)=})M|)]{)mz)}z)z)zL)yj)x)x)w)v_)u* t*sc*-r*=qd*Mq *]q*mp*}o*o*nN*n*l*l*k*j*i+ iT+h7+-gv+=f+MfE+]f+md+}c+c+b +a+ay+a&+`+_+^, ],],-\,=[,M[|,]Y,mY,}X,W~,Vf,U4,S],R,QI,P,On- O-Nm--M-=M-MK-]Ku-mI-}I-Hr-G-F-F<-D-C-C!-C. B .@.-@>.=?.M?.]>.m>F.}>0.=a.<3.;g.:*.9.9q.8.8/ 8H/7/-6/=5/M4/]4z/m3G/}2/2 /0[/05//z/./-/-/,0 ,+0+0-+|0=*0M* 0])q0m({0}( 0'L0'I0'0%0%;0$0$.0#41 "61"1-!1= 1M1]L1m91}11q11;11112  22- 2=22M2]2ml2} 2 2 2 o2 2 2Z223  33-#3=3M3]3m3}333_3Q3b3X3)34 44,4<@4LX4\4l4|4 444&44l4W4\5 55,~5 >m>,><>L>\>l>| >>>a>8>~>~=>}~>|? |?|??,{w?@i@h@hV@g@g[A g0Ag#A,fzAI\IIIJ JpJ,PJ;iJKJ[Jk$J{JJJJJgJJJ~K KK+K;KK:K[KkK{RK6K{KKcK6KKKL BLL+ L;aLKL[Lk!L{LLLLLiLޔLݫLܑM ۽MM+0M; MKM[قMk[M{MMM|MSMԙMxMkM N oNΑN+N;^NKN[KNkN{ɨNN+NNONN!NNO OO+/O;OKZO[Ok8O{ OOO?OO˶O۵O봬strak\tkhd @$edtselst mdia mdhd 0hdlrtextSubtitleHandlerminfLgmhdgmin@,text@$dinfdref url stblKstsd;text ftabsttspDc* `o * $Q#6i7DO/3rjWe*R:S J"  B< n93lRc ) ]&n^s _;2e 3 e9# qdZ Y'ۉ' Jci5(i5M-O[= 2Y.5D%eX:&Yu+$h*%%LQ,)2 s R rstscpstszstco]ZKudtameta"hdlrmdirilst\namTdataThe Land: Predators: A LitRPG Saga: Chaos Seeds, Book 7 (Unabridged)#ARTdataAleron Kong#wrtdataNick PodehlGalb?dataThe Land: Predators: A LitRPG Saga (Unabridged)daydata2018&toodatainAudible 1.97cmtdataWelcome to the long-awaited seventh novel of the best-selling saga by Aleron Kong, the longest and best book ever recorded by Nick Podehl!!gendataAudiobookdescdataWelcome to the long-awaited seventh novel of the best-selling saga by Aleron Kong, the longest and best book ever recorded by Nick Podehl! Book 1 reached number one in the Audible Store in 2017!  A mesmerizing tale reminiscent of the wonder of Ready Player One and the adventure of Game of Thrones Number one in Epic Fantasy Number one in Cyberpunk Number one in Video Game Fantasy In The Land: Predators, the Mist Village has harnessed its power. With core buildings, professional fighters, and now their own Dungeon, the settlement is primed to grow into a kingdom of true power and magic. The path to power has not been without risk, however. The Mist Village has been noticed. Evil nobles from the Kingdom of Law, bloodthirsty goblins from the Serrated Mountains, an undead lord with a penchant for human sacrifice, and fanatical kobolds from the Depths all plot the village's destruction. The predators are circling. Richter's people are horribly outnumbered by foes whose own power has been entrenched for thousands of years. Richter and Sion need to be stronger than ever before. Luckily, they are. New skills have been learned, stronger enchantments have been wrought, and the hundreds of villagers have answered the call to adventure. The Companions do not stand alone. While many eyes have turned toward the mists, wanting to take the treasures within, the Mist Village stares back with a simple message. Come and get it!  Please contact Aleron. He LOVES hearing from people! He also does almost weekly giveaways! This is his contact info: Website: LitRPG.com Email: Richter@LitRPG.com Instagram and Twitter: @LitRPGBooks Amazon Author Page: amazon.com/author/www.litrpg.com Patreon: UW_Pb?h>^i]\<֋EO+ҝyFFhs"ץ-Q#7-d+9n7O[6c^;u uI<@M-|N61HMķ.3#`840c=+ͱ:r{A/sһ_ mJ[?M9[" A}KK2c%dX 3NUܙ']sO@G ZG/f5KkU9ۊStNiGP2GE4aV ݕ턊A@ NyI#jsK8k<UUR?Z.W/R.dw*jM"b\&~(g,&SU2hXIEZ]ў:=DsS&Ťq5(tU]?5j8ΧqA3f8qN[ 'F)cC!5 ޜJT3j2f.yFn B_LUdi#XxkʷkO @H I zdyn2L~! φ>utzT遊F\:o6jQMz aq3FثMu 0$U~5k^A Ah5i-.wμ6pLۓdA=3W 61Cy "cqƄ -UK6Jidt˸CJ\J98 ۙkObVb;"L}څT[kOK-OI#m8HY<Pti{zHm٦}>!W$ J̈E+/CAU\ՙ٪# 1F.G1|GzmO- {P`^_FbFM?=AE /*l\=bj7aZ[Vwb4•P1N].VQQZ RLP))r1ǿlӿ=֪VGmmK<y,OA]*: F׊IE&sRkgXjx?Dxl,;H rJ \ 5 j:No^* yex_iDH= r9Z2Tgk{G1ǿ{K XiwՐ%ܟb3^)mho,HPRs$e^ןKr{G;jwf1Mo6v)߈ &Ei(>KZŕCsO(f.Tq}'1ǿ~"f|VҼ3ElK2,2;s08zeh8Ut Ny'f?3I('8n*}mpGǿl{]Ξt?ᶕxjy5'Wx PK3a%-0wȎPrw(|g(S]^VU&S+[hn%(ЪJpzӆ:UtMҥ F?4ߑxB[k ׽^:d4`JzϊdldK\uSRAIhg#>jzF-8S 9# gXrWi1j'i9>`Nz5,ML<9UqǠ##5W$hcr-H9 ~_6\T܍N>[)b]U]s;ZⳉdY%dNLEP_Һ-/WZŔw6qɒ3XeX80G"~dtzz~= 5+ Qo%[A ^~ ]j{es0@ 2yq\I#j,>%޻[Ɨp6NsֽkH5ecC0qּ7V:^thohd֚ҕ0~"2OnR;-˟R'sϨӱ>+Fޏ$BOWNx=ѰWhw{']HdɎA t>W2(M.TOKjszDĎ@Xލo>Ofc˘)S>7>qh4/$tbTG5eFy$N>ER+ yW'ЌU q]W%t*&R8OM/Ou'N,S >ι%p 8=9#٤3 S@%O _CPUcFxT6Z0!r:>4hМ ;J߼٦9٢p8oOJKɇGoƟy%I%I-hKSzS^Ϫ4<)2ݪeS_9$]9=h0(R7,T 5nj,s@yj2"+5{UX~-(Aڊ,>_2-;h\_^%2N<(s}Mokg 4&khރ9 > +[8<-pnVa*6<5e;^[jZ 2)@vZQճ{njo4O^M^[f0DCsG^-6{O,7G^X̮2GR3Y.d"$.=HacfkNa32JDzr ;6+:Rqm&kM-_[ Obs]C ,&R>a϶*;9M7w5_k7z,W {wkǸ,IϡW3f_[v0[1G6"τ?q]Ml&QPLf:λf{l%bQ! pxuN/վâ\-;d9v$,Eb8;q RQ|yW.4noZC<3RLXc!1rsjMLk0öXH 088/o5h|=VudRX9q݁1[sk_^i:sjp̝ݕ%@|3`SZm5s\\}U[e3LJaA$sԏN*|Ҷ9DT x`t x-4‰"` 8# ^Jv9;,Ov݊6~7֯cD'[>v4a ឭq${iVH~0g“kr#3Cgz_$Q=cWibXcr+^ݽmxc q9AtkޏoZքc#wOCt5斎 tO=Z[9"R 96\Mj?zh^-˂躽͹Ǚ@$#/M9Lwc׃W/o5ŕ]ZM$ dFQka&Rf[Av2֨3,s{^P{R~\Waym{ub-t95ʓZMkß QT5qV[_κ0~~?LW L&(8ѳ$RqQO&=ݧsksƼ9^}ϩ #=k㻽T *\1NTҽ̯  ?^bSנ|W+V%Lk \3M9jNk[Fפxbפilh<J}?So :4հN2 KtS/D97LT8\rpߤks`)-8`\i%x`$kw~&mPCIL$դb.hϭaK7w4ĢRn%pbop(擞 \QEasݥͤсF##uwRHkdHB ev;h?_0ֿߗS$hZs>p[X@8!%$ /{q^yw3M#GߗGMP5mʻFjWcnF88k/Oo4M6e dIc[~_ֿߗIB)]K+%Ll}}I @c8][^$3$V"o$0Fk1ضޗG$Cۢ!Hu;yVXim0ܐcȨ/KK)lanPNAP[89mG JN>^/ivp#5m-Q,8#^$8>xSM?M̝ggl,j:=>~7k) W>ۏҕєCÑJHȹ2=ϠO$tX>%RYؓC\]|1rNIX*rI$ڽƾ&Ծؔ LK+`9^J<'hWp*0pFI'ttF>6kg[L8\Kw954'Pnd)^֚O9;JKWxY,=K)'SאHuVyz]dOk%bB= ֹ;ڝǑsqYct2˙Bj0_& 4jч㓃ҺOBV&-$HntbccHySp}]5m0WJgAzW-IpY I u#ZóSd%Mc@DV%׿,'=E-/,Zj9wHI[>4ҡZ7Zep\[+2'>j\ާl;`̮* 5&cu\kXqB8`GWڟ֪鞗/X4?orW@ztn0{:y{6"# 8]qL!e;Y,cs ;}{E 7bix{~R?Ν< @G+mhϰ\ntC71CZ|?t9"'4X"i dF 8'SiܥQMs"C^ễ}j?Lම.lXwSl-t*FO12:|J[(541j+)#vy9o \(F$dq&dO(7e#3MT$i$s$^ ϊU+I ~UasɣYyA0.\Ϡt?Rc-6ʝe?#;fݬ\y.{;i-on}ϹV֥Uen% $/T > K{kmeo-dŔS0k]e=ƭuix!ldwnmEy $dAGcF+t7tcBI?Ǒǹy+aBwWRqޞ"!456Q֋x>)џʓ4'4hN N(`l8ң#?ZPoj4U"0PhFGJQJFij&(;Tqw֗b\Lъ\zTEʱyE2WܓS5|*w]7$\R3> /ͣCNׅH`}# Һ 51l{/Oړ%B$sfۇyߗoʺ[ΈkSt]ہ8~&hsp#:`zcq/+Gt DcoݮB{fTҌAM9z#'%AK:}x # h#4Dzƿ֫5M6T#!}<YY] YԆB;y H4 V\,6]$XĎ{ < {OqQW3{E݁'=Z4/s@Eα 01y6 u:ǩX3o*Y|pK8Ċ.8U6 eLY\  gڊ3 ],/tjV\u *#OFU ٢) 1+g#U>q}c\jyLi\4B=OΜM5XrAb掼v/z执궷|b2Gsr3PpA{i b%ڃԲID-NHjWWzl?ROAYU#2G~I# Cy? w<}i!DFTpA*Z/R(u 2KpO%ȑRy qEp!yQh[Bl8t8'HKmؽ/8Xǁ˜]ehfV&;81ڮ ®I/4;A(\ ^(k^"T1I?lzړQ5|l5_wkc$8}hCj?[w Hb] 74OMmFe#$vO"׎DHeI"3HpE="ZWGY_vbIMJY\_eco%ԧlqFbz Ork:6Rx [ӂ0{1(9C,QJue&ޮ% >9VR[+bd `Fj; EeҼ:ԉ=Ar6GABѽ 5PgCTuR#^$n ry \m:Mg鿑f^|@ӭ}m-Y9'YOW,WrɾLn==WS_ 쭷M}3} 0. n|Eq G[#?D]_J%F2<1=#1U|7|A56VE1ơ5ÙpUc֧1f[c=Ս9\(B}1YD{]{ȷ[bNK}+ݽ^_ 2AdyQpH$$zn^VY>}Oj: L ;P{c} ;ិ .*&2 2<$zk卶pJ ׺H&6-3Eagic0\qۥ)4qBxf.[5 )iyH_&9uwEEgh(="JE|ӕY]cpEo =E&AquA99듚O^[Lgu7RTnxV9Op \TLZuEG$6y=sEyKTX2iko)'1@Ydc`|P6ާYĿEā|9^ӵ{jUĐ\9W{]`E n1J+WL4^Xm@#R܂@ $Lb}3S.ys8<s^mV2K3- y>!x*'U'`FQ`&r8zfq\k &d;b~UqB5k7MuyY԰98{kk>'\xF¤?k.nQ*I4Ȍ1~&諥jp"e#p~ =y?`,}2{+ݼ]E ZHu_X[pǡA4(縧*} [?*GL֏.[teeJA\V_YdLqD`FN{N;w/Wɰ]{p=u z Ràl,{w44jKAT[J+wBWdAqOw?5v!GnAբ5~閱\j2$!v$@9$c @늧cJ}ؗZ r1և»!s\HWhG xxku޴RKwqthЎ(8 csN&Os_,vIo|w ƯSM{+AI3^s=.Ol]1$Kq.l`##S^K˻{{x*F2rI?{|h4Q">< {z׎>snj}nK-U]p0Tcc!pjġƣ>r}s۹Q%;)-#FG H Ӵf;]:YaIRO@=I^+I0[i2f$ tx䓌ӵy}=n̒195"ĨSnYؖb}Z|;ui:>&Y@@0Ju$0? Qifo&Y#s0$ O8;KH)@p9'I\$SE-/vv {ʦ9jF 8&\{Cjl|'Q֞7ͦ\),?Tw>oKj56fYrs)&5R6q6~kn.|z %?@ `=2a=Ǟk+˭cOU4QsC}kPf XXe9GNiݝV\/69<4Ӻ JVIҴm}oA-d &.ddn` \{-Z١XrIGzSB(kh^% GRZdEO֏,tWGV6(@\#j~(Gx-Dw]T?/i 4hC5hM;hn-9=(?Oƺ(/I-,l2:g8R00g-aRV&$\@$Î[JJ^Td2ȪOAkUgF΍gjDp?PC#<U h#Oe#.w##$c&"?{"~JwcWouOMɤmoFK4$AQҺ{WEB)eF`nbXpy <ҁ{5qqt&ODm밉cy`FTR ]%yY{@}7]@wwgQ(iQA mL㏮+txS6:>c[?g?3~uJOL&h$ܘݑxxZEOM9#8p;n 'ٳ[z$ #V8R6%,yvI֖%.5yS[L1P p<惪[i3Gm,7$1a@8^E[c GJ9\vz b1q:Ayig?:*]CTkigNkd LN|Wp{r: ס.Z]Iho"RqS@eA#h I r{j PS JX܉wDy 8 Ƨquw;4bw@]oY-فB+At~E6g)x̯+1%ğػw\Vq6i-=#G 57烎jƩzb-p O> 7W1qGJ\WuxSEXuXE*FqOfMĤǥ/'cޟHڛe7aN&w# LSm0jۻ Gߊ\P\va!"cqE(MJp(8=i\ޣU;})F MHoz& H7HE3P\&Ȯ:AW].ZX' n2h `'8~_*Y4|㟅Z܉[ԭE0Xi3K6I+ӯn.SM!3 Ɨ⋭+,V G;ckK8No7NrY#bjD14Q0 p”#1N7>ducd!Hֺ}wzΣOG:ėhi,3'4L=TwC"HX 3ZmB"tVXg }([Wn,Ņޯ+C)yqމ+ޠBqtu?F[ &M:g1$rT$`ÇF; aTkAX=ty'ۦQP\si[$:g4\_Xn,x[IpcU*n[[Ϭ7 .1ufpI++hˆ@M{ |`C;i#=ak;4eYLdRdL8>ճ 8jQȋHWB089jhclJ "PvbSzjkrג%.駜^j$Oves T ҳ3lhE#Ӄ}*GvZ^r`ExٰH@Adpj*Q&6)qK9_VC l'Up?:x@=c<'˞7!=Xݞ犛𢘮AaȦ8w "LU\~5Xcҡ"HDE0SQAWQJ61L"I8bNEJ MsA(dP-EtW&` ~tdE_,c>(G4beNQǐ{d|!qݩ"WW#n>!Ci+e'd 'txBR⻴\d=pR=+Ҭh'q 2=*O}+ZRN7l0ҵ;zt+Kmۂ{)'Z̓gkGm[pd@=Scl4=7kՕyu')<$`ߵh;bI,T0' űOjiVl4cQYV{duJM@s✝<ǽ ՠ#XO.zմgTXUhKzjSMSRRkB=/9$Q;uI44E||RVʑ 8Нi!)LJNISږ#g{5uT .jqrH$^j2?Z#ά 1AixtST=zVJUdӨ(&(E\1FcTnW?Xabދ0k[D mnp̀EpUOQ0(~]:n{9oBkWj3?k6=/,TVӿT~P?ֳDQEw8+%>qyA#0F^;~E>]*)jp%RpO|WY&; 2n1#ǷZ|3vl_2E, (v=}Rfx*G>mq-vp|ɉ=N;fK{yP<*c8Sk]E A\20=zsޟ,d/ ץooSY*GB,>l}YaLysrkYCp*1Ԑ8?j`V6i2WsP^v4LпT=A\ԩTU/&nz5Q)s5 JTH`N2qXQ| y'Bzޭw1jApaXope<C:j-SU-&,mX9t:4~"-47vqFJ> {T%)<RO^cZ,^}=OaO:ڄ)DVs?rʌv;)VekR /CbXSJޢf$'GJfYi^MzDy '8rߵ8!zv5 +M@=;Tt kevަ=,PBvvNONz]o5j|~'Mxj!Qg$t=Aw̎$c);姈5ה?e qߑ[MF) =^]H";f'Ǖ&w_e9ራ:A52K]lH9px# ȩRWGEL:y{loG` I9-u NE LXg~ d}j\nm&kHJ r:|NE2JS /28.Uvgnw5NvW[ŷo5mۈY陮0\A*Z{yPD8X'@Xw<TܩԮF}l(tn%K$(pI<֖:ai¤ǯ\r+K{3F=;CV].H2 H2 dަj? "xx?tS󄥷/qM*g@xxiYO&y*@_jz֋6q֠ӽ\ZIPl~I4IvX*{UmW0(PfeQ/z/Sj$ѩqh&_ K9vs>nu9f}:Ko.8y^R1zүELe -#nL~VYVmyAx5&_-|S[K-Httrh8XK,vOt|:G~/x$*=VdOY,"C.U>Q))NM820Qu,Cش{Iz;*tffFixʰDqG,+yr:ᑇ|c ϥkYY*VFdIew#Jw Ap@cv!ӭeUy=I˜5 -6A[# I{O/ nr K C{tXR5;=:XtWOY88>Zm-1qbn幃P d\uX. 2˴f$qГ\E9J+x RnhE>tLo))PTrqyZ!,OϽ^.XopddPY*ai=JՃpJC B-[^GtbϵYn+71BxOOjĴKbݏa\jdI Xչ oBxVS]7cj>ҵه$L^H-B sA( S~*rlΣ"c`(bs.^zδ4;KZ_n$yrm8$B>՞i$:5xt^KA`Hcwjeޔ#toY+1͹ y##wXՙ6L߳xr[޶cҮ.5BsO <RR{9-yB N bxɥc' # zTnKuw[vQf#Bpq*K ]_W{Y0 ԁ@9LbJ)Ǐ⺨2fN7Qh9pJu@6»ۂ8䎽=3vzs;8:zU gov(uGQ I9i=oS'z{  R*r6y%3gmihle&/7֎iR2sU]7NnC#~`q?@  []"=sۍ P"T_Z}:\v;*,I=GC\LpFY@br 5i6Thͻy?y5+#%[ $1E6ۈZ  U|7 BRyeH-tbi<7 &u*k6uđٶ%Asڹk55+{cg$(98T7 vgZx꿝[M/Q7W6hIhBp$dyRǠjkbP8G#4G$*zӭ,P-AZH\t1hėeΤ[ g" B[#zҳ9= 1(wSZ} "6ymR=֌.ϒqBGzh[$HsREHvX]C7FR'  *:M["_>w3ꠑPWgظ<޲j/L)ygؚ' ׿֜ObN7&99׆C֣<^>] "#|/Gbx{cϑ |%iIN7:; X(l=ܘۣ ?:u+jm*K9XFy3P`a~>]}{xU-oB ۩\lg9ȥXrn;Ij@21zM.!B@*S:aЎhևW"k-N0XI9GOַ/l B,n-),0’9$15=x3$h0ēJ\\8QM=@C';o4҃d=JOjj>gO0^GcK ^oI(Gprxϵh*H ֫ʙ\*Ő7MJXQ]۪V] GN5 679 AdcSsag@<ޔXZmew^iC >sF ~`}G5bDM é 8==NK٢Dק+: ձ>mʫщ3HWW G)|!nbBx9+KY…Q8koHmnS.W(%J:@{s!O99f$@QjurYHoITh `hе}>ʆ';Ԙci r0qzS] ÁץWty5x_j:,ycI Aݴ `~dV_3iCqzS٪%&Gy=Pk:&xO\nIpL`3gڹCZMQc,#ll,XI0O^q3WFh gKtn$էі͆ox7;p98#r xHkXxX{6˵R989z,ĩMDÿ?΢|=jv6:ꨰh(뽔91[ڶe<|NKpOjf#|+ Xׯ, ީUKciA= 6z>2 Lc͈پRIݎ1PIbK&0'3Pi\4ky,zaϿOƟ[}j\,dԵ:C4LMh2J>f5'Nq{RE^n-#O >^gru#'#qg/Tp-zW2E l \sS^@Vt,}qN5/%GN}0i[Xj.UAV9sMOizEgodmH$R>2͖0+-Xrl2D$G*rSYH|pDPF#Ž'=_Zo@z曪f*װޕ`!hް m72$?o4WVSLcrðrAns?^##g5cU$mk.)-|Iomm,z,C  C)CT`Լ?t&m5"Kض[@  >q+&[Dqުp낼vi 5F:ַ(xEѣ(Ij(.gEtpEOMp2)2zw^pq}qkyxg+EmwdeݎZM5m>{h9% xV8q\wz.vIkdo}!: y``:tgDm$lY7C aOOnr͵I SK{%t§vFjU_"!FGýMLF $`pg?V5^O&q29jsp#(U9f_%JoC˨Z,ml1Ny==mPND!b1ON*OJ|iKy'c^TPZ^HfD2 u$yk:ƙwoo=Yu80qX? `NcOt; mwDh%T /r۾6)q#hyy<{Dy Fwr2+֐j2$:#i ձl-y#*^rYy'uMsP>6K=>m)2ȩ08`f~P<㡪4۽;KoH?& 0cp3|>,t/$(JiY33txaJG ze}e;~!Gմ+Wk[=Q!׺\Yǜg'$p<zs{osj7~cގԩVqMHxHf(be_͞ǡ2H: [ݏ& ƃ`9#E-x9}^p9*Xq> IT5u$mқw=i-lg>f˳EG۷xcZ2==j9,Ĝf`tC'i?#L0rkN l&n?ΤAt5.b3آpj2^G,?UDG=%+2;+&ĩzGPx߽\pNHԷMnz]5Qx8}ki دbݍ1G^5',ռ />)]"9QʣH'򫎮̪tԥyA+,q.K;+񕅬,k}p+3Ll3JS$~Y;Vʒx(yjo\ZȎ!i_|+K;?ZH)43ݢ{sl?1ݑr;@ AFA ב;Rbvp?P^vz/ͭ.+Uvݵ@'xסZ=Z˹In2y<+`KRGޙL»/Y 0 G!AԐN{W"ޓ0qm3mҌcj*Jꍹm8vӺ<WړgAO/Z(G5PlX=C*BE5&݆LfzvM7ڴYjz)Q޴[\R|O&MD qSl%ЁTH|ƬI*"739`=OSTh"~LGQ;_%x4{T3cL h-ڢtzz; RbQaE1ٞIu֡S =8Jvfc>jMʣrN:?H8ݕOx$ƺ1$Hc;ȭd=Q,r|7%??Cç4Qyx=ZpZ`^= N-lcqڋI0[ցU"Oaf =ek7is g0?S^akUӔW ;s^CiP}cQӮ4BK+ĉņ?⺨Zڝ>Nڕi8qIҺ@(4PzQ(ǥ&h'4ҁց ރ;6Cǯ8ג֓fΘ=EL 9'l^?_?gOʀA#AP^MT`W 봌~is5.@%C~ 2:]f_ y+{q7bݥr6w%[Y|Hz|-5 I8 x~Ni++);A< u6Wuk 2}ϵcUM7(RWϹ%qK[!agpGLWEiľEdM1 :( o$t,.C"n3N3ҜaCk3XH3܀zEpTI߸S"HoZ[I m@r@O=~^H6J(^X]L0\q D+۷Vt2ʪN7L~Ng=\+Q%&IV&ޢT^zWj4{&QfFCԄm-wpҨ=l{#4_)q{R4˜ X kDKEGL'Oj \ڽNA5fvԮUydioE=Z'f$T) $UW-ʞ#'#4iZFS "S`zpW#fR0Zm8D"A r {żD228:ҌRq{$./ jܒ1R:֪8=N 1HB>cL tlG)hw=_WQO nM#=GU-w;JǟfRxGYH#O4ʻZ=wT[6/~b`}ICwjTsi`?UvYh)S輜}YTV/OE%;xCР9P'sJspc֜>T&MK$/*,&'Ўk5A%0_ha^y$Ib<&0}\vycis\ \A 檕DēOLp$~uxQ l5MbQk?TBL*#g?airOǜO8pcӭ 9SqMhz#GJQouI7Pu4@>d={WKmHzAs:Nii&q䛅b8$)*U }r9^O GQ#;=k^;HAU?J"E&1 KB4ޥ?OZНZ2=>lVvA "rJ4Sq]GRi ^-8PF y,}>~&^a3q# -7Ϋu&O`0JXp㧽s^Fa"䲸y6 R;ý-S2yQ '+á^Nw`|,ܒ6"/a3ϭ̱B#&)>Gң#5}amւB!c5s4_["QRb=n0ځ=?*zp;uhKE岱QuJ >q=6OFtR^}iݸ *~w=+ztbլ[+崝dTpH=Ydb@+0%@IsZ$bTx:M:ғ졣%C*@_i;dx-T5K[a HOA !fd0Fg'⢷ƍc޽IRcz(fi()TZ%r?\QUL!X=;;Csp#0G@=+Gl3Gtܷ*H>`յJ7w09'%vEE3ۘ{㠨ݧjo%9p:;uHhқrz\R C3B B $v}kѬmrE?m?^qxImFᑇb:W떥ʬw1Ml{Ҹn4\5+@ }})CFrrj4Z~~nhQ161=To8=M# =k&h\r19O^#k/XUo4OP*~kj-\sPw>mut3,;D pGQݗ(Gze&ˍW10=8I+62+ ]bow{zuB!!Ac\'|3uj2KK|H@82y♵M-m+1*e:A WG ڎQ} q}]_j$ ' drG>q Ň+OHnhC`jKeDp8z4gy:N0g', x'2~K}o]x&A{.[vnR~N@vH#jU [zApT"h{Hd_߈#]/lzΦ,`}6MR@>i}Q*jViE'(3R ,}W#pL)S-iqoCJ(OLQRBmn|ilO;7 δ.'Q>W<0z(I\#-,aȃMZ0ǥBt5 QAw;G5Yp{T3ȹf3ո܎GZ>G㧽[iiwvOxĉ +rsZ1OR: p3V1j(\Y5DR[p5q<%)6X?ppHZ`][Q?@Kn{G&#z#;Ǘ*z x=VЖy7_K霝Hd~^HU8'Ezp<8CҼ?-vw۔cOךm`5&G@#Z0i0z4 _F /4LaKlʀt: +W4巻u+9ţ_ 3}H ?QI`Epz5). ǯѫP{Ȟ1ܯOʧ;Sy-ƔYwH<.;`sdG^7v$|"'𬝷lSM^ƮF}KJIN~XU.$F@4tzЯ^ޕkB'[ikSמt2*F+=A\ qtAZD:eetd?B=9_jG!$3G$-^62ܩ^{AƃJ} 4 .$dtJuˡ%ͼ6O$QslVE@]=맼^:0k+V禥sEBVF,+dG^VPl-̍+}pdqs[)e%nfx|t .HReV^"QN%u,[x2;B"u?2バA~UqW&H{{\v4Kg9 '׿j}>Т+(#j>۠qV2= crzZ$]쉩fšy#85;㢎:WL ȕs<~QHU>z=#o}*YGjB!qTDTP D[hOBu1>muϧj<Ҥu뵫wr(RŲG'WyڸSSR_`RZDl籩I4t(_A+OGӠݲ0I?5[Xȹyg~AcQ:ṃ8uf$Vҙ$NeU*{O[9-Mgh]q֬wnn4H9 T Dñ#Ep_ Wq2ᆣ"Xv,IQϰ_ָqֻ)|VSh|+r4MOMq31G9m-ܔmv`QOq 0vHN|e0 (G4QjXݝӀ;bO^O^GsRv:ol8F"A\{`G+ң9Rpۂ`o z^ROr3Ab ` rs\5v8Y'߻xPI"  --`v* ME~PYB5Ŏ$H=?֍MgZSJ U죨 8sȧMg*qhB[%qֵ-Wfy ed=g=m#Qsw9oOoJ(u?Q1Oǽ,wd6MgVjKs8V/y]d8XWu jYX%<TONъV1]ŒOnbHQAQF=|ű whg웳F.gcw6Ή $K`ՙom%9U`G+n%Bqs4Ϊ4ђorWIJ! :H^={U[B) :ZIӧ켊[wOz*ݹv1Hޜ » $?;`.I'+6qUn3}!E ;XŸwR$$NF3ۨ~Q{}Z0d\=kS1}!|zzJ7JgsޡcLiF4MJe )094Iؘ$7O^ 5#HJGjAS'hEKk/ xEǯz$Vmvc5R ,bXikܹy}yϵpH乑cO|_m\z"TFf\w'WE5h]%q_sO1#B*زkOw%q0=zsNdA^T1>4$>Jx4ʾCeEL)Ks0ܓVa6SÓ*vlaD(%e޼ ⢲/*/'*n7cwRW n)ݼv4\w@=Luӥ0nb@Z:Oi_%NE|ŷf# gsWC2MP>σۜץM{- i+kA* e>nQtzW%nrSi1'?3]z[qX~T۝oCj14wi󧫧ː?2G2J oT.H;N4dGqW YoEjjt_ݞߠ,?VPu::mOT`O:f-* Xw^[8jڊ-L =k6Bu;Z5a;n*LSWRPè W׵Kbc/8=):iN4C~ -׌w$(֊ :@6_QNq7cҫ{;iB\Ld1'oD[yAש"kpFmiqj[qgJQc]nlQjZpzcx)XYُ/AR!Ce+zTJ{=jsQa߱˭\1V'0+kVi%Ҷn9 18 gu^\~jhhEu=sYhKFdn?Q\ (7违cA\')A)!-߶}\(<w#Xz{5 Ү-i6lP&KXgSsCIME=T3j zu7`wS)~e}jse,03#CQi7ÞZi29l~g#>ci2>p:sI&ݑ.cDhwcnU . mr}#&u_il숳L;IjI۲Gȯ[$*@:ax'95Qb.:I9ly=},,݂걛jZ#Z+#m`,lc03ܒpI>ԿhˤyMn"dgF@'IaMAB u!ʍY=7=Oziwvi<7Dgr${ e]`*AЊٙJ vywd[AKDLyS^+[:av8sVD),ˁy͏,=[+Wq [xĎއLX؀u*5=ˏ?mSW=(}{d{}y:g﭂O0c܌{ ⴧ}QۃpgE 0d`qӿr32ATp}HxΝskmGL^]-_>[ ,{c5⨵QTL\MmL9;N߭v[ۗ-@R_ު;p@  4XC4#9Fݨ[/j20%aޢBKdQ|+#,VlmP$ڵtax8H&ݑY<ϻsyg+-aLiqkEć`áV= Q]"Xq3kvö@lyi|`]4db|ze{-ߞY6JKsחɓRѭ@Xu'Em[sp]oVtap 35 n~zu dv}%_jփIg\=k6K2Pzj b`\d?^+6O@u%Bt³啋d,=;w9nxh!ګZӭ@j#(,޼m㈥y6@#;G's&l>c( ^jG58+@M\qz\|ݨ3D^[mKsaЮQwr9dq׎wTx p@S w73mR^I+}RXzm58=?*#?c+t7Xgӊ-ҁҹk{>;n(TĬF2q~8Y,lqh."lRGp#U ? g bkqo=hI_$TB0jb&ުOoz )V=MDsKD`=)wp7sQ6AX> Z+Q5;ٟH/ ܵ]Cmp&],?UӾx9#޶ه2.Kɴ1.@w>ՙ16AǨqZ:M]{!=9' GC:95 ;]s|ޒ1z:R&PvL*+FrQo d.1ϰz5X%^p6ߠ>1GXER`==VvA ryӌ7u6e F $?l!׬. mx-d`qB++w7 ÐG$sxW8.og!H|/ ܂Nths+$ICm\=..\?9tOkӅ*kE8n A\7~x+GBXgFd@z <=d88Ac?+:W<Nf;A#ugHG%!5'Ҿju?x>,267~JNي9Nz[v<*xn{&D˾<NOCXn:A!x+EK'j^o71scx C{X7yJ\sq\hEnx+cWakgo8%G%/'p<5;[$5(9#?@KŽ]^m&I[Pӑ\.*Ip'~ 7b#"Izw5O([?9Oq~jby =W(䟼T#;G}U;8fW=q5ٶ_#Њ5ywE1 &K˴NՓܝg^ &S*zlOR !Ԟ*:ݏRtJFךjb4\Jz:ր嚉ϧZW΢w'ze$ƓQO,O7<,9ޠ%ǜs2=M:\MC\uT: m?ICjR '͔& l/S怇w<}x)|WTvybBXh![ KH3yc] !?3q,˵s˚$ ~8S&ٔ 4rq購c z> @VbNyq=4 'J -VD]$ n8Te#Ў׊sy?EfCH`:ˊr8OΜJv iӘ6zgCqsR#'-:ߵSq{MSS]/J#=~r:k>W{+ds.K@5Eb01&HbCr{UbřI?=2y"2탏\] YX).Hfl)>^#!U9QRf܎wupG~ӭKix<AOfnF)>Y}*vȪ:U=2N;VM#7kzQ M'[vz]ԫr a8 ݷ`F0v{`־[;8@~+ WE$v/]XgVʯ̧v gqW-/u0 ǓGQxEF d2k_ˊ35JGv3μѴwѓbuؑFT0=y A^hG `OW IY<(-ʃJ{aK0,bT, {/3҄׾qq7ܸ% '3ak^7CѲ#5I-2Ķ>wr>7WZewV~Y#qלZ+FoS̭p=E+P#AkY8ۆv=?5#23pG* G}zס-U(zV+eIwڮ4K3>kD>_ZS}0qⱏsdf1ۭ tMsb\uz_ZSK:gn7xΑ S5 kc˼pj2bܞMS}j1eVk9aPXO˺D2s؟] 6 zTbG/)oVzq[+D@?AE=EcZ[jnKһkV# #(=FI:SǞElEr~V:Ƌai'=?*Iờ#S1IAX +s["e-ϧYҮoqćPa.\6H\Fy<B}VrgFD8G<`yɬԅ7A/ %7<Uri ǡ$rU8i))|!Q #L,)|:i>&,3#AyuHHL%pҋn$ y$s@?jF:>s=(. RQ@h:@:`~ w'Ҭq#>S\ÁTn0*rS7x /#45~2|z7ibU$+IFߕ~o2k\hłȭa17\zӭ#'c QBV`˱L}p2HwOc޵@!h|dQmY 6GAjxLH\=jzݑUrj5w*u#t#[Kl܇C#z Hpjl.^WfY!O*SMzL2UrH՗LVhRKzu ;BI'rMp#Rn=73_D.qO?sp $TSF#*Ą's goSZHy=350M=w׬B{{PsI=95}?j~nG$<7( q"|4+;^fjŅj[#y('}vpiq,RBHTktH8Bhビ;OPUC1IC4dϩx۟\PV7`A8ǥ;w1:= fڗss er:r ' M-j0˻"S9|@ `6Lwb@;3"wCGm<2'J5]:h3Jqb&ʉp؅#:Ӄv=U$K)%r Q@;=ED'bU_l%pw<5 B-f !#OX栂YQ0=rzz(9~5?x%݃泜su8{nOA<%V9<##Sc:7P]Má8Socv(F:o- 7OJe[BhhVfz=}-~*@Oclw S<{t7vΙ#~Og:1nW4]ђʻB*3x"(Kgdbmr*2ܷ#&"͹>eǨdSt<z`U@KpO$ێ< Q?0h@{) N>VA.C0nTשhX=n8w*\G =tculuךpw8&lۻ4ZU\9 2}9[1&U֞GB{ W5-m}'A/?L`+#FX$HFR($gMs:oФmbnN_YʮOʀsU^B=AUXx-Z@ݎLyS{:6}zU ͎޵nkVL{vQ@ހe72z?Jː?RNyb~©;PJtNMuQ. <0H,I*im!Ǩ8Lʹ7ZV:]0w09#w2=jGi}y5%lp@=Fq~h+]@TaZDW ~0x[.ͯ,đ@Y",i JNק>o['RYm: svs|bvs7= :~F :4B[bO5F}#JZ2Y;hWUI IBUݯca٦Og"$w2[ ː$ fAw"v{m7BO$W9 s 5Z7d֍6Ɉ8`sJ>Ӣ ˵"}?s fj[qoqzݤX򥉋1TFq㚟dGmLki2ٮPϔI#0z:5ge6m[4DQ8#Ìg1v~ f}M) -PDq\ i}y$rZ;ijNT!sВ8P+hZ2J'oL9ĞQ  ' v/dxaF͕bH8<}R*AeFP|r3IjYS3ܫ됙 H#r>С }F{mBT]>J{6 |,:`?B;z0zgޕ Oz -$gPIlJ>. U\',4T'o-C{YOiojmb2#XI '滗mwof3yDG@Bv+nȣzmA,e.:O'$7*{8bG|>ۂGިqz 7@yنGCI>͟AV* NP,@w4$sIjS7˷n`UL [\T|TZ5&b$/U-Qp!^GGml۱ %2FS 䘮BTw8*xϵA}z~C 7sEd*wc$\ 1`$Nj߷7uz۴QF̤ qAD{оuLSb_KƉ"w2IԎAEp} eխ:pqr M@%*K6퇐kJ+ ބ{bb=Nt>{x<מwnĎ[r\@:̊7 ՆhNd,H ;T:fd ]v =2H=j(&. HgHث!r}m_ Z. ~W'Qؑ<p]HbK.\{etOm){cؒ: :t=mƽ|nƲ֥KNrT9 B-#S{}:{T"uƫmsnϷ<+Ew?{bV)%&Ӏ9#<8϶q CK7sܳ^mW,#FA@' K֝:Ѷno&^Ya}kZ>*!U߇og>Iu;z[ W|2{e` 0|ņ`01UZmi)$1ꃗlt?&X"m[jYI,In$́4mvz6y@.9c8$tk׫iỻԕ$p wTz(8r9$I''V /o%lfB!yUGZOѯ7~Q9*u/ƚD2yL#fD. ?J?w'e.u(zZH|yBU8bBwC]HpW(|35{q ScC('ѳ:\/^/k{vs 118.H))jڣ#UyawPfM8댎qzOyUNY[6!<=Fp9jνoM6q拉XٳAt'mK-{wq[Ȳ20y8}ymze3YrG S|0n~,-'߸T 31^e?hѵKFd t$@w)Ih[XI|t}B"'imlgh2dHfpz +n> }Khݝf eI'vpqק+̼=y-tuͤķ2C3] *rA[JT}q,n@$wn]gl/OfS;FpI䎜W4"5Y:Zn_Im;GQR + eBg9-9!ֶ+HjL`n>2NR<=4eƣr{ԒqC S ; ?(+lD^=UؾҞq8QA]CumK9Ld~s2(Km,:Ԟ߅ L}lHۚQ(O?3$B_$l2xML}ҢyQh`4P2##JA򷏺׵DOJ)[* oFfR2T F Y4gJ\o$B[IZbʛ~uְⳞvMOz-C*BI #xҍ6Z: tJRpk(9OE{o20J;eH岚\\Z$I<)%ˣ#˻<)8'r[؊[x7L0/RGN6w>;B#oN7eoi&VG$\=,"yc`j3iݞ< >whiGzcϏT7ۍ1Ǡv:}ۆ8@y$^3jvO+FfwBy/ȇk 22idI]򏩪v6IsWxtAMpadgWZGq w8xF!9#i|ib)KŴo}8)˛pA.pPcg~#xgH~tK:uŸǷz@xExiߦA [;eNNq},w-N()7Pc`|@RUE6$dϦ5 OPM"%!XD#39,$rI'SQ[&n|gwp1gcFf#5xB xYI[i#݄P=EeYkޗn-bwyvzԏOxWd4zݔ}RDZ|֊p,$u (Fs{{>"䐪H$8r7wOsuq-I95oOu])dfwd'٦h82zz7KYJK R*&.#Q13qej%miU^Yb(.$nzN8Sƾ+I˒j X!=_SD;9.cN3yd?A[dI1ϧS^_|l[m{PcvB=TA|Faj^[nrs֒]f-RYDc<>FpsRrt 'vkwwpv1s^W;KC"[[K("% -?7nm?ikX5.b2)C+3rĞ䚭4f2$|d5[i6Z]ոX̐B7s u+#YO#̹! ʐ0A#e[jJYr s|j $2H==րo&dSrO'Tq))ހ`!~Q\3Џ_b;oRWnYzG'-[bݲĉ4`(z}PdmS,$6:ƒzO?2˴h'TFO!9zUKG6빥27A_`x^R+=)w#$lݷ9=rݰҔt>3KȻXПҙxx"bߩ#.#&TኖVG XƊv0=&|#VA܃Eb욁F<~5" uR6nXGSǷ֨8+ke~8?|1?ҦˡFvpCu'~tȂefϨlb::*y"'p}6v8[ hڝ[xBۺcXށ}"W)&qCA%D #x `tX6 _qq7`mE]3Q<м;m\u) #ߠ>OaYVQv[HUl6]3ᦛiٴ6 #K;Ʃ j^2%,r~%ws[{h[Ok&1ש]ou7.(RCө|[+er =𗆦IK}*9:O[i65P#q? 3i~(\21x Aű^ξK{Z.d\@N{Zi:Uf2rN$OM$k!TsCQNDJ形?@$rxukK.+{`jMsÓY.m6kp%- 9c汭6irtu#05H?["k {t Iɭ5E;I{ O;k l%PdUJ=jV|ĺjr& wyk?Ym4gKok&ˈ#ǒsI#w~5xHG5XgbF9`N98?Jmo[ u,v9id-,@gvլXkl739kTE[ ;l6XFGcռF.FQbBlIr teEc!! =4s_] @W0Qwe\ٯmݬrM Ŧx8wXڅ΋cv4} F[G8}:I'ִ|"Yki,c(OEsTi74$%%ǩrqL'zeOU?*E%n"0dFr3p @ҢPT(I=֓sVPbj 6RIE፽XeG^L&۹Hã25MB{ Aaq+Lw"kglN;d)BsDNvw[jW/ʮ0;G5~F[MSh&WLU7g 1צkk Wk ami# 'Ij$n$LNIfYɧ_i=8kK—Oa>&Bp r3Ђ2<u%uk-^aO*SN=G\ΡKx;2H ʪ妚;rZO9 pF@q`G^͡vH<)eʄv0_ʱSn 5 by`TQ4]2:#=@n\o:.wl֗Wu`@`2J|"ջT7U ; fWg91o7ԣcp"'=N@ۊhV1ѬC* >NQoC>"Kw+[@pAdsֽ'.|2]VSɯ*O Tug* LC<γR-XDnpzZrvir崞ǔ8RZ9 edRd%y 26H6Kчb=A}ZFg纟ocI jr\R;]?ψY5[3Měèsڼa` 8=V"voav<XBLqWD')5dwiv6P9"้|@7%ep:k_YU֒TLaq§{2_&  :yzV->dь]<={{av$t8?i̞&N I|R&2ۤ&{q;NT{t5I {L{ Â8vΕ49 f|ўg4|6L\NzWcm:]>LH#!eTc<JGf$̑d{JEtX`(HIÆ1gwId;kp?y#}hfĵ|7: }I:MjVv-6]F=o:h:<$ 8>Ʊ-h{W%]3*ub>!jjPؼl1ٴL ~']*\ށ0FH8kX|9*K8?Cֵ䶴jQo"xhU僰NcQ̢E)ǗGu:Vn[K9ΑS:IIɉs$Ұ|ykem a3M OĊU ;Nk[!tWG[4˸$1qV[:2kfB1Vi:v趺ʹ$A $1Uk[k/\Y}+PGʻ<#׽QŤ\5̢9L 8}.Jn (h(&|aW- n9$4h% z|T 2,[]I?4r$ I&5krIu%}>WAjץv (,.4״M9v ',Y{Y\{En*7Hk2fc~Dkks }x=@bǓF.H'*""t-k{^tΪR}SLϻ*,^P(+"^ 8nǃ=Ͳ,R;9Y-VSߏnީ(/Nh` x!]7;ԣfE8Ǚ?JFz OjY7O6~cT\ ;Tdl1ցi}L>:I-Quq|=Ap#ЃP=Jz8Ѣ}\oǰx}.p}+QR%XՎD~+/Pt2J[?R{1)|$wVO妡IyWPO lqq[z|hPl\`޸>cO"^=<]Kyo1~Y:XqtmkFPѥÂN8'$x#jS_]5Gu+#r;ۋfYONǎUd+{{d<~9kV msSYܵwKR23*x?CZ %ԡ'PAVt)#׈o"0р 8V~\jPyC0F 5j]T 4VVg CUSrj]v ڕdFG88H溽Dt/v$}Ig43J KSZi[OH!o>Qrz Z'2gs ." vs}ٗ͜]H67bz;p:rL|؛ xM%OCU^ylnm"+k{Xs?Jm_d*rsԒ{E *,$Ɨm'ׇ ǎߍ5Կ-hmWLB$bKjщ#7,<ݘvr@96`IL.v5b3leK,6s¡&[PtgXSiKn|`x9֣uIEؚ);#18<p0AsXêK Rxq5fcBW`uqnmni&ٞ%aoS:zW:-*Âb=KP$x:gY: *L-S-<;zH(07&C؁ץsAn@zf4-p4Q v:i/5mGI%E:d+y`Ax튱e,!Öʆ=ꫢiE.3[$S"G%_,XCn#3c5Wmʍ$Q$CwXvZU+[ G]ݚ5qqLtQVQZ&,NwknH- ;`Yr 0}h'ѵ㳎 %@!$gިFW4QKy7M{Q͜EYRǮ 0|ƧG󢊐[\]9'i}%ܓ@vD->QTSX>}ԑ]y>kQZ-A<= T=<>)^sZ݌vIY9'(Cˣ_hD q<~9wwiVO4QE#E2[Mr3)N_QzqQEn9kx+QE/pub'dataTamori Publications LLCԩdesdataWelcome to the long-awaited seventh novel of the best-selling saga by Aleron Kong, the longest and best book ever recorded by Nick Podehl! Book 1 reached number one in the Audible Store in 2017!  A mesmerizing tale reminiscent of the wonder of Ready Player One and the adventure of Game of Thrones Number one in Epic Fantasy Number one in Cyberpunk Number one in Video Game Fantasy In The Land: Predators, the Mist Village has harnessed its power. With core buildings, professional fighters, and now their own Dungeon, the settlement is primed to grow into a kingdom of true power and magic. The path to power has not been without risk, however. The Mist Village has been noticed. Evil nobles from the Kingdom of Law, bloodthirsty goblins from the Serrated Mountains, an undead lord with a penchant for human sacrifice, and fanatical kobolds from the Depths all plot the village's destruction. The predators are circling. Richter's people are horribly outnumbered by foes whose own power has been entrenched for thousands of years. Richter and Sion need to be stronger than ever before. Luckily, they are. New skills have been learned, stronger enchantments have been wrought, and the hundreds of villagers have answered the call to adventure. The Companions do not stand alone. While many eyes have turned toward the mists, wanting to take the treasures within, the Mist Village stares back with a simple message. Come and get it!  Please contact Aleron. He LOVES hearing from people! He also does almost weekly giveaways! This is his contact info: Website: LitRPG.com Email: Richter@LitRPG.com Instagram and Twitter: @LitRPGBooks Amazon Author Page: amazon.com/author/www.litrpg.com Patreon: 046%047]t048Ä0493@050G051 052053|H054LA@055~-056H 057zH058+w0590060}AA061(C062063m064065o066!`067A 068Ǽ.069T<070J>8@0719072Ӭ`073#C074075T076oݐ077BS078Ohv079_080H081082Gw`083  0841@085W~086 ,087 088?(089'i:p090*M0915/`0928: 093F6P094LfM0095OgB߀096V L097[u098^a099b=100g>P101jH3`102m`103tӦ8104{ؐ105}?106q80107J$P108Up109W110WdP111zp112mdat././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/no-tags.3g20000644000175000017500000020535700000000000017445 0ustar00lazkalazka00000000000000ftypkddikddi3g2a$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@$ @././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/no-tags.flac0000644000175000017500000001112400000000000017742 0ustar00lazkalazka00000000000000fLaC" BzAf鄚0 -@+>J+@-P -@4-X-@80M0@X2Zp4K2@>4@ -KP0M-@B0@-N2-@-P$-@L-N-@H+PL+@$-L-@X-N-@`+KZ+@-L-@X-L-@J0FP2E0@,2@,-O -@d-N -@T+Jh+@-K0-@@-N-@#%+JX+@-K$-@L-IX-@0Jp-L0@p-@0-N-@P0Qp2O0@l4D2@n4@b-R>-@+>J+@-P -@4-X-@80M0@X2Zp4K2@>4@ -KP0M-@B0@-N2-@-P$-@L-N-@H+PL+@$-L-@X-N-@`+KZ+@-L-@X-L-@J0FP2E0@,2@,-O -@d-N -@T+Jh+@-K0-@@-N-@#%+JX+@-K$-@L-IX-@0Jp-L0@p-@0-P$-@L-N-@H+PL+@$-L-@X-N-@`+KZ+@-L-@X-L-@J0FP2E0@,2@,-O -@d-N -@T+Jh+@-K0-@@-N-@#%+JX+@-K$-@L-IX-@0Jp-L0@p-@0-N-@P0Qp2O0@l4D2@n4@b-R>-@+>J+@-P -@4-X-@80M0@X2Zp4K2@>4@ -KP0M-@B0@-N2-@-P$-@L-N-@H+PL+@$-L-@X-N-@`+KZ+@-L-@X-L-@J0FP2E0@,2@,-O -@d-N -@T+Jh+@-K0-@@-N-@#%+JX+@-K$-@L-IX-@0Jp-L0@p-@02mu2{2r`0r2.0*2mg2 2m^20xS0-ro--h`+x-+H-rx-x-h]-+h_++B+\(J(n(hp(p(m1(?,x=,3(m(a,xd, /_/Q-|-a-m&-J(h5(;+b7+9-x-/MTrkPianoy -|9x-Q9mB>24x<|@@!<4'-9b`r\;>7h-9|&+:-+9e(h4mx4(p(r4m5(4'+r7x47 +0-|9xW-]9,-|<|@x~<@d0rmQ2>4|hCrV>;9CCrCm];>4CLmCrM;C <E@";m>rCm_;C?E @<<[Ehx@b< EO@@xExHx3@HE#mB>24x<|@@!<4'-9b`r\;>7h-9|&+:-+9e(h4mx4(p(r4m5(4'+r7x47 +0-|9xW-]9,-|<|@x~<@d0rmQ2>4|hCrV>;9CCrCm];>4CLmCrM;C <E@";m>rCm_;C?E @<<[Ehx@b< EO@@xExHx3@HE#mAxE|x9&V)x9xHE A >F)L-r>rA|Ex]-.9U&x9rE>A &*2r>rA|E|K92!-x9mE>A0-%9p-x9x.h .@f*mJ*@&*\4*@D*T@*@8*r>*@2*`&*@R*O2*@F*rJ*@&.\>.@2*h>*@2.d@.@0*hJ*@&*Y>*@:*h>*@:*TX*@*Y0*@H*Q0*@H*h>*@2.`0.@@*h@*@0.Y.@X*d@*@0*Q$*@T*I2*@F*\<*@4*\2*@F*M0*@H*h2*@>.\.@Z*d>*@2.\ .@f*\2*@F*T@*@8*d2*@F.\L.@,*md*@|*YX*@.`r.@~*`2*@>.h .@f*mJ*@&*\4*@D*T@*@8*r>*@2*`&*@R*O2*@F*rJ*@&.\>.@2*h>*@2.d@.@0*hJ*@&*Y>*@:*h>*@:*TX*@*Y0*@H*Q0*@H*h>*@2.`0.@@*h@*@0.Y.@X*d@*@0*Q$*@T*I2*@F*\<*@4*\2*@F*M0*@H*h2*@>.\.@Z*d>*@2.\ .@f*\2*@F*T@*@8*d2*@F.\L.@,*md*@/MTrk WDrumsx $ax$@h&^p&@$^4$@,$^$@X&eJ&@$Z$@F&cp$X&@p$@n$^Z$@&aL&@$$OX$@$e&$@:&^p$a&@"$@<$c$@p&e@&@ $e$$@<&cp&@$aX$@$^t$@|&e &@T$\f$@ $UN$@"&^>&@2$er$@N$O@$@0$er$@~$aX$@&kL&@$$h $@T$^@$@0&k$&@L$JX$@$\f$@ $UN$@"&^>&@2$er$@N$O7$@0$er$@~$aX$@&kL&@$$h $@T$^@$@0&k$&@L$JX$@$a*YX*@ $@x.`r.@~&^*`2*@>&@$^.h .@f*mD$@*@&$^*\4*@D*T $@ *@8&e*r>*@ &@&*`&*@R*O2*@F$Z*rJ*@P$@V.\>.@2&c*h>*@2$X.d&@>.@0*hJ*@8$@n$^*Y>*@:*h>*@$$@&a*TX*@t&@$$O*Y0*@($@ *Q0*@H$e*h>*@h$@J.`0.@@&^*h@*@0$a.Y&@.@X*d4$@ *@0$c*Q$*@T*I$@**@F&e*\<*@&@0*\2*@F*M0*@H$e*h2*@r$@L.\.@Z&c*d>*@2&@$a.\ .@f*\2*@6$@*T@*@8$^*d2*@B$@.\L.@,&e*md*@(&@T$\*YX*@$@ $U.`N$@$.@~&^*`2*@ &@2$e.hr$@.@f*mJ*@&*\4*@D*T@*@8*r>*@2$O*`&*@$@8*O2*@F$e*rJ*@($@~$a.\X$@f.@2&k*h>*@&@$$h.d@.@0*h$@.*@&$^*Y>*@$@8*h>*@:&k*TX*@L&@L$J*Y0*@($@ *Q0*@H$\*h>*@($@ $U.`N$@b.@@&^*h@*@~&@2$e.Yr$@&.@X*d@*@0*Q$*@T*I2*@F*\<*@4$O*\2*@$@8*M0*@H$e*h2*@@$@~$a.\X$@>.@Z&k*d>*@&@$$h.\ .@f*\$@*@F*T@*@8$^*d2*@$@8.\L.@,&k*md*@@&@L$JX$@$ax$@h&^p&@$^4$@,$^$@X&eJ&@$Z$@F&cp$X&@p$@n$^Z$@&aL&@$$OX$@$e&$@:&^p$a&@"$@<$c$@p&e@&@ $e$$@<&cp&@$aX$@$^t$@|&e &@T$\f$@ $UN$@"&^>&@2$er$@N$O@$@0$er$@~$aX$@&kL&@$$h $@T$^@$@0&k$&@L$JX$@$\f$@ $UN$@"&^>&@2$er$@N$O7$@0$er$@~$aX$@&kL&@$$h $@T$^@$@0&k$&@L$JX$@$a*YX*@ $@x.`r.@~&^*`2*@>&@$^.h .@f*mD$@*@&$^*\4*@D*T $@ *@8&e*r>*@ &@&*`&*@R*O2*@F$Z*rJ*@P$@V.\>.@2&c*h>*@2$X.d&@>.@0*hJ*@8$@n$^*Y>*@:*h>*@$$@&a*TX*@t&@$$O*Y0*@($@ *Q0*@H$e*h>*@h$@J.`0.@@&^*h@*@0$a.Y&@.@X*d4$@ *@0$c*Q$*@T*I$@**@F&e*\<*@&@0*\2*@F*M0*@H$e*h2*@r$@L.\.@Z&c*d>*@2&@$a.\ .@f*\2*@6$@*T@*@8$^*d2*@B$@.\L.@,&e*md*@(&@T$\*YX*@$@ $U.`N$@$.@~&^*`2*@ &@2$e.hr$@.@f*mJ*@&*\4*@D*T@*@8*r>*@2$O*`&*@$@8*O2*@F$e*rJ*@($@~$a.\X$@f.@2&k*h>*@&@$$h.d@.@0*h$@.*@&$^*Y>*@$@8*h>*@:&k*TX*@L&@L$J*Y0*@($@ *Q0*@H$\*h>*@($@ $U.`N$@b.@@&^*h@*@~&@2$e.Yr$@&.@X*d@*@0*Q$*@T*I2*@F*\<*@4$O*\2*@$@8*M0*@H$e*h2*@@$@~$a.\X$@>.@Z&k*d>*@&@$$h.\ .@f*\$@*@F*T@*@8$^*d2*@$@8.\L.@,&k*md*@@&@L$JX$@$h*d|$r*@$U*d$V*@&x*d&O*@$h*d{$s*@$X*d$o*@$R*ds${*@&r*d#&K*@$[*du$y*@$h*d$g*@$F*dm$*@&r*d&_*@$r*d_$*@*dn*@$h*d$S*@&*d&P*@$G*dQ$*@$m*d=$1*@*dn*@&x*d+&C*@$m*d$O*@*dn*@$m*d+$C*@&m*d%&I*@$U*dl$*@$m*d$j*@$R*dt$z*@&h*d"&L*@$b*d$n*@$h*dl$*@*dn*@*d7|V7*@*dn*@/MTrk Jazz Guitary 4_9bP<_p>h)4.<@r*>89v@4_9hA97b`9bs7n4o4b94:4X9hPhp@r>>9f@4m9h 4@97bbEh@]@[E>=9_<_ @U9{9h@_D<,E_@L97m@U E!7/9b<_ @c99h@[@<0E_@-07m@X1E97>9h<[@T99_@_~<rh)4.<@r*>89v@4_9hA97b`9bs7n4o4b94:4X9hPhp@r>>9f@4m9h 4@97bbEh@]@[E>=9_<_ @U9{9h@_D<,E_@L97m@U E!7/9b<_ @c99h@[@<0E_@-07m@X1E97>9h<[@T99_@_~<rbpA_F>*AEUpAb`A>[E`AhpERA\Ab1>5A E2@UF<*EU<@4@bi@;[@^@hpDb/@A@b;8D;[@b@UF@*Dhp@b;1D@Erp@U|EK@)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ˈ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/sample_bitrate.oggtheora0000644000175000017500000000600000000000000022434 0ustar00lazkalazka00000000000000OggS.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 eswC././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/sample_length.oggtheora0000644000175000017500000004000000000000000022261 0ustar00lazkalazka00000000000000OggSzI:@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']=././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/silence-1.wma0000644000175000017500000010513000000000000020032 0ustar00lazkalazka000000000000000&ufblx3&ufbl4 testܫG SehCO;J)rJX9.@ < _. Seӫ SeFC|K)9>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=  ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/silence-2.wma0000644000175000017500000005510600000000000020042 0ustar00lazkalazka000000000000000&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?[././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574016271.0 mutagen-1.45.1/tests/data/silence-2s-44100-16.ofr0000644000175000017500000002024200000000000021112 0ustar00lazkalazka00000000000000OFR D% dHEAD,RIFF6dWAVEfmt Ddata bCOMPSB,YB%5MHՋEi =&K0]>`6FXwuѕ'B!7Bm=LrW0cݦpŦe5 5R}M];Tß EfL 5, #mPe>S4~ ):.'3FxoQg?=(v˄P F]Gc}Pҿ*ǓxcpnYctQoDiX;]$uN.RZIJz#,fS!WʮI{?!.#;y|v03VVPye#UgVms397[6 ,G$\=V$TУw IRS1ZiU8kS崯l.b qPY <׹EĘT4 / f%p61B lz(9:E#RAlL~^P \f0fnmW|vyZ 7'z4m/PTɡm RZLD -\t! I|FC5V2P^ǩ+6dCP#7ґ5 LθrDe!.x@>y^rkt:L6`Fz佅)Ď8p.Q,ط}n?ҳ./Ӊ{ۨZ^g=gr~9ڹghϿ=έm2r)~MkeNz)10ZE'F~{8 :U(# WL0) ^4;A{ x XY\.tzH;98K掹rσL{H)^ZmAW0j9W >xlSË-GvBPZ~]KZ$gE%?|qKvxr>;:T$o2O7ٝM˿=nl0npzh|~騰9&J` N8'K:q"8\ >S,a`p-uH96N :eFN[w; m:2-m%8X}6eî4Of zPSm-p1gLJ}?R|D 0am>r1g$ۂtJO peOD07߿v@MVo5} EGjfG'$EAVYڏ<&ȯA+x%k,^oGhT[*9-k p|5K*"`AL5bM$kN1 >e&$i2.JOh].&H4Q@_YYUf͉+ۿ:rZط^C:;v6>G6 macDHYQ:â_R9kO"POʖ+z,%|QS$ٳ?ORHEov$UQSCn0<}wfe4Mh×[^`hG(Q=D)n-=׵YWzjih]v% ,3_FJ3i9s4=|4rru!/njj!LWC>aN)P(#gROhX\M{&U$xհ놔Dua1ltB)Bb'HA-ZhFτ cBވC=PM Ӻn6!ݏ"ТbWNz.-qB?E0(^ ׸j04LL'I]O¸d@dRT 7 QS5ٻL+pkzA$~a]5BkCH7Pa+ ( 6K({ @>(=f E-5 ٘~IGAV &AV"FtJc,ad1Wʛ Ps!<ϭ09O-\t_AH`nPacСϲ`ovA^V`Y9Bff)5 :-fRxuOpkխf~!-oLHIc lA^TK ӿMsŬO u%jP9zHwJ[5ئM=tG{}HDdvr{QI >=?:; sP2 {m}Z]dpp3 Vu wH/ ="ՔKZ6- 4Ry~B X5uE&Q}*cTHYьxyƎVrA㑎I?F H'`ho/* 7|Pg ">S>[FƃmYz-j3}%VDf-[%)UȤzë#`/o)v!ɴ "3JB@aoc-?`*¹r?ΧO(`41s!YKQSRLU BפbuJQF3Ϟz鸉 ^'4%~UI#%iYoYt-h=#E?܊l@Ye&Wwk-~M^Sh!6;E s\f7,5uОp4 %`h Oi׍ @ZW؍L~g4Q>rm]ࢲ[z^}f7u y\ids6[DqZүnx%턖HBV rIsx2U Ola䖔A|Vai|Xҗwݹ1kZѦ ׭x#8ORȸS{ؓ˩vbtu T,vnb4">5 0|U p"3fx3@GSw#ߵW,-`Ptժ$U)?rXtBnw7tJۜwgtT[˛E3dWJ%@–?uF{>Ux9VbxLjū<Ʒ%;?!/V㙈2=kC|3ֳnc}\0wj2Fh $o'?a (s%8-sHVVp= ?bxU(s9X<) EȨ$_QüJrJb՜f1EKLOR4o_!ڎ\ C`ob sQe6W}̸ܐj)6ݑs cpeHH{ 4mmKͣvKVoP<N@ǃqJFf__$z kE1WڀXD0R/@~T,w̌C3*?{FAOo?+ 0D6n;!Ɏj{r˘|k*@eu8sX IL[?TOL:&1R ǡE eϲ<&%XLWMTY8UhR#_PKoR\G!.:=OGZzdYtc@ 8F ܳ=@lł_k? &"m"#+/hEbY6}S^:zWi<*O^zw :X ɡ9h!KH00]"cP^}nO<$b.Kث퉱Qr)*\g:ϙ/F:6^vP(AB)yhjȖ6 +|s;y[41q"앬 H QpfDCmi@ͯ1z5^1v쇄 ZԨr  <=*SZƙDmF/5z=M8f)宑Yh֯T>9%_k603{'LvcDwܝ^Ca_r_TX o}%V> / ˅3:! xYw\#06JR :a%wfMHeyR]Ão;mLO7 Lq`4sP {"{*Y(J -W.*8tBc!5W/Op 5)A] )s|'U RROw^%vOO<32EU@89tra ߫:?t"L?X1.9H K]h1Z;Ei{پ-Ֆd󊚐{X_WaR 4bXh*' лz !R7rѳ=2`j=cI u݅!* 9T-(b?t"G9.X[rQwjz$sOxmdu"kl 1mIN6Gk)2L>)Pk=H߷51N3ԩ<ԏe{SGoW^G{u#-A/θC ?j ~+vU; ^d pyQMH5!߄5+N[:#Y|2nj;XWTunMF#;^vnY:(u+FogVI ȥ o3wʚb^19j9#z:´{,P!׏w޻I2hRj"Z8)5:cˉ#-@G)$,`su, <5~`Wbjf΅qX}`9,Eh,/;'ga LRQ1tOsh4ީe'RKZ3)"`$BcO"ɲiAhϐųf<0VLyQ͠&oœVcNux4)Ru]l-J#+H q8UȨ2n8ʥ;!q (^SkyBMqM$GFb Y7ri_lB,s GRØQȢDe`A MtgdBl(8Srl~X5gAkp;9u5ɆK\R؜%HUqƤl"{癶*ܴ.~( YL:OC|f }+@GnM P~1jR_m٨ޞf-@&ӥ(+TAILLISTrINFOIPRDQuod Libet Test DataIART piman, jzigIGNRSilenceINAMSilenceITRK02/10ICRD2004id3 pID3fTIT2SilenceTALB-Quod Libet Test DataTCONSilenceTYER2004TRCK02/10TPE1piman / jzigAPICimage/pngPNG  IHDRwS pHYs  tIME  6D=2tEXtCommentCreated with The GIMPd%n IDATc?YIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574016271.0 mutagen-1.45.1/tests/data/silence-2s-44100-16.ofs0000644000175000017500000002051500000000000021116 0ustar00lazkalazka00000000000000OFR D% HEAD,RIFF6dWAVEfmt Ddata bCOMP+ %@5b{LSXNtJHz?e9LOxF3S%>;zGf)d8ymI sN*X)QLA\3eͳ1 CԴOjʵQ(>2a8SCv&"?S}5[MЏT{,KsNAf[r]b@Ez l_hYwzr- J .uo_r$cɿsMʹ]# җȢ _Ul Z!yrwB *arQ`A&,|s[2[>Y6x~@;<\Q <~V+tsvBfNȱ9׭e#}R }+*/Glү β~v On_8Li ~ތYt?x-M/wLG%GtT|N,n%5LdȰL ZՀECA|7UBdۂO> f&x*)|GzcfvK89EP1۱ᾍM]:4$bd\y3\mu$Hv.-)' xu w_8 s_uD'G}=Dtt;rlƸyT&yh_r,%?p'S T_w<Ly Ss[N@j jFPHJ1_S1&11hrfb)kZM"y&&UAWYa^ȥ%j 'LaauF+<#KQ1+DAjF5_&^:r䌉d 4?;jDU<9j~$f#ƑW-l a 8<Oq] )H͊[aG8GQP.\ ¬RiOD4(s3VGUn(-mWZ6Gƭ"6Kɝ]ׁicP= |4ܖ(C%y:-#WI"j=.t_.O`kfw;M@@X۩Eɓ8$AD3Ώ"E,Ю=M4H`@>ȨQ]%[ӨV(XzPPi\h%5mAmy@3m瀀ƖYCQ@ 0 bt|DSD:3R{ON5Sb2!ـ t"xuݶTADR +T 4,wQ/YwtF42[a//n.ɪԒg^kgK Ld[rsE]ΠI5$"@)pˈ"ң¤2;_;Yρav֨p;w*$ql𗉩uC' /:G{t.wk tA;dKKmF !hbf$vtHt/=C-ooCU^JaB}TD7%l$cGm`D;:5n7?rhmgUI9نܩ4o<P@=ЯOÕ,F.pM{ocE {7h1ي/#D[i;s2Vs7 ո98@n~ڠ?qJ6%Of|奊4; ͫVwK{"]*H͏0CL;gOۍʰa?I~,W:͏:bE%1/;||ϴs6)5Uݔ@T'A˩]P 8 (& h\ (kjVi ]|qQdsC`dĄ+nMJNx+Dz/[)<{$]x>fVAOp4<=t] ֲM  xy{ϚULd&ܴabZMGl>o*a[\rfe>>!*E2)T$a4W7;Wmi 6Gs[BΠ̫!(߭9yrV~sw N,*gpFa)Y&41:?.&!! DJOp a5աy`v.GV^uJŮRέy'x~p=iu+ڤ_?mcl'Ql#pk8Zחi 1 TsVť#3xC';ZD{G)Կ^ Pǡv-B`gLV0oaswjT'o~8Iُ|5;"Sljf1P& ELN YfM9!4U"`Bgn&_V uPŅ> Tlz ()tz,LWo0Wx@>;vE?L }ܽ_q[pᛴ) &aB e_HZ.գcBSԃ)A/Hk]ؒ4^@nj^E^qg/ X4?l"ZsD(KK;vӢ+ٝ-t5&Gԙ;LlHV;"w:=^ gF3>)PibĔe%B]t' k(ARvm,ErS_} &|¸ *"eB~h<EAL.ZfQv߭M J&O@acn A=\a%Y^H'`MJsXG,TpӜOۻ)@ [;*˽WG\fQSVmNO{a,Z6ޤ^2ğD@CMo]Awk?n`;قndkD:MQ`Zg7N$q %>^TcWipACH|p9M `6=o[F2j؅ԫ@?I(䀅ZC)8AѫaZT⊻\2x|* 6inQ5`F#]5߈ה^Ix+8*P~ܗ /gj %^QzpdubҌ٨]ڽ 7jjvgGK) Y&hsXKCI]`'G3V]Y#g8 hK+p-f_Ϝ@%#tbէܦOMk$uO?~wosjuO?0A,^aKHbMc\aD2kEcCN*n!8̺g lGNяb-vK0#Q" q8+)$l@_%kXˀ%F e#k"(/£޸`7ʼ__[DG{cɌ?j}v:䴗-!&^7 k" Km ne03($ף Lmv2U"O&y*H%146Ǔc  g]vMs."KRfML\kNeDB6RV\&oFS2[q ;B#IK ;h5N&3vSCm( $)}w6y$6>%dohEZxz/2_ -T)?$M|,F;piHs=X\79P\~&)*K݃دeFh9"x4 @.gQ`n5:8J2]OlPtvA bVJܳ32Ɔj|nn(Kh[zkPa]E]Ԩ&Ԉ^ᙎ3Ǚ$='K!.Z&0HP߹}ʱ5@)Ұ&T 1Ąr#t0QDp!"ANJױ~Hx[kwWYfdv@*j/d9ޫjÆ>p^mfN~c셥Һ40#|sS,1$}= xEvdL l.p b۹h8 XvrԜi2İm;4M4n!5:b"tLۄ"3HM@ 9XJ;sRr3Bbu-kpl^9hR/װmx^s_Fա@`si:0— 'ҕ+8JqChZFobRT6$3pwC|!ddsㆭMʴMsuBlk}cQeL)*.xXsBzQ|/M$w/=ɩG9T|zy5bWDzlF.V]nqpڮ1Vؙ"x} Fi&WX0klC{pET4:u{7ju#Va}pw¥Dm2?xLGҨ!qAXz;؎k5]I!v L8?;@Z+WxI bu 6H#{w6)8ln2 A@R uyހ!?2-{  {L#xcJUNU14nj d@#Y#x80Ff>T*'<1k{af 7%Ý:)CgA؜X11pϵ?sQm с4JߜH8m(jA9zʦG̟gP>Îu"Ī K}dataLISTpINFOIPRDQuod Libet Test DataIART piman, jzigIGNRSilenceINAMSilenceITRK02ICRD2004ID3 pID3fTIT2SilenceTALB-Quod Libet Test DataTCONSilenceTYER2004TRCK02/10TPE1piman / jzigAPICimage/pngPNG  IHDRwS pHYs  tIME  6D=2tEXtCommentCreated with The GIMPd%n IDATc?YIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/data/silence-2s-PCM-16000-08-notags.wav0000664000175000017500000017505400000000000023044 0ustar00lazkalazka00000000000000RIFF$WAVEfmt >}data././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1594480640.0 mutagen-1.45.1/tests/data/silence-2s-PCM-44100-16-ID3v23.wav0000664000175000017500000126207600000000000022466 0ustar00lazkalazka00000000000000RIFF6dWAVEfmt Ddata bLISTrINFOIPRDQuod Libet Test DataIART piman, jzigIGNRSilenceINAMSilenceITRK02/10ICRD2004id3 pID3fTIT2SilenceTALB-Quod Libet Test DataTCONSilenceTYER2004TRCK02/10TPE1piman / jzigAPICimage/pngPNG  IHDRwS pHYs  tIME  6D=2tEXtCommentCreated with The GIMPd%n IDATc?YIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/silence-3.wma0000644000175000017500000007644400000000000020053 0ustar00lazkalazka000000000000000&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=\Wy././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/silence-44-s-mpeg2.mp30000644000175000017500000002057000000000000021310 0ustar00lazkalazka00000000000000dXing!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.2UUUUUdHUUU././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/silence-44-s-mpeg25.mp30000644000175000017500000001056000000000000021373 0ustar00lazkalazka00000000000000DdXingPp "%(++.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 H2UUUUUUUUUUUUUUUUUUUUUUUUUU././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/silence-44-s-v1.mp30000644000175000017500000003533600000000000020632 0ustar00lazkalazka00000000000000du=@"`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 Data20042././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574016271.0 mutagen-1.45.1/tests/data/silence-44-s.ac30000644000175000017500000025503600000000000020256 0ustar00lazkalazka00000000000000 w*gT@CuAZ|'Ϋ>S`k)T6ߺK 2_ rWatiT+k#~-(iiSS](ULR>R(j~B+M3|w0B}ZZoaVMRϩS|aƋښ$H웱̀)!㼔?fm)#\|5[`S1i* /:r?&vqMϟ%ֵkT_Ck^ @ J`4\~Qpڭ5C`V[q+ kY B4vΏ%@/gJ~$e.΃Wwz<ۯj*uZ`n.M UOӳv coZ4-dǬOŀ'صO"gD@Ln{F4뺓lP [À`xyi 1" YcCYʚ 䅣Qx -'@Q[:vfV/])U%Tsv'^NzL".ra8s*e(-JÇ0HCMp}D`YbKܔıboKqXPքH%zʷuUĵU"ᴹ}tϧH@%};=ha^h4kbZ%JD`ǠT ĩs$eJX=(z J+:R$k!0֩PlYk ksENikQWeH/:fdj:!_Z2:a=pK{ ΋ x wU@CuAY9gO)o:t2JaRo|XiamXPBRn<-(Ol=}IV#r2ZO~ϩB{gԨ *5zO=,'צJY2dɸjkۻuSH E̛@rhKʫRi!+#jУFJwvԯi챴+vd%_2yrT(6[ s7W %OjruMsD0FA.zӹiH>n2&I M"J0c;x]B0,輜/z(lh kZLd,<I,;!'FGifGuPJD=f=P~#ycmfi&,cڕU*2RԵL0p\o[];/;2Btytp*J} VIKLwfj_:9g%.\X{a7Z"6[WsOx6lnؕ]j5 DOT:b Oe"_ڢOa Mc` Y$ zBxqOuyoo̘f ;fqa҄jQعlͳn`@{l!j +j*~=C J`d%b9#V芘/Mzfhצs`ρ{#ZiDO v<[caT֩쵩)6lֵJyN˷Lͅc.(A˵F@tNZf k$<1'>i| wU@CuAZ|+%ѡ9okVZ_>} L'ϩ>|3Կ"2XV?T2e/~3KMJ} TN5}Y'ϟ>|gϩz%D$'ԡ>Q"D5qSo#Lnb=O 3\нsS1»yTGchЎaF;K6d(l19X׉n4 Ȝ'ApR̤H]#P .\x2FG6p ?H5@qx"Jx'K:W1wf,i#G 8tqT̛<ˠ DI,ph>ŵyBjcJdFp`<}5CES }=cpoTDx6ͧ ըfi%c%1\/+\n{o]4F9Kc TKZ0da&+Z⠣k}dNrWJ >T&p,ezmi:'hjFo.x] DmB \!kݑj hM̕CfOkਐ* yD\xf6 l5y]M:ͤL&HZWr`-N|$o`Get^uSĵ# :m5Q3ǘVLlgL8<s>E_)ݡkfI퉆/eM#Т#U6C_?Ȓ $EIJ aO1* 䱽[a.v%R& wkU@CuZ{zOmZTp{a*˝Q: tgօJˠ|+iaC|3OOrU7Oa}Z*T> +]'i_R|'_B} ʩ ϩ>dX1ni|Q"D='l5tOMp쯢< &d` i+so.[ZֵcJ֩Ua:9TpOviM;󃸚BVqcuFh#&*|+lX(pH}$$,߈F v:ˤ=[ ͬ龇Nv5Tr"W1V z$I wh !.C[ˇ{]HCpT7.i51& 9^rDn$;^!{֘JnL-a AHE R&X.tbE㳽K&U!x t^.PB]Jظ:TF2LbRLCSP _mb)}(W2hnoψ KB{ FZcH(=d6 9Bt+Tf._LzaʂƝ!"(;LZE:F)mRɥKJ>qJBkYˣb^$;aRs4dmWe$jcXnnRǓ 6)$A@(bYĦW Hl+h.7Lzj$ ԱFվ}C@TԐi1n)VFL֒*0RNS{ffN!q=\eyMmM.km5d*kԀ` wFU@CuZ|~wϞ֮МKL_ֳϡ.} Stu4?J*kWKZaCL4RS¥ O{IuhPOSM(OrJ} *W~Y2daJ)y9S\AH6=̬:T@d]/Dq]JUWR 6%e'0j*R0 N(TT(\f>9x[rNlY@k<-pPKRxDF}e4 3uPt "[& 4WbV8H.܏M;Ly Z2Eʵ27Y 0bn+FsZᵫnM{fdCFW8n*cQ{fAퟹjmFɑfUР/uyu#0܏cO\#αOI=I0k[2uͣ09fXٱEs|%AVp4vz5n*3qLb^zZZY8uѯ"oT}/f %WeZ5h,FJZO0w%dG#?jOV|Si_?JW} XzE_)#~򴫡MM4CzjT+ϟ|SO†*PԫKXQ"Dpt7<:~xUv.2+36^FTxt!5*2^ƱpRr /^Lď:ikIǏGv]Ζ8 `Yv3!l`;Su`F)[d),1 ݝKv ]kC6և-S KVDh걲[aVVlgke`;n,lƱ`o ɱ^Ԡ[k\yxo6ҢG/-1;{+f̋^ i5A'l/OEB hvW_#Ay/&bBЍԀ* wU@CuAZvo>TwK>{]󤰩ѭ^_*Iܾ%YҥZ'FIg_?K=+;B} Dt#2Zx9~W(z_WLn_^_~*T?{HY2dbF͜v]m5r;+elF*3U7=ppUr-6K 'ܐO.5.}j؎EcUcu=bϞyGHHMg.dߵ$Y.I>EԨoBj}\J|ٍkR v,t6uõB qx9 9ڌHѕLPjΩme"\FW{Y4@3WSZ:| 2XOSL'՟RvVo% t4ҿz&J0>i_Ct՟R '­]tXY2d;% (f*K|<Sggh6]ިŸ|J* jݦ`4S8( (.] n߱nϏ9ujM3֑m!%*0^H. Ț:ֹ@܉qۛ>Đ`šۨQ&m>٫me,r6b v,c3rap/1SGmYO0I+@jUNzϊo@ǭ.S{lym^>Rv)T#ڕqlJGBt^ NawhNӰ׾$~1X%JT n=rr\2nѲ Z4vQdH hʼnҊH k SSf|K5p.J Jfs0@Wcdє546a H/foiZ'%tM5Ja{ PJ(O>~-3I%jPܿJ:WЩB|XP?r󷪫~ԟrQ"D/l#qc܇~o_^⻤7_4`HUsWikOv\ km18ሷSɌd&Q ݚ; !5C$ֱ!HƦ F`;l[eD1ԳB/ Iw))ĒT4.HLյl¥w9;ɳAv)ux)ʚU6[6!QY5k1 O[s,7/Y{>rfºw:UΦ$YD ֌bcZ"JK;J^sʡѰ Z1\[}aWK2$6|Xx%&mlR\n*";#҈&4FAFfQ߶ "[6'Ɍ섐ѷ ]@lj@}HPL 3ǴuOAyggj@=6j(X/^Xd%svw9QjչLhnza1tN׌\Z2ҌÛb0q$Y"(ҞhFZH6 Ka iҪ/%3/~'ba5Pf+dZ-,4lIE5~N,p/bN6,ǔZ-KלP.i$.p"5AwlM Xh9&ʮNH wlU@CuZ|Z:|M7S|wџM]_ZܿLMtSsNe/XO>|gϩB2N>ESLRYS_LkҪi_?L/v}]7I_>Q"DK[kЯ|F36&8Ÿe5Si)ȷ 2 M,UY =9'i٧:;D/Ik5!I}.(V'QTQpN $K7ι ǜ*эΕ׌l jFI4"Y3I5Qigpv 4줍%MmJI4wqMf/GةEŸIȾqq S;I9"ѩ5ِ ؖϸ`JF0PثL\xF &>5.*q).U%!S-1.d-RM񴳔<(i4BG$a岏S6ЖacȦ R" :w1:phD-dl2mYbͭl)Jn%FIucjOA&:wҸKIxZ*krMY=KXm:nȨ%Ƌ|xHLZKʙj7P)+-9jr[5MI~t5&qЈ_練Fe3r2o5msZ۶̖mkA-pNTj\g/i&:r7IA~PT#*s"`ta^eXZ w? U@CuAZ^WJ=sPavAgΟ|Z霾~$/&RgթCT'ѳjW՟>s~ZiS ~P֤2ZOZS Y2dӔ3[ J&ma!cWOL&ßh^nTsSI;B!@+cmZuZI3sLz;7uÈѪJ`bܤtn㎋^zHx87I>F hkHA"U9}#ɦ #^6ZEGxGcB'g tTieV$26y`bNPKwgvr?vq~%. DAm{qjŤH)Q ?JrvаO#3) "&D0A G<{D +f'.]GE'>T4*@q0_(сEm̻7ݙui5^tJMUK8ɰg5%_/\c-U&RӴFLwVwOI$PH_2#g6/C+M,cp\XƁ_f]smYZ*=$|OmkZ]ne-]L'ʌCq,vQ%)2x-xj#/DC!*Aš !nX65)9 p#F bŐB4dGW]\`8 wwU@CuAZtҝ٫EPl!T*JaKns Ku;d`t*ԟKJ$V./Z BC!>NrȞ5/׭(ia>}Jt'|ik~ϟ>}LY2dʎQ2-QAEiҜ{ >#2tI޺&)DF5#|V3wBnd}D#nd%8|okspHj V5 dXy4#%T5M '`r[8#L)C:]^\DWZ& cĪKYJ%=oOAdq4Q[a7j֓6Ϛ5"F^UV"KT-@V`g4 N31].7e%9,kĕok=s/Q LV12CU aGmEj-P%`6ud!4y":FU3X" ~W?dHRk[&r]9#VҺvHGbhyM˒0/Id1.tN$km,6BpRG2{@Ԧ o&%0}wq)JN$FI[-H)\9i6;B;GVW$ن:{'Koו…'$6j1%0^Iza,"f,J2s(L 12xӕ 6<-6aYsY`+[p4gԃ%w0W 9Y| LՑVZWe w?U@CuAYtΞTNBVR>|UR¦+4|ϩ*}MJiiB"/>UJUԾWϟWRnU=I24}JWϟ?{IrJiSJQ"DPy?MF$oad< gAxٔhg-kRGBn.uX A?r49a`1> 8%Ɛ"Rs5iZA.ZEIna$P'Iv.EV.R( &*sHАFl+6 m$Mbh^VԹ(Aud$ۇ9,q5`Y~ k[V!tMINWN/ A$mtWv]v2HgkO,6˚ uG`cPnc@ <I~H[Cgj{inFsꦖI5/pe~O`{+DSx򣷪Fa Jr;;@9=h%Bdjh̷\{*rk:A| k"\娐t챫LZrT7k. o֍Iw{UWƔDVivl֕V6=IiX_/Ũ0$1; mAh FZHIɖ$dHQpUbZ&gc%\ijK- jؚҴuGA>Q]rpS>w*,)SVKuqkmV͵I_ w!,U@CuQZ|=2gO>" kV*n+WC3s p>ϩ?TgLZi*~#vOB[XTKn_}Y4ʕ%U4>Q"DU&)>x$A7<0/7; Nl'm 8M&6h4%m;};/ĿG 06r,s y,kՏ4maOe,?y*?Yԭ|}N'N׼)$27BpUd;WƦ曾5м0DdT1{U;kUEkֵmS.LRa/#bDJV x! x^B0r(c;U55iuX Ds/tA\mp QsT:PHNzuw;IdP^ 7M gRk.lqRࢊ*hN[~?C>9wvhS "5l`ֵ"IMm\ED,@ @Y#DȘRC&miJkl7MDb*d0R ;2o6&4/)rnr=61ϡiicI4 e wtU@CuAZ~UΩ>|4>SoҺ-7}ZuhTA~W} 7/JZTR} *ի>J#%Ot3}d*iW2gI_B|W_>YdtY2dm#Lɖp/HP(n8g?RY%vk se)u < Ni,'P d@c*bgMVAslTⱀ;M^ vA/i5fTѴ7(Tڙ䣉58Z-33P9lBfI X]5u_f^7`ļlK $eꓤр1Wa\z< 3.쩅73E4Sl¸n )El i(tڤ1.;GE&%)Nwbņii.۷>W񘢚GntOJi1!*9E WulǺ\)4Ej!hsfr9O*Aτ:0ےi-Nۺ12 <Pq;Mce~+`^ 㐶7{:z0ކ$W1"5Y^[v [ݯx`Lf_Em>c@rUBxXj6=}l`9w3usIv6s _j6sx]Di V64iSZDzMRn\'zS=,2R&9IJ3)K.fYl T+{7 wӡU@CuZ|4h :sm(kXO@ϡ?tkJ _R|XOJOܾIk~EǰRg|ԟ>~kSjP)} 'П>|Q"DR8U'm$'ƈ e/;n7ǩj Fb.u8+$CZBS$%ăܵA ,h$0 w U@CuZvgΝ-(IrOtL/OCsM3/} K/ު~:N>Jji>] -u/BJ̭ӕOkCY'П>a>*Uk}M*5ߩ|7aAY2d66Z3[~8>p';#: X 2Vf 3iJE.X0;d+{"y XB$L\^4x4e|ҩjwX`eE'+bwErvC!M CA+f6c"6lfblA9zxKM:o刞\ [>h.RtЉ`l#nR/eew4Eps}4㼌1x$D٬*;IN5/_3?{>x|ZJ2xF RG$jԤSXAf o;9Kv{X xd`S.G#4ZxW9G1$.vMvSQsX0 8$] Ū'̊A0ұkI)ŗT WIVR˂rrSq%ޛPR;kP#6I"jqHIurGRF ~c.ԡKdJ.!$UiCI7׏']CKd̶wkVɶkd`Tq:A ;&\y3` \ (Uڠ΂KZ72q:6 ՎHQ D3{`n`wtۖٴmkƍkZֶͭ[]nEŖ$RQxRw7MZMh&fȺ+Pk줂˓ L2d / @#ee]Xdi~sr9]Kň *z\7p'2ND]`F[SʖQ齧[4> bf%䶊mLr`A`F & weU@CuZs]#{`j5W{aJ/_>}JgРIh/%~'էBRO}IO#sMMw|gN_K ORZii@"eҾ(O>zY2d'!2E!wr*&Bc\{oZpUljPDVI4r=lk5ޤJT,8d !b?܀_ eOW66[\xɀ$!^rcUr)SD:*dcpҏMbK5i R$|@'C%QiVA<ޞaEkp7 =X\]tb_%$KD<)FF$j6hh Tz7$3it}JyZG(ĕéR0 `q)tm,%xj'ءa9_ ĉk [̴(Q^ -]bǯ11C${a N;IӃz6V`[]x"R:Qָp mNF٤`<xL*t滥O{d>W} zW]/~UȾ'֤*q^Ii?LiϩR}Y4R}M2UV~d>}Q"DK1+W((r!i 2T6\5Z,K1|r lBja' 3j hvՋ֒m#)'Ju"_fYna Kx.gV (mELH⨵mzҴ̈́ĵhIFՉ#p⤠dثks!nܿOB>1떫̳^+vd`:h"}3((\PHtZQ +` YrlQ$9C;ӱM0/#s;&Kl ,ѷ8yl~݉_4ֵu76k۪R~"E;/bߏR*'gK~UX9heXk.sn)BtmL6SvաoqH`6Edalu=Rص1-vCgL]2 2q %MĿKKoOU3ibb![ c)LSJaU&L]-,܊p9 ZW3rH)K@ڔyS>_MaKân t=Z2먉TWmLk\AmɀƪnL.Dz9$ $}!pߏ&TR+Jf4nlYLa - w8U@CuAZEmk{]Sۄ_9 9niRuM*P|d|,'{^3O>0VFã% JD˪T=-wdӭ]SO}IXPR~PQ"D5<' Lԙ ղ`,̻#,i`&3Y΃219ʛ/RiIx14t48S0T˓, u%*u eJʉ8 l^M"E#)N0XkF6VS`5఩YCeDy@A× j|$Y t6Z#wK1dZXClfY}N%id/~ V5ua0@`tXƱXE#B%*i};6/N0d+;*J1MFgmDh]L')kַV܍|]%^1nYۂ:+U1‘W:ԄTmْC9$:Þ8 \M8d-ng%ˏk֛"вIa)Nvr8;hȆ7CR||p[8wFaplF>$bٌwJf26oAq"XQ=95Ds^jQb0\ŅVYIJn C47Ɓgcm11R|) 96Ʃjl܎9sgA!όe&9|u߬κAfK*2Q(7SP60Rf״ w|U@CuYWms`OT4?KZPtdߺRJ` /R} 4gR5/K slQ^O~XUSa2WJϩW#J'ˡVg˟SJQ"DK)X 4z)۬T4 qLG ]84n?a5kXAh[[7cKG[m&^"8 g4<5AP20,se;#Z+`-꠹|^5cc!6c}jm05 tN[ZƓf䥬v5'aqCT!mBf孫ۮ fKmpݴeMLS3qQDx=PdeVUi b-\gFdSj H=TR"66*tZΜFxCeAMǫrGٿ6j媯[qXZֱZk)Q'Ѽ3/P?9'xh a=ގ֞]VmkTZ/d((Cm88:p2Zg il֌66#&jIy P[PѠ 8b[Yg W|k[gFѹcShr} 5-5Fu2mJxJ:,zLUZаY6SVZ֕b`d0]80G,b*1J%Fssf+?-vl w.U@CuAY:gΟ)% UB|JaSZok>~FXiafmTBKa(OR}IԡVZt2e/BE+{gԟRZ*;ᩤO?r2?RQ"Dѓ59ܙ,RBPQci/Y59ӗiڷ6m:uUA} j/X+m]}k?74;+LMȖeMcl״"d`MiEl_ `H?eHٜNdTVlZFT25sYęQLqԉ^f(F155'@Mns`f1LY$F|[n+W@\a w #nR5@aJ !`=ִ'l*J,שG @swlftV٢sr)&Hnٹ~4C |CEQYbеyS"$JgYLJJ,nKc|*ؼ/Qz5ոcl֓9-4k5U<呀D7dŕYOm< MKݺi2BCV5$E2̀x9 WNiYt <;S!ِ.IKLwfFT0$`rM+߄"J| eeVg}z5[6sH7LsFYPpsH*4 J|6N ]/5D[xvT6:66R#k*] wU@CuZ|󱮩%'Q9ژO֭Zo_>|K_|3!?K 2XULRe/l~S{MJ} gԟvdSSY'О|jOzD 4ʾ}Q"Dmq#Gt,Ǿ AucDıE), nZ4kkt6S~Hȱ+;^$r &iMZJs!ԭt)̨xݴm 2qjswE>"P՝țW Iv k`=rw^[_'`,2csBޝLep-> tᆛaF;MiBh#i!xAy<1p ">72!kva tR jc105LQr s2 <3OWI?vJw4]fLi#.pOc5ɳq;yu*<%Sw8XE8:-#u1OR Ayvô@,쵮է զ5'2e װ۾\ʸ%9]\6!W4E\ՊIΔf2hؤDm'2Iⶶ(Th:23X/CU~^:vwK; >L&p !tteWvmr] J fM ("EbjC51XmEHZ wU@CuZ{ZҽZdTpsZi{N'aVҘP}M'ϡW*3OL54~*ϟ>}]++i_T++}Zʩ?K 'ϩ+DVZniJQ"DMdz+ {' v.xZ8,6s65W\8>MJkʞb5g\Ned{ 4m{†̈́V*U*Z6 Ę.ؕUs^.y!&c1oFny@ZtǒSnYEYpTJ _!ke 3j2nd)uR[7*T(sbs%MI,NnDi;)32'wjoBMk[#J֩L* A:=Mv=`NyC.{iF+8+@eQ`XAZБ[d-!< }"LLFեˤz4ͬC;Zn sÀ֓F9) wh.Yġu#u ;ZAcܳ*I#[dI;(#p_Ru!"fǹs-ʂIRIjklD{$9"ݝ6!ۡxu [WdIrjђ4)ؤ}Scqv5K%;R2V ‘ lu9j*MkUVepK\r~a剁_eBƝ%;YE(4S w&T@CuAZ|Dk~Ϟ֮RМIsZujP/K *R4CRPJ[jaRY4R£n'aSSIUhPS҅IOV(ʟ} *kY2d:.WZ>zJ (qR8]1k UX>N540{TjEyLa'y(P.$ B3bU~d lF9swB!wU5-M"K΁ŋl`a9!a8`TBuEm7ĵrې.-GK⫺<|P -A'@˳Nu e\p#  5Ȋ}T`?麁xȦ*"fcJ'RqxXB56ekZw^/&^4vaWQFl.ϱHMҜzYE{ oaly=s=y Y:%nD[J ޅ˜fZ[# ̪΍zٱBK҃h. =~ֵk1CgXVNƑVhNGQ CxZʖ1m=~9]B[2t fZ2`' wU@CuAY^Y¦L84)ϝ9;:i_B|'ϩWJT(O?J '> XR)LbMNtzgk>}M,*ϟ|2XTܾ}Y*UЫLQ"D49kɣīZϠFJ@Ekn%^JkZƱNjo5w#t m5!Zdې ߺ|efnZɥ=D?0&ʸyq3|䡰o~gFM"ryJܘqk(5w^Z]EQ'쿞;6JYGДJYX6[Sb4f&bm<,T@oeWN6-9b̅ת)lNT+ɒgR ~ACo\?u:vd;$ 9kWr"ְvnX-M^ZS.#b,2ɻ2avEigSM2Sz53yqS8UB[J JD–؎2% X@d O\3uEȬB( peN 4]uO슫4LZ+kjVǁH歘/cB|t鐮a.ɳ9$6D, IRZi0ۃ 9u*Igq%v# jx2wQ\9T{9knIu݁R5V))20ڃkX{iV}Sb>ԣq\y K b[/R5Zp/Vƶtqgċc mC h~*R<+fle-VHWʒFCs`fH;;g)j..Ql6d|p#`_))&tWNqb_ڲvFt \CS֔e|1DRf/C+#\/`R3`Z*CL0"3{ g0IoE㣀MՏ6Dَ1)i)~Ijюђݐ-К Mp gq^kHp#OFmq\t0ty O?)7c@w OӦ–l'ӛwL9/rODA6Z@#a#+RԅYmơWAYy[qװcfqTu >@/~ԒD iDbbLhF'V@l`5v"IM1Jt\.짨0l8Si/?gMWgsP6ĆZ@j wcU@CuZt *֍7Ε>S]9=YXJ>}M-'ϟ} 'ПRuYgo_ 6ҿz%}JfmJzTR} joփ^ZWԡXQ"D>.iF3T7菂++;MASFIKccrn\҅HR\p;WJUt6R-SeIj=\ yQfMm@`;آjPlJ~9h>*8$]T-l$'c = 0i]`i.syIkȒIͩ)[{ Lc78Dh>$z _7z !JXZ { FuM=mU~ AsB]ލe.vu[VkZ%D$X ֔ ^tNk\!ȷr/lX"EZ)Ӧ:mh7RB{6e4M`r\3·c3/baweUbfwؘIܦ[f^kр$IlWciwyto=kJwkI4ڪK/lqfV9*=b0n"v )"N ڱ4h鑠+N]Gur`f "AX9[rIvH5TZX)+eM+_\TÜL +~H-|_|igJeɣOQAxĖlKgU0rmc w`U@CuZ~-TkL+_:t;%+*kj+թZ*U9uM0>{PzM(O>+%WϟjP?r3P} ji?s tHY2dU/F,Clւ$WPUq|-(a=RXcf.d[&;ldS䈳qg4l ~`=l.8qĄdL)be$#2[Q6J԰S`INmk*)՘ 9vy#LVO)qտ&%2sfZ0RҙN VקM[0WfEuq971Auh$kD4">[\!0pXv)M%s1/Zp$1i,Q0%F4Ǿh+Ґ7T2sGV.[{' HDg;R"yVT{֬lX6:Qύ$`9m?Fm&lm.'$ H`LY7$$YCTڰG$Va^iJI B1e5wݍQZqG# I`Ʃ6Blmr M;$K&$,kQ6h[6a.MWwt\AacJPW&x1MBe((%c`X wSU@CuAZ|:{]M7ϞJ綫sZoUҿKZo?L/~2XO?K9'ϩ>|N>6ST5g_JL'u|겪O?K9gv|i_BQ"D@-$;k8| s%ܙYwYR?i!FQLp5pIsTl,E\2QTV;xO^=)G{"crF積y;ŊZ\&r]05 gF&kB)Ac2n%4]"qn3ec| z4!x5A9CX:Z 9ghcP~0rv{.L'O AvV-.;H-iHƵ][Ú C.ۼ؆*,r ]JJY1+tÒNxFs^6Ą#A 03W L2>m?D!E}QzX:[lF YNMf8mP3&Ev"Rg#*YlMԡ۝ yRV~yIK4fԭԘOۻrS1}D.xV@bb ^Ko]rڭ`%Z bZ2JC1WP(Q˘V0Mhk\TtO]AL!SXm`ڐB  m.졲l a$xo]ꏜSK56%Rie@mY E@U.pD~Fa-NwmY4gAʑնm7iA m wU@CuAZ|Wʝ>|sЕ&tΟ)ZZ*v5{ S0*{ g¦jQֈBc~~Y3ܾ~՟tܾ~,*OҚ_Sra6h wЁVO6mXMpX'),!,#D:m_~8I>-1D!},Wc^rRbĄ1.qa%\xcd23|"ĽƯd5fHJNUd]oy/`|VȰAi][Ouo "'s`1g=7I- V$9рGM~\\J>dR'C&?jzilbhPTܷfĈ#DІ!<-ҒZ޴wI B.ICGf"65.wj@Ďr LCFBUYu@Ӥ@KD֘½@XFPѩ\@$Kmtmvj;TOe)1N҆Gÿz*4EHfCaw'&@+* }Qꦨ%6b'/)tG7"}r)c>g̮s i%rsBGTCuyVO]-W-#ՆAB«LiK۹0>a:"d ]CMZxvʖRA͈sw׆[ؑ!G 6<&9=I"F׿:)k RY%\Ur.m w yU@CuAZsmҜ-l!TSOaK]?K|IjBԟJtV,54Ws XO>EtFh(PCSJW_R}Y'¤jiJTSSLQ"D݊v՘[VF%2&72^CLhLYQ2 SK̸2ݬebJ+oV#D*Qܞn*q\NۏB4lb-S G/ fNM:RT{Zڵ,ݵzkl\\1lG4&|c:WEhjU܉ĒcUrP$!i1VP w6U@CuYNkWVFNk|*k| [ܾ%-LIkR-/VҲ/>+rꟌ/ׄ*O}JRJϩ>}Q"D͌Wd!|pvd{z| 'PYiC PjW)1+fqSL)+BLGo95uZ]:cବ-sqmkPmT4-hZ`-3E4ev' `ͤ ]ۺr,566N U+mp,vmh.i0a n^76Z=X1kX:Nhȝ4nF$oap8kfWKm VROi7:lu6!Q'z39HJfn|Ǯ fLK Eٲ +l0!2یCFã!4hAg m6.R )` NHFkYiSfSg`U&Yp(Aug!海P5PgnRQ|Tֱ8vQ? d iBx!ڻe |b譺Wk\cDϖw9VkYk7!fU5$kkM]q ~\yo+q]ګj%V4-k2`\C tE>DsE@#*9pn0d664uVEt EJDzw!Tpd:XK mymuo2 wSU@CuAZ|o=RgNҾT—5)^6ϒֆWi=Ju 2ϩ,AMTJXi` Vv4V'[eNWAITKY(i%U24?LQ"DB|[ٖF .;gm K&`֕Y D۠ȧ0']ĝK!66b3vd9l(MY08T\~}& 3;릝<^j$,潶+wD4u%+̓K ׾!ʅ PXEVRBm 2Y/2N*m&D7<2{ycAӨGڎEc$-\NKݲOmgF~7`_a8h]ʤXѬb5j6diNdPM"9P?D_ ;knӵXRv$lF#`S2.Xfo5ʯz" - ^:i̜5kZE!ƙƻ0Iw!.& ɝUB!Ws;f9 뎴mP|)q>#41 a<1 ӭD9ψɖMjF;jVOcVHI^i;R{ I@7s&vholUwfR㶭jFT Т˄DFD[u2zla0ƞd$UC`%MC wU@CuAZ~EO>1>So>*TuW}Z'Գ~Cs ܿTԟR} P+J%ϟB2Z*P|+I_>}d_?s dҾHQ"D2Hv e=Cn p(`1$c-DSH[R5fZXFuv5}4͢|>ww:5Jkֵ#nE$r0"V~1  g$Ep"aU]BY&ͦ(#\R@kr1My5?! h ̤6 85:nM.3yAYGX& sSIqY92F揄 <֮g[*{]Ec15zReLp.X-GX&GKHqD x^X<[YsG%kZ0c&/@A9,񡁕 b OwILxUO58\NL[o lFT\qMk7o̳bwp P¤m 9m,/IF@U r򄣌Q3lw?.Y?ĮZ k&AVZ6tqdt֩V#vpgK6DSUfV h\ ^0qcx1bd'~#H9Ήñj4v\k}uc7{y 0۱HyTbGwlT8 ٧m⵶yW1= wPGU@CuAZ|u4hL4>sQ$(ko (O@{Y?tӗԟVJ"JikWϡKI*OV%~EzЗg|Ԟ~ a?kIP%} 'ϟB|Y2d;z=?85l(ܡ18Q=o8"F5دf0iA0yIar2>"s)VL|5e |*,Or&o< 5ha9EpT\mz`s=M8xڗdFj`WB[Z*׻q2 Ȱ~u}. e 3JF۷e0P5-TM]}ڵ`:'IҲRH)y{gvx S {K ^A m;t'v5l}چٮ!Ȣ쪽ڠ) ۱Nݠjwv)9SVt/?mn5ٮ^K+" d۞C;H";gOˢR);swH,m$7*h#KZ@7iF1uOsGZd}lLm+莑X'kP [8]Z:H! wU@CuAZ N-'/rgԞ}IL?[j_CsI/>'u}ZWҾXP>\-hO>#[2Ui+҅Jt>Ծ*oh5ߩ7Y2dꜩ+ۑcy%1C\8 Q';Ew7>@4.*rؙ-THIINsJۤ')Y8t+##:- Z%sZEZSw0|nQ[mKe[Z28-`m cfl,5kՔPL(ST Dw8pUϐ7  >b}ۧPdjy]j%r葜XIM%lF`KoUSt.B>9f)-E kꏟ峙 p j'̐2Su, _!LF @$Gf SGiiwh Um%]0^3Îֱ;8#y+&k zgC b SfD&慭lfbHnl`N,Drx|5B|6ҋ -YV.0Ep'e sm=@qAƉ䈊-Q'Xv T}: cA|cԜ&N@L(m36Z!|@U5L/zr:6;OX;;=HKWushUi~v^K6;v*v*mN2| m}eJ]Hgd V9|Ç w_U@CuAZv8Ji|wm O=E4TաKI4?KMm*Ү:Ma>~U>3ISJS:tB*ΗB~uxTCKM+ԝB|KФ ,IlKY2dT$5uoqA3`s+!Lp%"e SB Jj`Б($}ѓx 7AQUiFx1MVP0^g;um¶1\^OKsLquQ#Mﵴrֵ^cr<^#`codJv>U>1o1K'#1:XLSC61-⻵F U#nXݨaMd+7;"|"O[i&Ȃl#f ,> PD=lOdRH‰ Z6C+.,x1T\޳>˾wQ~뀧!WFTUmivmvhzSօd~UEY "Z|D@JR;v9)eBQJF~? Nd յ(8?Hѱ?$uhPF%|6-$Bϔm@-7tkq=/coаF%Љ.y-9kb1U-PNo*6x8vM&??ރ:H2  )UYޞɌ[H/G x y|A:<%@rԇ6z)k T5$2X wU@CuAZ{d#aLuW||S5/?LРz[O>~-(PB(JҾ~NaB#*tMwOK gI_aKϡG{i¦ϩ>|5/ҾY2dmNЬ)Umʼn?.z'a2x5X #dmNQ%d]e>wdpej܌r isHeĦ.caE1I^B3%2q⋄ZpK8eK> ɭRlR]g!C.@vϷ| `[$FMՒQiYsf7Hh.CgpN4@PQC |/VfF_LaP}#fG=vcѽ͈ S -`\anW5h-Qҷ6wv8%CnFi mSZb!r wU@CuAZt52UӥzeOb!үW>}94W} Zi]PNy]T>.gqaKRI~OR} 4M3O~:_>~Y2dZe!%˱>ȃDB; ]#,3Ik5kQsP# >XWwՍ6%?EBBYv~Z"YVtR4k#r S [9|UrqA"aF%vsFv 0 6+DԚ̚]X å(j$MPXy%(/WDGR:oJڴEmf@|洓g~V)@?1B7{ _cGC0=J1UUb-^z(3,V݌Q;]Y&BO ac.Y 6ͻJ5L'FbFF`NM3 1vd3+Iy- !+t봭mANF506/Vđ ~heuoo?oiT]B"qn4&+kj5O0%bXgY@Pj(#HnŒ* i-5ALgJ1d0\يO5:7 5>13iWU5kl+0]8b(Ѿ) ,}sC.Hz<ϼJ֮4LXJ׀ `TwuF9II׺Vt(PҾ|㤯פ'Ԩ{ϟ>UVÓ%/?JWԿlU=-_N}]RʟW|Q"DkfLGԭ jى~%~ egtޢDRQ&ÆܵN#[0Ì՗& SHh6~gMyUKC`& M1WcԦDj5,L0^I#)|{M!M_{1s2fΧS:JdF5IdOF& RAQg0Ă$OQ oE(Zpm|ej7BN6Ns`A5L5i,G¤H&baiLTGD La& j| Y NM1j%Nf5zEȗ_?T#pUΪ5uL1@lՓֵ$ƲAM[`in j_Ozv/̓;0@"VZ0\ٶee k @M_9 n,xzcUۂ+bdY^ erh aVRIHNCʮ;eϏTXdnu Ok wU@CuAYE'mWs`NׄN>}[ZdR 4 ~/Ҿ} -*ig+/„Xy]5/TjW՟{aW|Ϟ#I%˟WJg˟V|Q"DӯMW2RuDg'2 ;N8&KV֠ eI^ko hksõn/Yֈmu6} vA8(0]:B[xfS}d>_Q 8km찤0R7QRqSF̴‘97gA䓟?9',0q RHƞYVnԫ5& fT/u~"nEN5&#ö.c%qZMkZKj[[3cQ4vn1nM.dBA6Po4F!קK{)S&W35iBKHqeX.^WY JRMmt1f5 \Mm߷KZ3:B^.1'nYRʆia9SK#/64|dJ+'v?BB7US>RqҺc(TB-sivP!+SPϦ p }ީH(n%-kZƕV{ZSQ]S 5Eertwrz\;CŊ5kQNk)Q'-ו-ד^%"GG xhӎƴ"+{Z]MZ֑0 wT BT5|_ k>Zi_R4VKԡ|(ORZKZ:K (iiBgS-g}Jj;BJ\_VMR?RY2d2zfs4U8Ι`t!M6{ ['QK sKfUDR{ mcAJ~< ]V9` dA+ֵN#JuF;Yi4ջ#W/(ܪ+E8AH5kcbTƭl3g$ku)46s &^㓵]E]?:լ<8|?t(Ga]"d,e?}ѭj2F\uX+CK ,= @'^[ qYf$[?+f=|b<\.6rayiΰ 0ȖfDiB Ko.%vSD%2]bZcJJ`ԧj#+eAmb$3\T7m)` tEڵj" fFa^kS 40kz8:By0h>vZEom+j9q AFڐJ KkLC$$rȍkBHRmzLm6jxPݺ7ÈRyIh:(J©<nFOXՙbCfb9MsxEfnЖ8mY7qf>qiO)4@q kG:K+Ab)Pyrޢ ˯Ld[浭)CнRP wfU@CuAYS%'T:tKosZ9Ko|Ko_RZС?TRT8+Yl|-tVK|T*T0?SIgԞr*Oz$= 4V}Y2dQ;2;F^mAyc)G楣 6(?z[4mT8sy(kNl?:v$"sDp%>M1Nd'N-+`rNru^}Yj] R]Rz6Tn7B jaOZ'$D˜L K..pەH+$ L`֍M&«0p/\7"ڼPMJ[7Fkģza.+b٘ nZئ݇)SU+$uUl",-QI|6̰MP}%Hl6,:J Ƥ 4XLXW#!2kA'Jc6Mi2厣  3ى| ޗTH1JڃTD*%rKMM<5Wq[F@Oؐ$օso.nGJJU^Xalv("EMXF[.؈G6t gkBL@^ndLddH yjiⴂb4`8d0Kydsd^#3HZ>m7װG/^Ǜr"+['~Qrh^Ľ0s l˴ozqV-+l z wCU@CuZ{Zϟ)`Po k]T_?S 'ϟ>}M2ϫC"2XP?T~m*O>}Mʣ+TMI5}ZO>} gϩ%D%~*n_?rQ"D+KKtDr1l> {J{7)466GefFЛeֆjfBzD_.Ia6sXE4[[.R6aB'2Iz얹P}d?:2UҦC$f& fj6"Ұ5y5#n"nlv'fd`*c)5"ֵixBˀ`72U љ;1.F)VZni~Q"D#y\#tfI; dP=יikLhD$!i#BUct5d{ \nċ7K L!O;n7 X]JŶSLbLCc` cR- /h,^ X*nnF`6&d FXrㄸЎX/˿tŕ ƛ+$RV;r9jyNZwRi^e(xmx .^6Wj;&4ё[V j^6mQQ[\zal- iE7IfJ`=dfkV¶"|${7h86ѧMZGHځrݝ$U+ ()x8m«ڂ[_iRU0mcVR)P\'Jl C:p N}FSZ|݃|KX$&5MՒ0ܶ,oα 5G5UMRto#Nfv"VӲU#\ (I^2qtLq ]+rd TRia d٫k`ւTe wU@CuY%6F+DN֦ϜKVB_ϟ.} _Srs?J*6?S pGTt*s b5SJg=uRM+O} 2XJ>}Y*TG~Q"D汓3TfWvv+eP#؍g*"Bm%4y5kb.֫Z]:K-7wX<& 3Q@߅˛-lӔ 0g,9LQ\ݽ(;(~ǭ߰7o] '\UYT캵#U`Y.Q+[<<_9R~PK͇ Bg&}vUmO K/ٷGɉ/vhHGyQ8j0WprST4@" g\AC6Tim @&9n9R*Rř0(-Μ`>D=KфLɟ{-s]4]+h(kID+MsAHŸlDd8<+g1MM$i]?/ɂ1J9m \v|z2+flx72ᴎk{\z(᤿lI']P҈!iDsk-ЙeZ؉&Gtrc^ZE.|EF5#~W:}M;W E_Q%~!}LAOA)ϟ|ӥO)-PSKXQ"DONc.*b'reN cX'N)J6I{]󤯩ѭmoe}M-'ٟ>}ZF?C']U} 2һr #}Mg/x9]WJzT} *n=^Z_B*T?{HQ"DZ՗b,2&}lkרbTVlזmjs 4ҶAUʌU'̖HzO Qqک^ 0MS`M@I6٤rS }֙61A%exKWLE3fovm%bZMkAh۪sJplk Qm& A4▏;x%F IR~Ώbxj\LB$Apx[V5ƎZT.㜂`>m^g!BxOqdլ cRYPֶ^,7ZWhFpzx9>\KȒ j_J 6٪ёF"xg4-o繬83mn䐝]>,\D,Nc'7!S AM{.l7*unܒZյCRA I:Gf5 - jNk\W0+w"Ya∦SԕjMޱdse˛"ɢ'@7sB̳Ɇ{쪸%}&15`⹐ wU@CuAZ} L_>T<&|g+0 'ڟ+B_(OҮ&rd/>ia?tЩ?K tꕌ4LY2dE,v%h^  My<˵xF/En?uK\bN8\Nbʦ}""b3hc"5 3i,44-ٖFkYG :s5̴#nʰ:ĸֵ\YhIk4 cOt%ewQf #dՇrXfk(!qISpmndƂٿe 3BJ 'R!&S5sd1)>S|,N Jq`1!|MW6T[/|;[w$5l!5yN2jIXRwv>d1!{w@e$#9Mw#Gȏ,U8&kZ&k*,FZ $T;==¾`t#H$0U48*7IWchH9I:oh@Mj`J}]/$\}GFjb[V®ְ 0ƹ .z#AQ`i@‰ݲfq6ؠ95/tRXna?IM*Щ|O>~RS%gթ?JKu|П?J󷪫J'_BQ"D,,d>\fɒx^uTť\mpf|) &)B$`k Uuse^l Yc>1oJpR+EO*h`WяǨ1;Ɏ i@FLhHO4Z%}hv-slۊw&.cl7Q!Eգmth1`P">ʈm) =sygk2V@>bvo~2ܒl xE9QkBG3pZzajMbmiFa*-s}2ܓgq9JjnHg7_ZǠ NsrL`TvWN`D!0 Cn21iEMm̝r^ĝ&Ń9h+SQq?y1 Ǐl]I ۹J$ږ@i*? 3'Tx ϸOZҷɕ`h1FxMkZ<9pt%``DlU#I2&, ]JJ=mw1+t$ᕳSZ֑xۆ1a\N,nb+̥j39') /E/QjX:䃕v<Y2d,;lmGJ.[>g>YGH8 C< S[N$VZIQ Ɋ.)bC(lg0 4 [ܣIqPƢ#2$jig8pu/[JZjWe +Ã$yG^h cZaZ]J3'lA?߻VIn!41D6d)]mVٴ ^=e} 񟴎.f#y9YSx"CMS~>ݔr3w@ֺ & - mbx~wOs2o AB:Xqs1#KfcEs$g αИ \AJyD{cFd5Z$ "5VsZ4+Ċī\:&,jxkLjkn@R/C/ Mr| a5bfD,$Cf$4d`[G^4d+"dc1SK`s M٨D,ٍ*-z ٧!%l^6\֘i%AAy]KjJ~lkaFVF u<Uwz&@:R접0Mm贍!SR *vJO)؏B2YY1K/dE]s6Y.d wU@CuAZ{]L꟨ut3ΕS]gΞ~iJ٩giCR'ПWS Wϩ>u3Fh ҆9}V֥YZiiJRY2dRLˑ? Kl]40h%ػ< Sƪ}“_D-ȺG+FIM"&CD u;dX3V"@%7ȶ0DRdD Fފeـt[?/nRnbnz+!Ɏʶ]V bεR}T;&.Ԅd@Mu*4 ˾m0-CJUɺu,S*mPkոR권'ʆ&}@$ X:9<jOXvB]a0clɢsC;V+! "^L. _cz8FyFkW-0 /t]v^. @PTF &N}zٷ_}2ILF`; t2ɒees)pw!)|w vr^ XM]hu42i[GjV^4 0bJ %ܟJ8mlFkI0:0E\BH6>i{c1oF}Nm-n Dk[H)iMT`y wU@CuAYj4VFkT*D+vӯ zKt޾*d^ל-w4 6 ]-uVr/>,rɟ9|7/ׅM*a?LO|+iiR|ϩ>}`Q"D]QR$ Tg:%@ON3n dМH5ԁ? Q#"$C :YSHTk`ٍH\`.PliYuϗ2pVԖ' koPc֝CΌ7QDdgXPz !I$)2%l٨nA! %VkS>Ml{;G"a:XN e9)L#XD/+CBYݧ=r jfLݼxҕj 9ѩjZ6 XOv@SpZ hƝoz>H@B=7alivP.R0xG$q-RƳ-Ԛp/Tٕ飯+k.(Aud6=0< T0o\SֶN-֙g)t wU@CuAZ|Ϟ֮SN>Vd%T6ϒTթB+I҄Wϩ*MT¤ i_UZSjOԾքnUAI4&KIW&z5SJQ"DJ*|hTzjAڻbl?{16I4m,֭F*lڵ`npkMSڇ} <K`XHo]Cbkm;QJikHt/# L `˾ULo:P񂍹Б4]7'pƷKv`"B܋Q4f[w>Mӫ@Y"'1Jv)Y5# UPئ1Bt[,3>M}0$h2TԚs4.eb8DɎ.lc;cM7q 9Y$2k\浯rpTMY+0UV:s$b}:5Y(d׶u4(ƒ:ѩM=N̹Vw| C3@Y-V6REWmO0t!VlƩ쑅/2N*4mM'd\\h muME>) c[h]c '-H4w_X6uY5і8 ٪cPMINePN"|rg5>t>~/*TuW} ԳpCk ϩ?T} е+J)|OB~WWԫ+i>} ɓ%|_>Q"Dƙ1$^FD`u_- Vcތ5+X(ư&n:\a6lG'zO ̳^4eo- 0(ce7BB_JF}gv74uحyW,䐈l&I\ubGȎ9Li6h-LjԘBs" $xIi6ӳ!N3T)4=po b0 kSH[U5"ȟvnƯ}~6v/~>wW)F6ֵ{H7o,mZ+Dgq@~Hj% *-Ԥ-6iMc[9Nkr +5)6_q!mQq~5:nM.${ ثP68S@.M& "Cdҍ|%Ϟo`P^,mqU(c֩LsHzX%rG] :hI~8 6(kO`x^ΑvtۅVnsFk ;pؽdgf0r=QgJ`ޖ?J녪}YZҞNV(OpsxʒwkQ~a>̳bĴZS96D qYb$E w(U@CuZ~54hל4>~l!tү-hO~uhnkCLZOօ]ӚiR ϩJOBY#%~EzP|*} O|3_?CJj_>} ϟ>~Y2d4RMWaX=>^J\!?ҍH^쩃V:e5BkZäԫ 6x:rS0߹\v(\ͻὍsX]˕ţ!.۸<է]>&4xg; qtODW%. #`A+ kpclt}梼q1[ٗlVŜ|5+Ř嬛Mcb 8P.+2"[zod5%{f0i؋ۓrgI"뭄hIaP >"s)[G *k?Q> `9w3}CsIv5hb5EқQ:.4 4kӺ<5U)GErJ[K/2f7e3PP[@-V]}ہj`;:'ep> ߮S[D`A7lU=Y BNL$7PwvqM<{zy˔UkF̛W`p.w|Cf@e\͌I]:5Kɖa8ʜK TubTW1+C -7\G+luy+I2Ƌ  n q[@n wU&U@CuZ} 'Ο-~rԞ?X_?sJO>'kJ |XPSJ(Pܾ͚kL2I!Үt>~Ծ*ih秊Pϟ|Q"Dbv5rr'qK ;zv5x5SXت [w/`swj_>䝇#)Ae5ck+*mdɹi0&- ΁;j%NߗW—m| $SYTvi!^+ԝR| *5 ^IlK Y2diQR#[; @~y~w|w2Jk˚}x>m֑F8Ϧɹ;Ǵ`e"h Gp[,jHVDK֑kA16д3mQqEc,ПFi 0XSTWBT^CmCA3 144"wB1 –ڟRңBV%5cUc\^ŗT@C 8\dL똉awH>pȂ jOnfw9 \." -  }сKA72'X-vlLi my<0/:ڽ+ÑŃ jz.[scek5z8=5F ڴ܈T >9o.'#1:QD6jGPScGvHۈ^;PTB\t"w$(UjJ3lѴptR@޲@6pEQ#%-ppaF}!?+`&B9jSA!(cLeTqpF.t*JHضm;iGϒbB6U-jSy!DS0 OG+B/9ݨFNJti-W`).lr w7KU@CuAZ{w_a }IRt|S*U?KM,'РP>3~hJ־4Naa$c+BL75ҿJJ_FakϟRGn¥M+ϩB5/ҾY2d8Nҙ'nUƏ8s׭bu.< 8'.*0ݧH%k| ˂d+ BQ+Fn> /uhW:äYf*r+.eUG e-d²v 88AdvLx`\)HvTMcwQvߑMhj]<]W@|CK01o|kZjU mv^:˜@$@ג)7a ].,veJ֊˭^nHZ5J!feIݪ>#Ȳ}`gUeFW`K)ېo' BʙWR.|"(#J#r3/ NF1BTz`| ;4$W ]4 W`m{<| f?Ų]䒥jnbv#=pҘ.r' 6-k/>!?PQCԂ.iZ=jt1،l*rgr6 vB+2M%Fo@B x^c\ӵ%Oܹ0Ed[ak]ǁ##1r߁ chtժ^J*0Ou"RUA.Hp5HNb9mׄa)SOR9B> w U@CuAZt-tj5%zga{]үߥ}:jOR} ZZn)|]BRO>|otcr/ҦaB} /R| eJgП\Y2d>75h7C2 6՗F!W^5zXHi+10zJ$ V Uװ^%SoA8!. xVRZeHWX &*@d !ȻޜdNLokSbƕB'0ּ$0OÔ b-;{ kZɩUpn`%G5Sv@x _0Z i/n6&a5j%JEFƶ'\iug(!tZFmB}Bj֮irYj0EڅYZş-A7ѿbp(G4X"| ˹HIΏ;(j Y/3=4ho=6Ic%V(ORN;cވ4B44t<LrlmŤnYvyzTlKa>jle'֢ZSYO0,c#fXzGڶZ'\QV&6tp=ea$(O%ALHȘۣl \Ƥ+X !sF0[n'WqR@ŷ:܆M5#Wpa wHU@CuYߥTjRtN9IM2eV(UW|da>/|'ҾI'՟V)]IiCJ2ԿliiUItN}M2ʟdR}Q"D3[2 VjLC 6N$:p{ 07KDZ8%rlƴS& p,lj" ?n9ݼ6?vuֵkbEpbFyS͕5]"LUKϷQxUr9>SVb&b׌& mDSՠ_֒E#̜98ABIX1DF <e8"kx]%'Rŝ&U< 1d*AN5MS+STwV;KΗ"Lq;t,mmqޢK rPK.6NpI4\LճR3$jYDsZbAm0k]Lx(Ă俔 !Mj”Z!BQ(ƐDl4/#!GHo36lw]"̻#Y+fk6m*Vr@b2m9ʗm#ICM.u[FWB) kl$v[H0^^W3MD:%l;%f6q ҂1ljf3:4GD |6-[o&Ǭ4Fcfv w^U@CuAYE%SmW{]sׄnBU:*O>uZ5B||-g{mi_R+_BXyn7L*IaԟWUMM2+J˟V|Q"DcKJ3_n_~ V-<5u1ݴr6s$誵y Ҧ:JX/pn_+' ~\YҦ)1\#msZ-MNRQV Jqxj_Ok*+ձw#m"ͅAL"n#X$LMO/ԩI HtUw8t] [3% ^+6\Y.lYi#EdC)'CDg";L;'sv v+HClLRcHU<.HRjk&fO9}/x2.EDS]g;nɧHƍ.[B.I|O2tYrJ/ho4y5JQ96;JmhX5nRKqu7gA9e(?d9l6")FZ,& d+Rрk2UCI۬ra0$ɤxvS1m]b{Sk5楶2Gi2lCM&ʼd KY ۃ9gef^WRd*1Oc&eXh^fd_Ii2}]xiŗ %\u+18Rnj6{GrR֏ut#]dH?']Ғ6B|NEm% ' wS0U@CuZt*NmK`kߥT6?K Wjat_SRI`~'g?S](UL+:R(O~՟\3|jPB}ZWaVML'ϩS|Y2d́q]>nTH/EqQ n.F/~+y\8̨kem tk=n,P˸o^1y8AGAe3lJ:[8XmٱL29Mu.H+~W#x`}Vm~XC`Ʈ͖7"3\ֵW=k~4KlTn er,N~%S @c !w!䴻\>mi5t+q67F.C!ƊֿOa@a,ܴi_ެ@ێǵN7 ^)qርa0YL ZqS,ƎsZB 4m򦩀Zop2ͨ8hI}5*ɸܟ4tOy6@R Q K7KNwRa  #1C}%s -.v8m#Lb рW#[l>P+Z: P įv'ci|T,R0n:rXV~!h1~|€qNuidQrASlPpKP=3<0)+5RlC#(!AnaPͺWcU;CiQWEAwU73CE2uN-샜Z. 1O= wU@CuAY9gO)o:t2JaRo|XiamXPBRn<-(Ol=}IV#r2ZO~ϩB{gԨ *5zO=,'צJY2dɸjkۻuSH E̛@rhKʫRi!+#jУFJwvԯi챴+vd%_2yrT(6[ s7W %OjruMsD0FA.zӹiH>n2&I M"J0c;x]B0,輜/z(lh kZLd,<I,;!'FGifGuPJD=f=P~#ycmfi&,cڕU*2RԵL0p\o[];/;2Btytp*J} VIKLwfj_:9g%.\X{a7Z"6[WsOx6lnؕ]j5 DOT:b Oe"_ڢOa Mc` Y$ zBxqOuyoo̘f ;fqa҄jQعlͳn`@{l!j +j*~=C J`d%b9#V芘/Mzfhצs`ρ{#ZiDO v<[caT֩쵩)6lֵJyN˷Lͅc.(A˵F@tNZf k$<1'>i| wU@CuAZ|+%ѡ9okVZ_>} L'ϩ>|3Կ"2XV?T2e/~3KMJ} TN5}Y'ϟ>|gϩz%D$'ԡ>Q"D5qSo#Lnb=O 3\нsS1»yTGchЎaF;K6d(l19X׉n4 Ȝ'ApR̤H]#P .\x2FG6p ?H5@qx"Jx'K:W1wf,i#G 8tqT̛<ˠ DI,ph>ŵyBjcJdFp`<}5CES }=cpoTDx6ͧ ըfi%c%1\/+\n{o]4F9Kc TKZ0da&+Z⠣k}dNrWJ >T&p,ezmi:'hjFo.x] DmB \!kݑj hM̕CfOkਐ* yD\xf6 l5y]M:ͤL&HZWr`-N|$o`Get^uSĵ# :m5Q3ǘVLlgL8<s>E_)ݡkfI퉆/eM#Т#U6C_?Ȓ $EIJ aO1* 䱽[a.v%R& wkU@CuZ{zOmZTp{a*˝Q: tgօJˠ|+iaC|3OOrU7Oa}Z*T> +]'i_R|'_B} ʩ ϩ>dX1ni|Q"D='l5tOMp쯢< &d` i+so.[ZֵcJ֩Ua:9TpOviM;󃸚BVqcuFh#&*|+lX(pH}$$,߈F v:ˤ=[ ͬ龇Nv5Tr"W1V z$I wh !.C[ˇ{]HCpT7.i51& 9^rDn$;^!{֘JnL-a AHE R&X.tbE㳽K&U!x t^.PB]Jظ:TF2LbRLCSP _mb)}(W2hnoψ KB{ FZcH(=d6 9Bt+Tf._LzaʂƝ!"(;LZE:F)mRɥKJ>qJBkYˣb^$;aRs4dmWe$jcXnnRǓ 6)$A@(bYĦW Hl+h.7Lzj$ ԱFվ}C@TԐi1n)VFL֒*0RNS{ffN!q=\eyMmM.km5d*kԀ` wFU@CuZ|~wϞ֮МKL_ֳϡ.} Stu4?J*kWKZaCL4RS¥ O{IuhPOSM(OrJ} *W~Y2daJ)y9S\AH6=̬:T@d]/Dq]JUWR 6%e'0j*R0 N(TT(\f>9x[rNlY@k<-pPKRxDF}e4 3uPt "[& 4WbV8H.܏M;Ly Z2Eʵ27Y 0bn+FsZᵫnM{fdCFW8n*cQ{fAퟹjmFɑfUР/uyu#0܏cO\#αOI=I0k[2uͣ09fXٱEs|%AVp4vz5n*3qLb^zZZY8uѯ"oT}/f %WeZ5h,FJZO0w%dG#?jOV|Si_?JW} XzE_)#~򴫡MM4CzjT+ϟ|SO†*PԫKXQ"Dpt7<:~xUv.2+36^FTxt!5*2^ƱpRr /^Lď:ikIǏGv]Ζ8 `Yv3!l`;Su`F)[d),1 ݝKv ]kC6և-S KVDh걲[aVVlgke`;n,lƱ`o ɱ^Ԡ[k\yxo6ҢG/-1;{+f̋^ i5A'l/OEB hvW_#Ay/&bBЍԀ* wU@CuAZvo>TwK>{]󤰩ѭ^_*Iܾ%YҥZ'FIg_?K=+;B} Dt#2Zx9~W(z_WLn_^_~*T?{HY2dbF͜v]m5r;+elF*3U7=ppUr-6K 'ܐO.5.}j؎EcUcu=bϞyGHHMg.dߵ$Y.I>EԨoBj}\J|ٍkR v,t6uõB qx9 9ڌHѕLPjΩme"\FW{Y4@3WSZ:| 2XOSL'՟RvVo% t4ҿz&J0>i_Ct՟R '­]tXY2d;% (f*K|<Sggh6]ިŸ|J* jݦ`4S8( (.] n߱nϏ9ujM3֑m!%*0^H. Ț:ֹ@܉qۛ>Đ`šۨQ&m>٫me,r6b v,c3rap/1SGmYO0I+@jUNzϊo@ǭ.S{lym^>Rv)T#ڕqlJGBt^ NawhNӰ׾$~1X%JT n=rr\2nѲ Z4vQdH hʼnҊH k SSf|K5p.J Jfs0@Wcdє546a H/foiZ'%tM5Ja{ PJ(O>~-3I%jPܿJ:WЩB|XP?r󷪫~ԟrQ"D/l#qc܇~o_^⻤7_4`HUsWikOv\ km18ሷSɌd&Q ݚ; !5C$ֱ!HƦ F`;l[eD1ԳB/ Iw))ĒT4.HLյl¥w9;ɳAv)ux)ʚU6[6!QY5k1 O[s,7/Y{>rfºw:UΦ$YD ֌bcZ"JK;J^sʡѰ Z1\[}aWK2$6|Xx%&mlR\n*";#҈&4FAFfQ߶ "[6'Ɍ섐ѷ ]@lj@}HPL 3ǴuOAyggj@=6j(X/^Xd%svw9QjչLhnza1tN׌\Z2ҌÛb0q$Y"(ҞhFZH6 Ka iҪ/%3/~'ba5Pf+dZ-,4lIE5~N,p/bN6,ǔZ-KלP.i$.p"5AwlM Xh9&ʮNH wlU@CuZ|Z:|M7S|wџM]_ZܿLMtSsNe/XO>|gϩB2N>ESLRYS_LkҪi_?L/v}]7I_>Q"DK[kЯ|F36&8Ÿe5Si)ȷ 2 M,UY =9'i٧:;D/Ik5!I}.(V'QTQpN $K7ι ǜ*эΕ׌l jFI4"Y3I5Qigpv 4줍%MmJI4wqMf/GةEŸIȾqq S;I9"ѩ5ِ ؖϸ`JF0PثL\xF &>5.*q).U%!S-1.d-RM񴳔<(i4BG$a岏S6ЖacȦ R" :w1:phD-dl2mYbͭl)Jn%FIucjOA&:wҸKIxZ*krMY=KXm:nȨ%Ƌ|xHLZKʙj7P)+-9jr[5MI~t5&qЈ_練Fe3r2o5msZ۶̖mkA-pNTj\g/i&:r7IA~PT#*s"`ta^eXZ wjT@CuAZ^WJ=sPavAgΟ|Z霾~$/&RgթCT'ѳjW՟>s~ZiS ~P֤2ZOZS Y2dӔ3[ J&ma!cWOL&ßh^nTsSI;B!@+cmZuZI3sLz;7uÈѪJ`bܤtn㎋^zHx87I>F hkHA"U9}#ɦ #^6ZEGxGcB'g tTieV$26y`bNPKwgvr?vq~%. DAm{qjŤH)Q ?JrvаO#3) "&D0A G<{D +f'.]GE'>T4*@q0_(сEm̻7ݙui5^tJMUK8ɰg5%_/\c-U&RӴFLwVwOI$PH_2#g6/C+M,cp\XƁ_f]smYZ*=$|OmkZ]ne-]L'ʌCq,vQ%)2x-xj#/DC!*Aš !nX65)9 p#F bŐB4dGW]\` wwU@CuAZtҝ٫EPl!T*JaKns Ku;d`t*ԟKJ$V./Z BC!>NrȞ5/׭(ia>}Jt'|ik~ϟ>}LY2dʎQ2-QAEiҜ{ >#2tI޺&)DF5#|V3wBnd}D#nd%8|okspHj V5 dXy4#%T5M '`r[8#L)C:]^\DWZ& cĪKYJ%=oOAdq4Q[a7j֓6Ϛ5"F^UV"KT-@V`g4 N31].7e%9,kĕok=s/Q LV12CU aGmEj-P%`6ud!4y":FU3X" ~W?dHRk[&r]9#VҺvHGbhyM˒0/Id1.tN$km,6BpRG2{@Ԧ o&%0}wq)JN$FI[-H)\9i6;B;GVW$ن:{'Koו…'$6j1%0^Iza,"f,J2s(L 12xӕ 6<-6aYsY`+[p4gԃ%w0W 9Y| LՑVZWe w?U@CuAYtΞTNBVR>|UR¦+4|ϩ*}MJiiB"/>UJUԾWϟWRnU=I24}JWϟ?{IrJiSJQ"DPy?MF$oad< gAxٔhg-kRGBn.uX A?r49a`1> 8%Ɛ"Rs5iZA.ZEIna$P'Iv.EV.R( &*sHАFl+6 m$Mbh^VԹ(Aud$ۇ9,q5`Y~ k[V!tMINWN/ A$mtWv]v2HgkO,6˚ uG`cPnc@ <I~H[Cgj{inFsꦖI5/pe~O`{+DSx򣷪Fa Jr;;@9=h%Bdjh̷\{*rk:A| k"\娐t챫LZrT7k. o֍Iw{UWƔDVivl֕V6=IiX_/Ũ0$1; mAh FZHIɖ$dHQpUbZ&gc%\ijK- jؚҴuGA>Q]rpS>w*,)SVKuqkmV͵I_ w!,U@CuQZ|=2gO>" kV*n+WC3s p>ϩ?TgLZi*~#vOB[XTKn_}Y4ʕ%U4>Q"DU&)>x$A7<0/7; Nl'm 8M&6h4%m;};/ĿG 06r,s y,kՏ4maOe,?y*?Yԭ|}N'N׼)$27BpUd;WƦ曾5м0DdT1{U;kUEkֵmS.LRa/#bDJV x! x^B0r(c;U55iuX Ds/tA\mp QsT:PHNzuw;IdP^ 7M gRk.lqRࢊ*hN[~?C>9wvhS "5l`ֵ"IMm\ED,@ @Y#DȘRC&miJkl7MDb*d0R ;2o6&4/)rnr=61ϡiicI4 e wtU@CuAZ~UΩ>|4>SoҺ-7}ZuhTA~W} 7/JZTR} *ի>J#%Ot3}d*iW2gI_B|W_>YdtY2dm#Lɖp/HP(n8g?RY%vk se)u < Ni,'P d@c*bgMVAslTⱀ;M^ vA/i5fTѴ7(Tڙ䣉58Z-33P9lBfI X]5u_f^7`ļlK $eꓤр1Wa\z< 3.쩅73E4Sl¸n )El i(tڤ1.;GE&%)Nwbņii.۷>W񘢚GntOJi1!*9E WulǺ\)4Ej!hsfr9O*Aτ:0ےi-Nۺ12 <Pq;Mce~+`^ 㐶7{:z0ކ$W1"5Y^[v [ݯx`Lf_Em>c@rUBxXj6=}l`9w3usIv6s _j6sx]Di V64iSZDzMRn\'zS=,2R&9IJ3)K.fYl T+{7 wӡU@CuZ|4h :sm(kXO@ϡ?tkJ _R|XOJOܾIk~EǰRg|ԟ>~kSjP)} 'П>|Q"DR8U'm$'ƈ e/;n7ǩj Fb.u8+$CZBS$%ăܵA ,h$0 w U@CuZvgΝ-(IrOtL/OCsM3/} K/ު~:N>Jji>] -u/BJ̭ӕOkCY'П>a>*Uk}M*5ߩ|7aAY2d66Z3[~8>p';#: X 2Vf 3iJE.X0;d+{"y XB$L\^4x4e|ҩjwX`eE'+bwErvC!M CA+f6c"6lfblA9zxKM:o刞\ [>h.RtЉ`l#nR/eew4Eps}4㼌1x$D٬*;IN5/_3?{>x|ZJ2xF RG$jԤSXAf o;9Kv{X xd`S.G#4ZxW9G1$.vMvSQsX0 8$] Ū'̊A0ұkI)ŗT WIVR˂rrSq%ޛPR;kP#6I"jqHIurGRF ~c.ԡKdJ.!$UiCI7׏']CKd̶wkVɶkd`Tq:A ;&\y3` \ (Uڠ΂KZ72q:6 ՎHQ D3{`n`wtۖٴmkƍkZֶͭ[]nEŖ$RQxRw7MZMh&fȺ+Pk줂˓ L2d / @#ee]Xdi~sr9]Kň *z\7p'2ND]`F[SʖQ齧[4> bf%䶊mLr`A`F & weU@CuZs]#{`j5W{aJ/_>}JgРIh/%~'էBRO}IO#sMMw|gN_K ORZii@"eҾ(O>zY2d'!2E!wr*&Bc\{oZpUljPDVI4r=lk5ޤJT,8d !b?܀_ eOW66[\xɀ$!^rcUr)SD:*dcpҏMbK5i R$|@'C%QiVA<ޞaEkp7 =X\]tb_%$KD<)FF$j6hh Tz7$3it}JyZG(ĕéR0 `q)tm,%xj'ءa9_ ĉk [̴(Q^ -]bǯ11C${a N;IӃz6V`[]x"R:Qָp mNF٤`<xL*t滥O{d>W} zW]/~UȾ'֤*q^Ii?LiϩR}Y4R}M2UV~d>}Q"DK1+W((r!i 2T6\5Z,K1|r lBja' 3j hvՋ֒m#)'Ju"_fYna Kx.gV (mELH⨵mzҴ̈́ĵhIFՉ#p⤠dثks!nܿOB>1떫̳^+vd`:h"}3((\PHtZQ +` YrlQ$9C;ӱM0/#s;&Kl ,ѷ8yl~݉_4ֵu76k۪R~"E;/bߏR*'gK~UX9heXk.sn)BtmL6SvաoqH`6Edalu=Rص1-vCgL]2 2q %MĿKKoOU3ibb![ c)LSJaU&L]-,܊p9 ZW3rH)K@ڔyS>_MaKân t=Z2먉TWmLk\AmɀƪnL.Dz9$ $}!pߏ&TR+Jf4nlYLa - w8U@CuAZEmk{]Sۄ_9 9niRuM*P|d|,'{^3O>0VFã% JD˪T=-wdӭ]SO}IXPR~PQ"D5<' Lԙ ղ`,̻#,i`&3Y΃219ʛ/RiIx14t48S0T˓, u%*u eJʉ8 l^M"E#)N0XkF6VS`5఩YCeDy@A× j|$Y t6Z#wK1dZXClfY}N%id/~ V5ua0@`tXƱXE#B%*i};6/N0d+;*J1MFgmDh]L')kַV܍|]%^1nYۂ:+U1‘W:ԄTmْC9$:Þ8 \M8d-ng%ˏk֛"вIa)Nvr8;hȆ7CR||p[8wFaplF>$bٌwJf26oAq"XQ=95Ds^jQb0\ŅVYIJn C47Ɓgcm11R|) 96Ʃjl܎9sgA!όe&9|u߬κAfK*2Q(7SP60Rf״ w|U@CuYWms`OT4?KZPtdߺRJ` /R} 4gR5/K slQ^O~XUSa2WJϩW#J'ˡVg˟SJQ"DK)X 4z)۬T4 qLG ]84n?a5kXAh[[7cKG[m&^"8 g4<5AP20,se;#Z+`-꠹|^5cc!6c}jm05 tN[ZƓf䥬v5'aqCT!mBf孫ۮ fKmpݴeMLS3qQDx=PdeVUi b-\gFdSj H=TR"66*tZΜFxCeAMǫrGٿ6j媯[qXZֱZk)Q'Ѽ3/P?9'xh a=ގ֞]VmkTZ/d((Cm88:p2Zg il֌66#&jIy P[PѠ 8b[Yg W|k[gFѹcShr} 5-5Fu2mJxJ:,zLUZаY6SVZ֕b`d0]80G,b*1J%Fssf+?-vl w.U@CuAY:gΟ)% UB|JaSZok>~FXiafmTBKa(OR}IԡVZt2e/BE+{gԟRZ*;ᩤO?r2?RQ"Dѓ59ܙ,RBPQci/Y59ӗiڷ6m:uUA} j/X+m]}k?74;+LMȖeMcl״"d`MiEl_ `H?eHٜNdTVlZFT25sYęQLqԉ^f(F155'@Mns`f1LY$F|[n+W@\a w #nR5@aJ !`=ִ'l*J,שG @swlftV٢sr)&Hnٹ~4C |CEQYbеyS"$JgYLJJ,nKc|*ؼ/Qz5ոcl֓9-4k5U<呀D7dŕYOm< MKݺi2BCV5$E2̀x9 WNiYt <;S!ِ.IKLwfFT0$`rM+߄"J| eeVg}z5[6sH7LsFYPpsH*4 J|6N ]/5D[xvT6:66R#k*] wU@CuZ|󱮩%'Q9ژO֭Zo_>|K_|3!?K 2XULRe/l~S{MJ} gԟvdSSY'О|jOzD 4ʾ}Q"Dmq#Gt,Ǿ AucDıE), nZ4kkt6S~Hȱ+;^$r &iMZJs!ԭt)̨xݴm 2qjswE>"P՝țW Iv k`=rw^[_'`,2csBޝLep-> tᆛaF;MiBh#i!xAy<1p ">72!kva tR jc105LQr s2 <3OWI?vJw4]fLi#.pOc5ɳq;yu*<%Sw8XE8:-#u1OR Ayvô@,쵮է զ5'2e װ۾\ʸ%9]\6!W4E\ՊIΔf2hؤDm'2Iⶶ(Th:23X/CU~^:vwK; >L&p !tteWvmr] J fM ("EbjC51XmEHZ wU@CuZ{ZҽZdTpsZi{N'aVҘP}M'ϡW*3OL54~*ϟ>}]++i_T++}Zʩ?K 'ϩ+DVZniJQ"DMdz+ {' v.xZ8,6s65W\8>MJkʞb5g\Ned{ 4m{†̈́V*U*Z6 Ę.ؕUs^.y!&c1oFny@ZtǒSnYEYpTJ _!ke 3j2nd)uR[7*T(sbs%MI,NnDi;)32'wjoBMk[#J֩L* A:=Mv=`NyC.{iF+8+@eQ`XAZБ[d-!< }"LLFեˤz4ͬC;Zn sÀ֓F9) wh.Yġu#u ;ZAcܳ*I#[dI;(#p_Ru!"fǹs-ʂIRIjklD{$9"ݝ6!ۡxu [WdIrjђ4)ؤ}Scqv5K%;R2V ‘ lu9j*MkUVepK\r~a剁_eBƝ%;YE(4S wU@CuAZ|Dk~Ϟ֮RМIsZujP/K *R4CRPJ[jaRY4R£n'aSSIUhPS҅IOV(ʟ} *kY2d:.WZ>zJ (qR8]1k UX>N540{TjEyLa'y(P.$ B3bU~d lF9swB!wU5-M"K΁ŋl`a9!a8`TBuEm7ĵrې.-GK⫺<|P -A'@˳Nu e\p#  5Ȋ}T`?麁xȦ*"fcJ'RqxXB56ekZw^/&^4vaWQFl.ϱHMҜzYE{ oaly=s=y Y:%nD[J ޅ˜fZ[# ̪΍zٱBK҃h. =~ֵk1CgXVNƑVhNGQ CxZʖ1m=~9]B[2t fZ2 wU@CuAY^Y¦L84)ϝ9;:i_B|'ϩWJT(O?J '> XR)LbMNtzgk>}M,*ϟ|2XTܾ}Y*UЫLQ"D49kɣīZϠFJ@Ekn%^JkZƱNjo5w#t m5!Zdې ߺ|efnZɥ=D?0&ʸyq3|䡰o~gFM"ryJܘqk(5w^Z]EQ'쿞;6JYGДJYX6[Sb4f&bm<,T@oeWN6-9b̅ת)lNT+ɒgR ~ACo\?u:vd;$ 9kWr"ְvnX-M^ZS.#b,2ɻ2avEigSM2Sz53yqS8UB[J JD–؎2% X@d O\3uEȬB( peN 4]uO슫4LZ+kjVǁH歘/cB|t鐮a.ɳ9$6D, IRZi0ۃ 9u*Igq%v# jx2wQ\9T{9knIu݁R5V))20ڃkX{iV}Sb>ԣq\y K b[/R5Zp/Vƶtqgċc mC h~*R<+fle-VHWʒFCs`fH;;g)j..Ql6d|p#`_))&tWNqb_ڲvFt \CS֔e|1DRf/C+#\/`R3`Z*CL0"3{ g0IoE㣀MՏ6Dَ1)i)~Ijюђݐ-К Mp gq^kHp#OFmq\t0ty O?)7c@w OӦ–l'ӛwL9/rODA6Z@#a#+RԅYmơWAYy[qװcfqTu >@/~ԒD iDbbLhF'V@l`5v"IM1Jt\.짨0l8Si/?gMWgsP6ĆZ@j wcU@CuZt *֍7Ε>S]9=YXJ>}M-'ϟ} 'ПRuYgo_ 6ҿz%}JfmJzTR} joփ^ZWԡXQ"D>.iF3T7菂++;MASFIKccrn\҅HR\p;WJUt6R-SeIj=\ yQfMm@`;آjPlJ~9h>*8$]T-l$'c = 0i]`i.syIkȒIͩ)[{ Lc78Dh>$z _7z !JXZ { FuM=mU~ AsB]ލe.vu[VkZ%D$X ֔ ^tNk\!ȷr/lX"EZ)Ӧ:mh7RB{6e4M`r\3·c3/baweUbfwؘIܦ[f^kр$IlWciwyto=kJwkI4ڪK/lqfV9*=b0n"v )"N ڱ4h鑠+N]Gur`f "AX9[rIvH5TZX)+eM+_\TÜL +~H-|_|igJeɣOQAxĖlKgU0rmc w`U@CuZ~-TkL+_:t;%+*kj+թZ*U9uM0>{PzM(O>+%WϟjP?r3P} ji?s tHY2dU/F,Clւ$WPUq|-(a=RXcf.d[&;ldS䈳qg4l ~`=l.8qĄdL)be$#2[Q6J԰S`INmk*)՘ 9vy#LVO)qտ&%2sfZ0RҙN VקM[0WfEuq971Auh$kD4">[\!0pXv)M%s1/Zp$1i,Q0%F4Ǿh+Ґ7T2sGV.[{' HDg;R"yVT{֬lX6:Qύ$`9m?Fm&lm.'$ H`LY7$$YCTڰG$Va^iJI B1e5wݍQZqG# I`Ʃ6Blmr M;$K&$,kQ6h[6a.MWwt\AacJPW&x1MBe((%c`X wSU@CuAZ|:{]M7ϞJ綫sZoUҿKZo?L/~2XO?K9'ϩ>|N>6ST5g_JL'u|겪O?K9gv|i_BQ"D@-$;k8| s%ܙYwYR?i!FQLp5pIsTl,E\2QTV;xO^=)G{"crF積y;ŊZ\&r]05 gF&kB)Ac2n%4]"qn3ec| z4!x5A9CX:Z 9ghcP~0rv{.L'O AvV-.;H-iHƵ][Ú C.ۼ؆*,r ]JJY1+tÒNxFs^6Ą#A 03W L2>m?D!E}QzX:[lF YNMf8mP3&Ev"Rg#*YlMԡ۝ yRV~yIK4fԭԘOۻrS1}D.xV@bb ^Ko]rڭ`%Z bZ2JC1WP(Q˘V0Mhk\TtO]AL!SXm`ڐB  m.졲l a$xo]ꏜSK56%Rie@mY E@U.pD~Fa-NwmY4gAʑնm7iA m wU@CuAZ|Wʝ>|sЕ&tΟ)ZZ*v5{ S0*{ g¦jQֈBc~~Y3ܾ~՟tܾ~,*OҚ_Sra6h wЁVO6mXMpX'),!,#D:m_~8I>-1D!},Wc^rRbĄ1.qa%\xcd23|"ĽƯd5fHJNUd]oy/`|VȰAi][Ouo "'s`1g=7I- V$9рGM~\\J>dR'C&?jzilbhPTܷfĈ#DІ!<-ҒZ޴wI B.ICGf"65.wj@Ďr LCFBUYu@Ӥ@KD֘½@XFPѩ\@$Kmtmvj;TOe)1N҆Gÿz*4EHfCaw'&@+* }Qꦨ%6b'/)tG7"}r)c>g̮s i%rsBGTCuyVO]-W-#ՆAB«LiK۹0>a:"d ]CMZxvʖRA͈sw׆[ؑ!G 6<&9=I"F׿:)k RY%\Ur.m w yU@CuAZsmҜ-l!TSOaK]?K|IjBԟJtV,54Ws XO>EtFh(PCSJW_R}Y'¤jiJTSSLQ"D݊v՘[VF%2&72^CLhLYQ2 SK̸2ݬebJ+oV#D*Qܞn*q\NۏB4lb-S G/ fNM:RT{Zڵ,ݵzkl\\1lG4&|c:WEhjU܉ĒcUrP$!i1VP w6U@CuYNkWVFNk|*k| [ܾ%-LIkR-/VҲ/>+rꟌ/ׄ*O}JRJϩ>}Q"D͌Wd!|pvd{z| 'PYiC PjW)1+fqSL)+BLGo95uZ]:cବ-sqmkPmT4-hZ`-3E4ev' `ͤ ]ۺr,566N U+mp,vmh.i0a n^76Z=X1kX:Nhȝ4nF$oap8kfWKm VROi7:lu6!Q'z39HJfn|Ǯ fLK Eٲ +l0!2یCFã!4hAg m6.R )` NHFkYiSfSg`U&Yp(Aug!海P5PgnRQ|Tֱ8vQ? d iBx!ڻe |b譺Wk\cDϖw9VkYk7!fU5$kkM]q ~\yo+q]ګj%V4-k2`\C tE>DsE@#*9pn0d664uVEt EJDzw!Tpd:XK mymuo2 wdT@CuAZ|o=RgNҾT—5)^6ϒֆWi=Ju 2ϩ,AMTJXi` Vv4V'[eNWAITKY(i%U24?LQ"DB|[ٖF .;gm K&`֕Y D۠ȧ0']ĝK!66b3vd9l(MY08T\~}& 3;릝<^j$,潶+wD4u%+̓K ׾!ʅ PXEVRBm 2Y/2N*m&D7<2{ycAӨGڎEc$-\NKݲOmgF~7`_a8h]ʤXѬb5j6diNdPM"9P?D_ ;knӵXRv$lF#`S2.Xfo5ʯz" - ^:i̜5kZE!ƙƻ0Iw!.& ɝUB!Ws;f9 뎴mP|)q>#41 a<1 ӭD9ψɖMjF;jVOcVHI^i;R{ I@7s&vholUwfR㶭jFT Т˄DFD[u2zla0ƞd$UC`%M' wU@CuAZ~EO>1>So>*TuW}Z'Գ~Cs ܿTԟR} P+J%ϟB2Z*P|+I_>}d_?s dҾHQ"D2Hv e=Cn p(`1$c-DSH[R5fZXFuv5}4͢|>ww:5Jkֵ#nE$r0"V~1  g$Ep"aU]BY&ͦ(#\R@kr1My5?! h ̤6 85:nM.3yAYGX& sSIqY92F揄 <֮g[*{]Ec15zReLp.X-GX&GKHqD x^X<[YsG%kZ0c&/@A9,񡁕 b OwILxUO58\NL[o lFT\qMk7o̳bwp P¤m 9m,/IF@U r򄣌Q3lw?.Y?ĮZ k&AVZ6tqdt֩V#vpgK6DSUfV h\ ^0qcx1bd'~#H9Ήñj4v\k}uc7{y 0۱HyTbGwlT8 ٧m⵶yW1= wPGU@CuAZ|u4hL4>sQ$(ko (O@{Y?tӗԟVJ"JikWϡKI*OV%~EzЗg|Ԟ~ a?kIP%} 'ϟB|Y2d;z=?85l(ܡ18Q=o8"F5دf0iA0yIar2>"s)VL|5e |*,Or&o< 5ha9EpT\mz`s=M8xڗdFj`WB[Z*׻q2 Ȱ~u}. e 3JF۷e0P5-TM]}ڵ`:'IҲRH)y{gvx S {K ^A m;t'v5l}چٮ!Ȣ쪽ڠ) ۱Nݠjwv)9SVt/?mn5ٮ^K+" d۞C;H";gOˢR);swH,m$7*h#KZ@7iF1uOsGZd}lLm+莑X'kP [8]Z:H! wU@CuAZ N-'/rgԞ}IL?[j_CsI/>'u}ZWҾXP>\-hO>#[2Ui+҅Jt>Ծ*oh5ߩ7Y2dꜩ+ۑcy%1C\8 Q';Ew7>@4.*rؙ-THIINsJۤ')Y8t+##:- Z%sZEZSw0|nQ[mKe[Z28-`m cfl,5kՔPL(ST Dw8pUϐ7  >b}ۧPdjy]j%r葜XIM%lF`KoUSt.B>9f)-E kꏟ峙 p j'̐2Su, _!LF @$Gf SGiiwh Um%]0^3Îֱ;8#y+&k zgC b SfD&慭lfbHnl`N,Drx|5B|6ҋ -YV.0Ep'e sm=@qAƉ䈊-Q'Xv T}: cA|cԜ&N@L(m36Z!|@U5L/zr:6;OX;;=HKWushUi~v^K6;v*v*mN2| m}eJ]Hgd V9|Ç w_U@CuAZv8Ji|wm O=E4TաKI4?KMm*Ү:Ma>~U>3ISJS:tB*ΗB~uxTCKM+ԝB|KФ ,IlKY2dT$5uoqA3`s+!Lp%"e SB Jj`Б($}ѓx 7AQUiFx1MVP0^g;um¶1\^OKsLquQ#Mﵴrֵ^cr<^#`codJv>U>1o1K'#1:XLSC61-⻵F U#nXݨaMd+7;"|"O[i&Ȃl#f ,> PD=lOdRH‰ Z6C+.,x1T\޳>˾wQ~뀧!WFTUmivmvhzSօd~UEY "Z|D@JR;v9)eBQJF~? Nd յ(8?Hѱ?$uhPF%|6-$Bϔm@-7tkq=/coаF%Љ.y-9kb1U-PNo*6x8vM&??ރ:H2  )UYޞɌ[H/G x y|A:<%@rԇ6z)k T5$2X wU@CuAZ{d#aLuW||S5/?LРz[O>~-(PB(JҾ~NaB#*tMwOK gI_aKϡG{i¦ϩ>|5/ҾY2dmNЬ)Umʼn?.z'a2x5X #dmNQ%d]e>wdpej܌r isHeĦ.caE1I^B3%2q⋄ZpK8eK> ɭRlR]g!C.@vϷ| `[$FMՒQiYsf7Hh.CgpN4@PQC |/VfF_LaP}#fG=vcѽ͈ S -`\anW5h-Qҷ6wv8%CnFi mSZb!r wU@CuAZt52UӥzeOb!үW>}94W} Zi]PNy]T>.gqaKRI~OR} 4M3O~:_>~Y2dZe!%˱>ȃDB; ]#,3Ik5kQsP# >XWwՍ6%?EBBYv~Z"YVtR4k#r S [9|UrqA"aF%vsFv 0 6+DԚ̚]X å(j$MPXy%(/WDGR:oJڴEmf@|洓g~V)@?1B7{ _cGC0=J1UUb-^z(3,V݌Q;]Y&BO ac.Y 6ͻJ5L'FbFF`NM3 1vd3+Iy- !+t봭mANF506/Vđ ~heuoo?oiT]B"qn4&+kj5O0%bXgY@Pj(#HnŒ* i-5ALgJ1d0\يO5:7 5>13iWU5kl+0]8b(Ѿ) ,}sC.Hz<ϼJ֮4LXJ׀ `:*UVÓ%/CJWԿlUA-t_N}]RJd|Q"Dk28K+N'KvzJ~-SW6 ![b2z0OlMkgir(`is\lH55Fj.䐖wGnu46/}ɚi@Gւ9-Qַ157^Z:9H( "("Udj)DD nݩq 5y_}dofcQmf]Yq(p‹8fsT83 }[؍O .l dKW6&yt46\ƽTL!-< ܱ-/_6B7Oz!EՂِf6\<8EP|\Wws (5H6SNӶE13 նg8Uvݺ=햱2SmCb"/!*x w5E"(%8ZkܰhO)i j+7ؼzt3_ L`pU@"RfDH81BuBAۭ%"76 ~?mۻwT((|klֵ|kZր&;Whl?>cb>-(w 2}E5dkZkZSR././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574016271.0 mutagen-1.45.1/tests/data/silence-44-s.eac30000644000175000017500000025503600000000000020423 0ustar00lazkalazka00000000000000 wt 6DSO>uY_JP9XikS 30LI_s]L4 +iCKJaBZgʐԡCT>ZiLԫ{ *j}J]{ST$v $5{<w͵5u;=|kO澳ZpU(gi=7$|k[aHnbj`SNၿMP6 5jʻeG!K1Me&ty*wK:S @(vtҽ s䖗kYV#)P݂t rhlȄΝh\~+zak&=dR~,>Ūyc:"em@^F:gwMB' ElC^VH2ċ٤ Mj.OomEx$.{Gn,K+iq]X\_ v\]Th[ےO޽E]qTQZb}Jp>G}hOQ ST}Iwa=޽2W7\]M@ݭtQwqZ㮼vW)|袙pVM `9Uj_\0%wo3)RЯڑp7}u#^RyPHJm2iu['59:&9Ms #T =|iĴk7q$I,&8MS3G%+>gҀ?{z6ڹV9k[qX d3D4k90h4"A';-kTuvZՔ܎<(-azGz_T|_F? B黢PddDvi0[j]z f& wt 5ṰSi]uI,4& '{ZjП?zja>}I霾´I)|ɜ~ZjUd_B w/Qϩ>|ϫ>}M3*%~%> 'F9 mw-GI!ft~f8WpO2J,m٩wl(jY)DZLNV5[' t75#s)1!-&dFL`Ū"i*ЪMkcix"k\6mӢ<{pިS#-ck_lO:AN -Ho$r#irnSOXEW4EZrӌKUcB@;&S 1]_$ OtefE8ҿzTqݎ ֱj6bk:yK^hvm۠,4Fٙ4,}I'_C%EsM]F鷖Sǧr;qm\zkZַ,iZ5,aJBu s+ZH(wq4-6 0,bGBLU4$V i)_ldg 7p,2Vk:oSm3\1 dKQ$X2߹0% o.u#u yRTܹdJZĀ݄E/X"7 @vu/D=ikLN7ZT ٤"Ǡ'1s.]62 S[9pڅTtZ1c Чb! _mb)}(W2hnoψ KB{ FZcH(=dde'O8ҰJ&nK1yd,,iܮ."ʝ4S'-TSڥJb |5Cbֳ FڽIwkT¥hڮH^SZP^6mWwa?h(K7zu6immEH^UH'RƍSV91 PkRB[]c`P=Y2kZK4/- A{$F"ٙ-fmH\{~hgW^f[SK@t5 w wt 5桱Sԝ=k|w>"ZeƵV} sU +} WΡWSZZS e4} 4ҟ(PZ{KBtB~niB|SϟBT+OVһ]5 E2;2q*k()DZJ @K(#K{IZ@ܙ*K2bX XXs#ANn 2.3cA_B cnGX Q/eyKג [ –2 Ӄi4f%DL@iv:Rm`.܏M;Ly Z2Eʵ27Y ! %ّnM{ar( م^ c>l==0"BmRBmu25ʳCz%66rR3Nc۾($dmr{59`\09r1ȱbK҃w;hkiUeU8f;-7&0E0A8uѯ"oT}/f %teڵNbZO9䬈|'hd3?8M7D51=>ʴa75@>=1Ti6vu~isd[VYB+ z5 ZO.wѐəJg,{ {8dО )[%jl•1E]+ӵ/x/dQ0x{[= :،(-4jIni)YU|g-$` wt 5Mz%4ʕU[W|0ПCSIgԫR|ʟ[JWOҽmhצϟ*M/] *jiZГR %I_N|+*|7RZZ.NFgPOޠJf]%yfrvԫÈqj/ ƥBB{>SD DS^Lď:ikIǏGv~ SvWqv&r;WZRXS{%1Xb);ևm]YZx4] &ƲyqS8U"0PL2'u;Ͱs\.BspunvDzvua/UQwp l6mCnrRm&,i?(O>*ê KVDEuTnaZVlɵvX-c,b> @ƸmDë́^[ gQkيcX:vWω O'#IcvZj> eu;nntayen2-6րC wt 5S|򥵳\%Nj0RN'ϩ*Φ*թ=5,*O?JY_QOx:${7I{ĒLEwsJgrL>rK/R@H9NKZMl%t#d GZMIWScOHN$\KT aL%Iٺ\Y m!C&ث#h"5DGVbiG5E^FH 7Ex2Њpt!m] ,:l:\@T-ŦU[NޑIKos_\ {I4nܧ)dQc* NuKbڷG!^':Wq1WC]5Ԉ;.b6w!Q~P h\/M‰$%~@ < Caqک,#FAnmZ3⁐Fٰcf8?>lSm3˼I*!VtOAE5*КW9R9]FY41"mzjߛ.74gѵmx1#1uRE'^'!ofnj\(Ў5(ޢ|!@ vJ OC#YKu$ӱHI`VW\JXȭ wt 5ơSaatלϝi}:gN_}M,50>]:4|-(Oԟ2UO! 'ϕCJ>(T?|j_WT/ -*'rda% ÅT oǀJz 5SI_%BuF zƊgsArxe{?.Ciqsy-$TT6@y"D*D"hZir%ƓnooB'w knFSŶ'cTJOq. aLɆ{ڕ4xM#Fd I<¦2 $5*=X]ax7Zc֗)y<˶);jJkZ%t#t)l:r/DYXҐ'0M'i?zkAnPfm*NV7P9C.R{xd+.*Gk%D X -(^V8E9k^qA<9^^_ݤĬg; 8~9QO}.=Iqhs[yٸCCip_*͌y靚젏<.o@gpohʴOxJT*!Z?|FըIH Ɨ[cp4n %\E=HV w%7G.\U 6 %w'9`tE*SÔĄd (. |\l̓U}]-/\Zu֫-gtz10&8+ 0̓EMCu wt 5ᡸSNaj[[aW|ӥ7M7ΡCs %OWSM,*ԩ>+iS +oB-*PB|i*O>~,'ԫRWҾJ„ϝU\ܾ3]n7{"omB~+]|WtF+I 86j0-iԴr${ 2I֘fwdp[)2v̨6h7ĴkZȐ3iVŶY+.CK9D,R09k! I){pUA+@(!" hܦc@!meh'iZ 1UsNcbT:6Ё+WF3\[}`JӿXl*{%&W- 0Iqlײ LJ#1dvLO4Z#YG~4l ۖTvkBO .pu0$ 1|xdGTu{&c4rdNiZFPppZֲh@ {Ipl?ՍP>X~^#rJ%rsڑݗcb8pPbpіf+ e'nw8qI&l ݄pX6eI iҪ/%3/~'ba5Pf+dZ-,4lIE5~OJ;3ذcRuh/^sOY@a"iEZ.{YԬI"g%{ wt 5ᩘSO|ikmjܮ~gΒk t)|'}IO>}J'uY.d 2ʞr2f{]4.SJf_{aJ'IkcP~rc/F~f[{d4Fm#Z9R)tZJXL{*zrOTӳO}uv^#4s#[#ZUYoI)QI~b9S>#mF1#e-NKf;SԘ\KO OB q|:aRյkn2Y}ꍡ3"nZW5Y|Ɏ܍7Pf߰l1ʜȘ]8AWD< wt 5ơTҺTwS* 3>tkǵLi'~2ϟM70>J>T'ҾtΒLZOܾR4~'}M-gL`_r;ã3FtaIDͬ$?j6);=D9s ٍʚnx*i'uT* I['ljM9˚cԁټEKG*ƪ2$!53}5w suGtXȔ'CQO0]LvE6pZBYMqlZIr4ƅHOb=8ύK$X5ov6ުm 5H*] vޜ9~?%GKl]rՋIR@~!:i'jC_ |#~a@|.ې+i(V0Dn$/hf]vn[:4/:%&*vUMk˖VɎ"tC$Stv`DyZ`1kIf% ۻUƤ_\E; "~GNj x"T>EqSK[ [9#29Gqq\Af0+&42﫛n =woW&'n7 Znu:V>TbEog۶B.$aI}/FımŬ؅U0!Վz r@ے"Cn%Txm .kSrF9_3l0X3nalu wt 5SN]M/:a U*S [sU['Y#aV2ZP%Z(u|(nb etTD470}FliCK S>~FПCK]+>|`]9Q=&E((:S6}_$fN;Qݥ(H&o*nPғQTm'R's!)|IeaKRJ'r6vh\Ʀ k%RZTss! ߓԞiciÿ"7nխ&l|#ZƑbn4DH[,-ԬTOͺi%=P$gc& 1(\o˶JsnY6{gՉ*:W*Y(̨^ wnT!⪅PR|0#T522krU+ Zؚd#@RpF?w+d|$)S-ԓ9.n];$+qV7/"ؘiYrB0 u%ΕIĒm,4\!**٣zvIJ;a\FO.%?PT3u ĔH+rų46QV&t0Ȭ^MܜnpG8:&13RyQ'\DQ}Y+OyeA1`}TD@ j`IvƜY)h9s rmLc9mw)Vh<ϩJ4I_ ϟ>}ISjU+KJ'|TU'ПR'Ҿ|rOM+TҾ|OSJW' Մ_ ,!,/[2Um sjHcMζԄ ZficϘ}hSHu) ⴭU-"k$NIFE "pM907m=Ub*2lx]nn0$ d`G~ /IfXյ.c6 ]Y 6|a(\MX3cE_խiHr:RrrtKwơxZ]y&'o󢻷JciC=즿kZx7!fEsDnP֙ jh?~'VI/`qLOm-Cch9N}T֐ mE>Gx[MPgʎީu5*Pk^klh2X֎T*FuDzw&Mӫx:9IGֲ%ZLn z&yɭ0#2= (G*)Z}Ik>dSIS0B} 5/>'¥]Ew_;r+ϡT,2|'^J$'bW/&cA/MwbZGIRsqYP NKbw4w_wa<4l.YX$8)X׫ij >BJju+u_/_S` ӵ I 61FL1kBUT_wXfnBz(aRCT UٮkZeyNMѦe)0"w%+u_ uIН)'J~Oi}M+ӫB Β~ni~TҤTVY*P-'ϕR|3Iֆ!SLg:JҿJ*%|3^MCDi28zSj (K(aPweL-iL)fqH,Zen%ӗjc ?Ļk8"7sSgm݋̉ n8DWG_C'dbi/LCc@rUBG)kе+=}N`9w3usIv6s _j6sx]Di V64iSZDzM-M)C˞P k)pY~)XJI}UU6 wt 5fSΩKF3OӛoaG{]-5N4|԰} 5ܾ]*Q?z}W~*O[.=t>ϟ>0ܿKZWϝ*KO>^G Mlm-]$'8nV"w m&ǂwԹ2Mm3E,yc(Z;]r3kZl8j= pE-x>BlB66w32FV%bl;qa5IbjNĥWӻ٢q9\44*tCp B]$K3]L[2Ʋ7nQ5/\BYv(mZW,4lAn[gBcjvnsT_TMvc>pn-NfYz`{❗)d9iƔײN;X1<'JF;~]_ ]Y,nTӀ.gR6r/14 R?`^Ϟd oK$4N\l)2yPֲEK?^GkR)-;nx3x( qȫ26 1vZK ʉY 2]UgvN}UQ׭ 7mXְ.٤^QjZ<{"oش~v$gdPmu⢣d b5!vz @mȑu㹍o65lf]ϘN { ֺPtI!Iq -PC  *L6No wt 5S>tm7ϡBL:|滥aį} i9~Fj_~S1uI*PSIik}J*U6en{^ϡ>4?K 74R[oaQKϡs :@_+Fzrz߷G$dqU kFJQ#abm8^h70䍛" v57NWDDI1kh-Sii^hSYư/cn=QIJXa<9StpPdٟ͘Ȼ=͡s[YaŜtrnM-4鿖"zstd<*lk`JҋB'-FvTtuJ DڸMd9>WEFe"rVIk¤NV֣zG' Sċ Ј*Q5f0vڒ9#T6"ưlC o;9Kv{X xd`S.G#4ZxW9VK}@gjqiL}5':Ȏ"EZ*j|Ȭd +ֺĒ ..!VxqMk:wLBe%7Ms,a%V,nН?0]G]CRU$bg&}t'JFI;[[SRv<ːnr0m^Hib:rWc`٭c2 P/))*v>er?oqp<=Tw&%Sg"_L~: wt 5SSsS H+W]MuO=T7ɟİR2ZO> RdWRtiS iBV4s LEҟ:UOa>EDB֤֫*5>_.}M1XOL@^9)܎S8 !gsUUN!ƕST#b5sw3X9Nj5p@+=m|6qr=^ǖ k%1cZ]tLPmGGuN3?VCpRNZFd-i(Pdem 0F"m[#%)n''%?LJ8gcHFxHGlD;8 3[傎ތ}%][C6zf&Zv3M=eIW;*c+6=<e-dV0 ,a2ICmX\)GthѹIP s.ՎHQ D3{`n`wwۖ+jֲk[Z^kޭԞS<ĀJX*4/QnWfIX6D^fɦ`Zt3ɒ 0HHz'{ Nuwyae5#:T&Ԁ nJvZr.DPJGz̀ߧwLGtOv.#+)Kp(tt! _LnK3 ؿ&n^KlOfV IKtY VHw wt 5S'!SW)>{ Wԩİ?JV>\*OA~+>:4ʔ|+O|'Ujk~:rRXU} 'KJk.'aB~]7$&Q0yYh< PQC\Vkcm\ -S~1,h.{3 kIYp@B8. ^Flf6 ct6M"Y 쒦szٖ[AyB. 1zQS11-uk a1-Z$sYJ/[kb̄r?d[ t[Í  -t\G4`k^JA.M($:p{- H(\L\,Ԉ96(jqiOZwɂf16Xؓeaf nįeZPlZ Akb۪R~"E;/bߏR*'gK~UX9heXk.sp{-N6qR|'O*6-O|Wܾ$]Rwk jꞪ~O…Z]ƧAi>z!ZL"ٗbDvpB5>1lBq ;YyFCF>BӘ+RKͤbi3`hq?ҧUea S.LY rh$+O7,`ti2N5=FB@l5ÄHQH zm4Ӫ4V)mѥMUᶅgRfbM;Qc !\061m|%"(Ib+eYk) ӬQʾK^?}ߋNUwm0 US\:,]cX@,k)Ka)SJ\9|.p/ pY)SȔǒ䭳6k ')kַV܍|]%^1nYۂ:+U1‘WAbQ`RnUI"$3Cʬ9eӆB>}Xt"MvQ4qnwё nI:ضp;0<)  2H،|IŽ]/fEi\USf1Y{x}h.6Y 'tjV|^ >lf|@rYe'I+O1,G{|i,5J',@6;Sh 6#N\rHpsYI_%w3hlC3I%]-͈"6M) wt 5MOҾsm_*|*O9X5П: 3$җԟSe54|niK=2a~XOkbB|+¬J ~W}JT>] 'Ы>\W]8eLbW: :۬mMq9K8mKMAk6̵WSo 8Q,*Vդݑx N !u@[`n%n8+b5ZgdNcFpL21qe: KHg((Kƶؔ;sգ575i)hOR 6rQ( ]cZ"|~vCҥ7M@3lAi1S#+0PNd.DxL%; n#Qߊn?J3B m<}.;`qSY~;`n y;/;/?<j3lD5x!&ǵ5kv< Ya˩rA`l̄Q<4Ml3m8]f~>T7 M5$ ]wZRa?9'xh֞tL(`ҮW{ ʟ/{Y0K 4+o_B 4_?s FwiB|O> jПɓ)}JB)_>~o>*ԡQ M%T|锽Wf;"b[K%h~hV vO%:r;YwBG*F Ќ ߮]x~__asE`k >pX;8ZHJ+J76: ph[ `պs6yQZv5iڤ}^5Ɔ-B bw+MEA{Z U7B1/L֧+k$kBɑͬ7v>2&N,%!E>|#n+W.3xH M)bG0Y`F DHU*EG6f5WVG @swi琏Rf3B+RW6A ooޥ.bJ*4u6EkH8MPl<q[:D[[>v͝xK)kU %Jϩ>ϫR}]Sj%} xOU@]8mN$h7^H=h868Œ"Ձ\fmn^Aha?N$f8|j~j@/WIe$&>r6dw H^7m&hO);y `41oEyV6i.5TZ)Ck]bzE྆'WcK8 9k]uj؛>R 㼖Bv5N9 nȯR-f/1zNcSU:pMHA<G!xAy<1p ">72 &WFi2ٯH5LQr s2 <3OWI=vJw4]fLi#G$a=֣}QgvUy~K;R@ybڼ٥mZZaiio !^&$ciH+QX+;5k]O:AD˺a9(]ֽnrLW*wciro؄ \]n 9hAAHGDa'b=Rzɢ7(` Uxa,V&X50YmVa;yMF5#ml˒UVv2hXyAV&C%9".^H wt 5ѸSf~wJЫ"[Mt5?K 7N„[na>} W} 2eNiiV|_'_;Jʧ_?I]ПVUIXP>}M,'_$sMWX\Iex;Usz!"n%cG؎`2sƪ}" j_3?ow,#TvWA[&܇#e8ih2$,k(& %E\ץ eHI [ћP31䡔۳VG8Dsi28G!:2e"f&R : >-B9QlmzXd P%RAv[Q7V4N{@ ʓ;Ex7FWrܑ֦kT֩RA'R5iG ٢/(wq4-?%g8 ZfX%`m4-!< }"LLFեˤz4ͬC;Zn sW8ǣr518.Kq(û]HPT:˱5!ʥjw$Q-QdF$`XCDͱrZ-i|IRC٤"2^ pO،n] p м_:-+ĩi5 촊c$1}A N-MiǔhW5/7ϋ(HpX* F7!\2$pkU|wʕ&6KӫR zXUU 4} †zWS *ϡ җПp?{ JB'~*OB|gթFT+UV]4Vv}sEYtC)Cڡ5G ֒]@d ƦjlI*(-OmFP Xl]9H(Ang7dx ͈[U*ndkdw=t0XҪ jZC]Ɖ ڎ`E[:RQcB>DrL[Gfd1WP̫]Űb5H֮ } wt 5M%4aNw>|5ПCJi>}JWʡB} Wha=Y(Tׯ2—ԟTM0d] *jsԓ?sYiaV|K¦ϡRZe ^3mw74|s];bHCht ī}MkX)M\F9jeU62gjmivb3?_7-Jpd[Aotu lW;9zgJ ytd,'ɊQڦmb!Nii\rTIm;/砮'MFh:z%V X)G. 0idv4%];|.uNϸDeYlk.d.G>Ss-ʜ^y2S?R5m.GQnl'dA6jDZ ژWu`M5yiL<b g&ɅDhabAkS9M6['#Q#10.P11%+a4ԤO)m"[Xt&Oogxv%JX6 PʜxhVi[V2ksfԭSYSSU{tKLw wM!$pedMvkI 9u*Igq%v#Ngϫ* j_=h_QOx**dԟ7/k} gй]F҄Μr,'.zR/f}`:o^xc@h 0~UxXPoWPXR(NMMb{*GX_'+Sm8txj3Aa{ZD5Hխ+m$g@ZSJr4\ $\$]f;zMBӁz֦7D+όH0@1Pև#Ϝ"z̡*NnՅS|,Cs` ̑vvR];*]m<ٔl rF8W־SJSJM騮;29 %d쉃MV#.!kJ]8In{# tpXYwREe$iEa9 2eOJUua3J$ڷB}j`K]zH"lymN/ӱ:"jюђݐ-К Mp gszQkP$+Ov =n!`C %&L}~cکtR͝>q^:s{{΁~zx%}B FcH~Êઔ!D` 61;fV|6IȺێ6&#ج,-,Q{%[J#jfB5)= rJVl#X DbgK[ٽuzFÅ?f&t41$--Zh: wt 5ᡸSiW|hat)wϝU9ii>|+P>ϟ?#=4/z-(OVΟ+U5{m'ȿLWkҤTS|tU7/Ҿ -*("2Vfj\@Ep`"%ti*xک)pll`V˚PćbsTi v\I)UJtWO;U%"KdJl;9N6(F4rҧ#p"?ϻ}al%fdJ#2,G2/lfd!>[Md% 剃J COs:KZK&.FWt`7&JNQ1@͢pyYǀHj=g{/E뛽HR},-=#L:&2u]FY/!༴8.~Egn]V]sjܢkZմ#c9 ֔ ^tNk\!ȷr/lX"EZ)Ӧ:mvz(-OqR4 nKaV fe L3쪸L}La\H "Iخ53 z׸3T2֓i Ui6&^̩~G-l'DLHac[ ezb}nJFH6;&`i"6e]@[d4ڪ#twLBdbA Gbx_m$$ak #kֵQkmb>|]ר ĕ/k&$>42òhӊ̴s[* 'U | wt 5FAS.aj[ZgaXBӥ1|-].W{[Uei_VMj֩R˪n_ tB3iB|5I_,%OҾ|,'ЫRIџBMKQCSIng`d7/M-j@]ׅňm:Ax@ܪPPN:k5R +ϛa iRFĔnҞ 1 pCAIҗ2-~ HWhcV:T'mBgqHFDr)O RH3)uE3d.K6ֹӭưXe@gH-s{Ձ:tb c5o&0-G_mh5i>TD!:^B<o$|r޿)2(C񼖮)x2 G)#Z&٧$YVQ5k#Zd<}IϟBuYA 5/?r2Wa>{UR|Y=SԟSJ4(Ťumu`nd Vۓ+61 Zg$(:& .rrJD6؆ 4d<2QX77-?ax_Di8GY*ZYN" o;,U=֏0p"9m{8켔03XJ D{i v82n3ec| z4!x5A9CXMj󫪊Ԓ'jXr4Ơa$ +\0Op[n=Z\wqZґkZ5RHw6Ze5=R-}ALv#egֵO1 N+` }Ґ\n(c , r6zKAn,_Cu[&jCe4i2-Wm%&r<_)|]Ji5汱Bր #swnV*f/بLDCT)tRM+[UDAvsbұlKbep R}C G.c[oL0 q4 VYqQҥ;.vknFPcYapcn0twe eh[!$~"7T|r] /փH*kl.bd" UMqߥpD~Fa-NwmY4gAR]mviGQ汭% wt 6SʾTӟ>34tNϔLS93ZSP>~4R'~Bɝ%~0VtaR}M,7NJ]ѱs0BU7S]!>ݕiMX5k%%%) m&̗|$I.)h= š{b;6㒓$&He`s *rrxcd23|"ĽƯd5fHJNUd]oy/`|I3,&dcFsV"sȦ6YM5Rx6 B6>Ia:k"W1n{"=i1SK`sEj7V$FI |S,By[+%mZ˽iV\Dlj]v;rԁ^%IT?NwUtx,Gpd44,x.@5ۺ/qI1ܥla%$؇7}xi/U3bFL$"9$ _^SHv`%N&KfraUĺ* wt 5fSnm/&a ʟ*{Ziϒ\tSogKVϟV-*PJ'aT|r.4SDT'թBJPҿJϩ>~'ПSSM2WϟR`;R: qJĦD&CKiɐb+*&Z[AJibY~r ˱S %7Z(nO@E8.[RTǡMs6[1T0Drcs!C u͇B|B[f-YuRf0qt$$lI< !W-~Knrmᘆ̴]9b`]ү2r|PH?{?3'D6'[tI*4%)":  m y)w:CrO0`ۍ'%$5bf(̚9ІڼU INHL۾,5+ϋ@ _0ҁ"w)Xh25-5hZ5ZZg}~<}J+xe{n=ƽ:wjZf:kH֑LcVv$&Jy$Pl4. 3ēQ0n)+4x=-G4,)xԘxۃdR!UPtZT yCZr2cGYUrբqָˬTB.F>NM:RT{Zڵ,ݵAsp eW `^A^Wr'J*Uʥ@aX wt 5᩸MN_sZ4:s]թS]ϕJo'ϟ>^-4l ]dKZ7Im}J'Ε|g_[Н?Te'ϟ}V'iWt7.*|+T|*P>}I@_j:xOlv/O* -(qԟ4a*J1%l۠*dKQզGrz[0wQEà:1Rޗ>.sùDRF r4PIY;7i; LIݻ+C[s`iPFmvX& [p4PN53W sp溗^8f#dԐΒll֠t@DɦCt5a#5YЅxC[2r[hg,j7*{Ic͸G5c^)=)CAq4 #뚵1/<4Ul4RzEYs urTO#3n2 GgI454(HJ$ك&9#lhHe[(< [6e6vRePCiS ̡pj *ܦIijʡrA^<҅h>Cw[tָƉ!rֲnBia, tZlB#~\yo+Vfn]S^SHGٶE)kWP\C tE>DsE@#*9pnb0d664uVBXBxdT*P(cWuc8`@c-R4kkk kNtF6-#0m wt 5ɰS5|tʓ:vaJį|5/ҿ{IwOTi}Ib nWKb50} 4>g*r zO:2ZϡCL,2dBcc2 4ׅ O>~VTYZWP-'Ⱦ|5Iֆ4'ԡR]_J&JT%~j@5&^~v5AT {= 4D"$eijִН$fNB5sam.[kנS]ir)#"V~1  g$Ep"aU]BY&(#\R@krE*p^CbMOfB(3) H"`N NtKr9n5P}x54sH]LDɕJ74|%ϴ7v<qS1֩$ AD՚!X\XZN1"hB6!ۜteI;AƿSp1!w 1*I+p^7%4.a^ G{ gB~]س~PM:L[2 A`^:8m:}MTĻ83%ߛ]pRm)v͌4t Aum a`P m7CO.@2?qtp3;O(kHnahvٰ;[{4g,n#P9EޝΉSh$n[f+Q\ǀ1! wt 5FɰSөCFgН)!G{]4ΓoB}JԳ N*QSK]'Ҿ} ZOR}Z7++քu0>ϟ>\K ZOR+P>|^oG㦠؀q͔%>1F'8gM҇٤R}y&ۚU0yIar2>"s)VLu֑!(q`,C} N]甗\bƺ  '( YOLNgR4L ]RZHKi5RA*׻q2 Ȱ~u}. e 3JF۷e4bYM]}Uv3RؤI:VV\Cq//o!' +zjS:BoSfBqa~ln/^z~*r(d%z+ֳَ9݌rMg; )>v2a:ӌ\9sM#,FRlTΉRYc{xNMJ[р Ztap=[c0r`D4U8+!,{"T8jrgaOiq׈!-vƭvyYP59U5Wjd ۱Nݠjwv)9SVt/Yb)fK+JaΒRO9} 50>Ҿ~'…kB}I,թ%钫I^*T|74~70S{G]'M'ϡ -(@S"pr,o$6(sKJ0hE\[%J ))1d$9I8t+##:- Z%sZ:Zİclw.| nQ[mKe[Z28-`m cfl,5kՔPL($M2#1;|hf18"#S:Q+D@VKtD(b0[z'wp|=Nϧ5Lyl.J(M_T|-06D$ K+{cB &-mjQř”ZAڐؓ|B5;o$uN"JI^XfBّ/mɹk[Y;g,*Ol٬086X0k l ;mqѥ-9( T<]K3TaW OjN<6?z,/)ZN6ҨtmQf?q}I̯Bd{R1;#e R)"kѠuHsV@y)G)ch>#a$`%x']79%VgeTNVjY} 6'x Dwⶾ2ӥ.$uEa8֫k]pEW wt 5SSMsk{mm1|)įҦ ZOZkiV~utik ¬'*LPҟӤԔFiW.t¤Zi_>_V ie/LbX@2yF.2&lb%|0ILA*wAubV MPʜ3Kѱ)u#:QXI\3>&4nx* ҌcX0/dN[mkW WS$B|qTHlEmaט\6+tTž@ϑ/Ǡb52kNvSp۪,cn0}đ>E'崓R dA6Ftք]Rl2%,Q#K_ Jr$4!`&.BG^He ѵ9Yu`x1T\޳>˾wQ~뀧!WFTUmivmvhyS"jmm&NKT^e)A$ |#wj[m$(HEĤkok2Qjq~b`"I{qAСJtl 8Z0>H )Xd ha'>mC~2z%=%g85rv8Rq]4:2T@Fa9Ya6cWz޴Z2C#(^t,hUgzx –\rc-K8ǃ^t_Af p#Fܵ!ͪ#^kZUI }` wt 5ᡰS+!mk `{ߤ[ʟ0|g0>c}IIiB:4aBVs 'USk|Xe:JM[ ]V} j;SL4_.}Ia~'_]>z1 4Rv@@%Q7$3["ֵ6fO溿vY hF+2۴ڣJ(}၎WE-{7=u@i>Fs bD}z<}Lxr1R/rlJ슡Iu iUww>Jqn$4nRlVIEf͘I!MIȌ!`E' w(B+VSrYf5VNdqSGQiP+2HҕH< !fCinhW6^)Pk-aV<&DN4˩vÃ"FD\~DOR1'Kh&%1)u NN`]NQEJ)j>\3+ $vuhXDD$ nb]j@ Sk/7vC7m-hk"ޫTYKRQbfƑa.$ _ k dӴs,6SEj߬Pbucי1AA![I}F6",5OPصr5^*Tn"$ htn 00?; x?] ;PۑkZf[T֥uئ`J! wt 5ᡠSO9ɔ®+*{ n/ҽϝdJ(НRt7ꤩ's?|4 _} 4:L4R}ZOM,*i~FI?z]KWl9dvv5ˆHBaK$q|Y-}f j:} rLm.>N껸i)* 7Jn6!mcs0 [9|UrqA"aF%vsFv 0 6+DԚ̚ڏi].pJ~EkT)g! D' +Ƌkԡ3N@fv>5;J]o h?: 7*TnQV~ڭZ{ 1jdcl gXw{Ms$z1 kĩ-jcLŁdx Z755M!-Ge+h6q2 71n m1+/5>ImYH; wt 5M޿U k5޿:OO޽JaB%}'ϡ>FpV|'PR'-gԟ}IWҾe4RmJsꔾT74Ҿ lɐA5m^{1/į8,S(JJ$pۖkrqKVEdi:M9@^G{yP 4 䅭Q`s =D5ILo&Oirm_:16u>rRW6zH(.S$Z6rRI0n0$s H( DPEaR5!۷jF$$$qy_y:7S2(6cQmf]Y,#V!3@Mvu]㼣!.Zڅ.mEˉWH19t4 c^]TL"E<5|`[WAl3la")!'niqamсMUUV@&baiLTGD La& j|Y WaۊC1kI1 ӬƣP踚`YkJnJFF=H ^tXְĀXMyJZBZ/q|$Nm8H(Y"Ax9M4NRl%Whqdī/[r["S,#E\熸! !9-$$:sN \EA> p,, Wt,jb` wt 5M/>sj_*t$Ⱥt(5ư} +$ hOc~iiSK=,'_~'u|2RK޾|O.\WЫ>\;u^IvPNlVv6IGYIc\>p`T*!< ʒߦf!oj_=ȳsY$Tf4{,)1*MB&{Ѷ KiB|*ԟ>ZПXTCKJ7>k>~TQ*TR*j]OL.fVJ3?ɼaAkd9*)uil@@Wo_[TTbUu&P9!k  ^qSf;f1̓X(Inڽx)FU}\('RF_װr.5oca>#]5AH詡UFvmlw'j~uYyq}XQhб#-ut4 ~[et&*l>V+X({(Of=#͸H}~ V3FV>tX ߮sÛ9~z_0<4XhaW%Brn)g.-@Ã@zXA%o0jS 涱.*ڵ7GA_P:VS5Ulތ? jra#4%g-=ΈH_17&m^( M`\b.3{_R8h0R V mubɐwqa. ZТE:kclիU#€֗!BK@ι86ٷP-R0rxƬ36'lkk/ 3v-^Qjɽ[1JyH8jNA NXtPW=܃S9MԠEunG&ĐkZǼ={`Y wt 5MO:I-w:ӧN[|_S|j_Cz'օ J¦GI_BekwJ_ҦR0IO>ϡR} ST%}IxO@:5XfGuQH=k- ,rY(Դv!!&¥S@)uZA3͹ bba6;iA9}IwWFVX%sfSR˽az#q<6n  ,iWmJݰn[]bMzC$C6r`M`zYpvㆶܨF)\l!$6RgT@apMk1RWJ IKf`8oY9c5[3M[۰9~ؐ6:{*ujJ]¤`Wk~fX&(L&>ƽOk Q|H3y$ho>64F/Bd4$O6NJml*eQF3_B&Ʀ3TN#XvҦPGяPwE04Zf'ӵn=vY/F7 w dЮ~؅-IJl-DH{ aJlDq̍]`#:Ot35!&GE /72&2B"E-Z&Sc@pazQz 9$S ɡqq#2,.Fg 8|ۮn c` ^e6 M$+=B%A#q;d.] {v(r8dU IyѴK wt 5fɸS&|L4*}(U׃]jП?Jna>|i}Z… NЭ|iR|nU'_: jOIПB}IP>}M,',*$+Sr>iinF6Ӣ<@dcOpޥ&F,Yl Y]͗`)&W h^\\:Õ݄m& τ sZh|'/hTe-d k{)k 1җiCRWʫB~UM*PCJg՟F5sM ]؄o" Dn!`C0A!̂--iĄ-$hY nFP0݉dn3gzp*CLv݉nP n!v2mVS\2AOallv6B->נ%V"#rE۬:Ѯ/ EIe@Naʖ3KB9c.T+lOLVM"[jج0'-M]F;R4@|2<6 g/+{DԓZjXh-IjUH3nRj ;`oH J)K7ba+60i5RϰY|`{7h86ѧMZGHځrݝ$|"ZưV|sU?Z˟} WNQYTۅ :|T70O]P>gԕ?s*i_B|KiTϡR;_2fT92V#@1 ߓXzQ"Bl֤}pdl+]Vz}EWKT(.a?z8'gXMxL/6+w Q rhW)b'2Xt#r(oPwVǭo[Ja]MY֋+afF6!>JڊS JȄ7R(d% C%FM\˙c8ܘW>L˔hkq)D8xhW5>* m⺢ә8FMMwI]&к>Knq<,+=ڲ:ʗ Z;s2gm9xquw&eU^Ca{2i>RLp582$.x+3M`M7[=hgB;ðّ g>&V!ҥ-xV2s8?zOi* &f]$m(9aw!ɹM&؋ÂT|xOc.D&JN?]UCrO7lUk%dz֜KOS/'jW1,P5PEX. _]3ɻ9dۄY+maeo[=Y]ZT678SZ:r#S4IHp,'S\S/F wt 5&ṠSк4SB{й\_>vϫ*} ʟ>ҽIm_Qgl**/ԟ+0V e%ZЧR| NiN|+*|L7jZ8IeWSdB^ k?)Vt1i8獰{ɃbZ+VΉ' /%E Rt!] w͘9@#b6ee5yU٬m Taɂ{ #{RđkX3aF};?j CTbkJhф' R9C ~ݻ6{[L:䍪ᇎ6aPl2Ę;ƨ4-C.\t>"4Ɠw>?*ܽ1ϡ?"4/R+i5Vۗan9{jLWsҥ UStr7/R @KZ ]z!]OPïcl]*͑NsW*6ZN޴O2Y!?"l+EjCY>F`&'q%HxSϠ6)x1,g$,ĒZb-4#{i+.7_ZeO?R-gСB~w3O!|'ЗSK >IXP?|_WTd7/ -*`A@quUŎļ Zc)'vRCT40hIḳNBP>sU4>Нot8uD7(i) oL-ٖFkYG :s5̴#nʰ:ĸֵ\YhI29d(YngY$m1FY=uaǰ9؊ͶJsʛku-;&4G^ )_PPY=t8?o) OLs3.p%jeM*ś1Aʨ]i}=6{S wt 5SNaV]ӚaW|ӥ5=]M*ԡ.~O}Ii|+s JoVM+ϟB}Yʒ'ϟ>,5/>IW_>{Օ>WϝU\Wϩ?J'e·K2A 6r| 3¼8MܬX65C'iցz*9LP6?,YF1xH%cNJi/W i+ 7@xثǏc}#&4 Fm$wS-Z;AmmOM;O::BFCѤc{RE}dRa UN Z֡VH@ }ICI,<]p`oXe %frYfKˀwWRBzhJ3AQl w=s/"SSrG{9XOdG[LmE!ݜwz% $ `jwPVɔ%45`cH2(km/qutDذc'-ec| z4.'85Aˡrm \eX0.X@ 2GrꐃЯr` YV2(NZ٩kP0 8s˜e*z%\6Ҙ:Ge𒃫kT5Ge'L˖71 Rd(f, rAʋms%b\Y¢/N FQ)J|B\ڧ3iu(v/IHCX5dfԭԀ ( wt 5SiSB|'ӝ>A'֌-jLRt9 3Z~MhO~ԫ>'vdɜ0t.J+I_{+NK 'em@Eg ܋ H\g kwĊI3P[+!\RĆ5  Q`hG,⡍DFe!emH(5.p4 Үe֚UCJjp #rgŗ'+chsXGAVjbB֘u*W[̝b~X:%(!ڳXvo [f-{a6@xGx3ک!{?`]en^ 9» EVk]fB|VyZJhOk̫}$~.X Bxpۍ𫘩[0sb+%~C αИ \AJyD{cFd5Z$ "5VsZ4+Ċīcb6ƮV"40{Д'k`9sV)oYdMD6k CKa&@F9Hh4.dVDɐcd6Q^Y׳T[j uKS8P"DKםUkzܭ"ә!6o#cRmIPMr@0ߠh:J.ט#W!:Km9KD֘¾J5{4k%MK+̪Me'uSxq~D#ЌƖGz*4tR Wy=͢iVKE wt 5SgTEwO&I>tkϓ>tSgKM2WϟVMM70?{Jϡ>TҾ}It24SDT(Oܾ70?L~*KM2WϟV]jW|y)]r4@Gt {틦 dvܒ'29sXORk蘨lO0G+FIM"&CD u;dX3V"@%7ŷ};Jm$Dd(L0v; g|^tJM UTǍ#6bYᤆǭ r2 QZ DުzI$F֙=Jpق$boXF%Yv-aK{@vvY눻2"?r1OJҏEd:1Vk,T֪OvČd@Mu*4 ˾m0-CJUɺu,S*GlIbxj=BR6PccϷHdKG?^W7nRb Lm4^N}`SrR "^L. _cz8FyFkW-tDH*%DuvӁ2*?ѳ[?D]B<6P)laY2LlIVw!)|w vr^ XMY,ЗJi:!5kh-JƁbЌ]@\DX mԷ-i"F@(lkw6>i{c1oF}Nm-n nk[T1Zn* & wt 5fᩰMNQ]R4>{]ҥiU!^sK['OS"k 8Oa*k|'ӗ|ei`[ϝLϩ}V*niWK g.*|+_CKJП>}I+1JD*Pܨ ImՁ̘2P>Z\#iƥ$Btҩ[1`22+C/ₔ<0͡Z#g$ X`氐 _HnRuI*ˉŲ}g1sa1tg-֐>hM%#'v-rCjwliYuϗ2pVԖ''K[Kj9qZJʈ_єf(rlq=*AP)>q4D_p`E&D[[5SB{.+L63Mǰatr-Nc%K{=ŵ{HDh_LW9 Nv{# ͤ x+CUs]S&)ix(@SpZ hƝoz'r zO2ZOҾ~3ϟW]IE\u MB|;WTOݾ/f;#meչZ_8ZRr*֚kvzy<-o:)gVбƑ޺#i(v֐ / Co=:dxrUS)*"ĤN<`na+$M!jMI. O r-Dљlg7NdW)ئd+UC#bƮ@~ܡ DR-ٖ&F4rw{*jM i92j"f idW~J }t:T +~AI|DGmA`)&`ِ#Z5}#MY+0UV:s$b}:5Y(d׶u4(ez<,˕aap_39Qdce*TU~Bj[h6j^_IdUphۈO/8털A]D=‹i}SkA #RǜowT~D)ߑ~?KF&2#V;5L})9t~QA8Xʁv%X| `Sf.ͮRv)#U))u,.0gWy=cCR쇠Aiewt kZ*1v wt 5&S|ӓ9%ۤįRT>/ۅ XO>}I_VTօZWPKϕB|/Ҿ'ԟ>]%_OIOL+ϟJ']782$ȑܔqz*{цU1EsWk`Se5hLڣA01iAnyx}>;p 9De5MڦM-إbLFX`_K"@w&4xg; qtODW%. #`A+ &H.(2 `O.Agã5(Kg6 %DlQe JBvG-dkV †9tXq{$OUA5$.6han6"ܩHa5X@>Vo 5~,)9,I^$N]Lv]t,QFjtNo ;=&tkϟ_5Oi4rP} 55>]*Q?t… WϩBlԟ]%jaIw/4w0SKG}4*M'ϟ>|+_L[.Eە4S/\B_xx@;ĴY4zUP/.\@ACJ]X_>䝇C󖣜TZ 812-۫A^!2'-IodxliY `cxR)u|)vGͰI5x.a-X+0al 1pCb=rly6@:!d); ~׺pTە#& ;Zȅ?q)ZKJ0h8;@OӈD%&)c6 %P ʉ)YPFa_ ]|v%sZj, 6 T믜@n"n::7^)Š# ^**kmKmuiA]XBDҁxۉ>ŝ;>CCVc1;tU<Da&NkB# E^H6ތEVh]yp|=Nȝ8T0%FU 泯6 &L`!']\qdBI[Zř”296V^WVَzX8D%)F̅)iGk4naz@xp'Ͷg4;,[zr_<0z0*>ü:͔. FvKu4['Yc wt 5fS?!MnkK]MhQ|Iį} E[j_~u7JiuI0SJ)|*P~FO*t} 7I_>aQb4LbX`]6; Ts+zy?}4/fYA)B[W9MysBuoɫWf\1}5M.=(  @`wj;GecJA^jѥ}ljޅɛj/{g^3NxnF4NRҸ :*NźBg+biD CPhD:hb#-9?襁GTa!#YJ9: .0A#W/p 1|"t2}l.k+pk0cKGM[ז{ŠEϾtUPt6J;5"ji`~۞UGVDV[\^naSibuQ=-ֵHJɞlښbP!Vw`aJzG-1f=X =Y:$Hؖ#c`])qKjjW nNđZՍIUmZ6jT>h+ʹ0B &x*Y/9k4 [x<6~UxQ6kH؄701CReNoD.o0^u]RUNFŵiH>|LݷmْZԦ*B`Aȏ V_)t7PsPj0+m! wt 5ᩰSkR k^}FJ[ʟİRYZia>$Z4ɟ=kBV4s M+ #ZaUƺR5 _V|* BO〯#L|V˚vq! yIoHU^UwSD~D2x-7ҫ4oKVvQ{a*HJFcH`TEPp5>j1\*2x;X[u*Ju%-SԿ7x wt 5ᙠSN'ImSU)+>{ /=+ԓR} JtKϧ?z7ʔ|ϟS|0_~4>s U} %}M9k.FWk>MjGF~cFX3~$URڲ* JIէ¬{xCߘ#;F_憎ɁnT8 `x47/OL!co㍩Ox+ZiNAf?0`g LqU Ur=mjfCM{.Gs{T:8BggVp'۠$dLmѶͮcRatˆʐ$t#V@`RupQy ]]-;K{ [x!N[dӫzDŲ˝y.L wt 5Mǵ޾*kW|*tzOi(J4B&K 4|N=~*O>'MKJW锾~e4SKJzOsiT70% @Fkf\I(a&ćN|TY&`hkY58RM{` 37:Lv6Dw5U7o|ݱ:lZ-˛9tz1Hg< ).}&{ӥۊGC)VZH˓b͒nmDk'-Ao%yd~oSJ4I_J'vp|2dܾRK_>-'޾~ji^Wܾ\:uiQq&`Y+;Á*wŧFF;F.dV$0", =Ltt^; W:N!!M"RcNF Z浒Z٭ UqR(^pnqvM<^F4o.[B.I|O2tYrJ/ho4y5JQ96;JmhHջ$+^vtIkRJCLκFa9Hݢ"qdeBl`B-eX3DQ-ޓ Y3\`IHTc$ں5kSIg5KsBth&MvYWةs! pg7LL+JSTr_ =a{@}%WLavz/\.j^qokQsְܔ]`]|qC鴤#5&6hbHq wt 6᩸SORsm[_=*P9XORS 30ԿJԟK4 +i?K9aBg]җԡB~> 鞾~ԫRҿ{ *ja>}J]Kϛ\T}R\XզQNSpmJVvP$솶*p}!0e'X@<}3-^dDJ76 ؍)T({?Թ B_p oOM6xժ.L! kՏKVhwj lV~%S @xh{1;KK\>mh^79W#/a8MыЇC3rSnڀuETM:=O 8n' ؆]ezSҋRg5 Hf?*j\p8G6y>'@q]ƥY^QpWqx7MTV CDdvǮTMJRa  #zxBm#Lb Y).G :+T? },(kB@( ܉_Z>bf 1e6ZuK'bähToߏ94si0E*ZU9 7-P\ W ^AÖ-@ψz8 %|80jcMDKYKYbjhtHaEAwU73w!E dJNn샜Z. 1U wt 5Mϓ:|MO|ӧW{ ʕǰS|PK ko… uJt7iB{aO} lЩ}MK0>}Jp>G}hOQ ST}Iwa=޽2W7\]M@ݭtQwqZ㮼vW)|袙pVM `9Uj_\0%wo3)RЯڑp7}u#^RyPHJm2iu['59:&9Ms #T =|iĴk7q$I,&8MS3G%+>gҀ?{z6ڹV9k[qX d3D4k90h4"A';-kTuvZՔ܎<(-azGz_T|_F? B黢PddDvi0[j]z f& wt 5ṰSi]uI,4& '{ZjП?zja>}I霾´I)|ɜ~ZjUd_B w/Qϩ>|ϫ>}M3*%~%> 'F9 mw-GI!ft~f8WpO2J,m٩wl(jY)DZLNV5[' t75#s)1!-&dFL`Ū"i*ЪMkcix"k\6mӢ<{pިS#-ck_lO:AN -Ho$r#irnSOXEW4EZrӌKUcB@;&S 1]_$ OtefE8ҿzTqݎ ֱj6bk:yK^hvm۠,4Fٙ4,}I'_C%EsM]F鷖Sǧr;qm\zkZַ,iZ5,aJBu s+ZH(wq4-6 0,bGBLU4$V i)_ldg 7p,2Vk:oSm3\1 dKQ$X2߹0% o.u#u yRTܹdJZĀ݄E/X"7 @vu/D=ikLN7ZT ٤"Ǡ'1s.]62 S[9pڅTtZ1c Чb! _mb)}(W2hnoψ KB{ FZcH(=dde'O8ҰJ&nK1yd,,iܮ."ʝ4S'-TSڥJb |5Cbֳ FڽIwkT¥hڮH^SZP^6mWwa?h(K7zu6immEH^UH'RƍSV91 PkRB[]c`P=Y2kZK4/- A{$F"ٙ-fmH\{~hgW^f[SK@t5 w wt 5桱Sԝ=k|w>"ZeƵV} sU +} WΡWSZZS e4} 4ҟ(PZ{KBtB~niB|SϟBT+OVһ]5 E2;2q*k()DZJ @K(#K{IZ@ܙ*K2bX XXs#ANn 2.3cA_B cnGX Q/eyKג [ –2 Ӄi4f%DL@iv:Rm`.܏M;Ly Z2Eʵ27Y ! %ّnM{ar( م^ c>l==0"BmRBmu25ʳCz%66rR3Nc۾($dmr{59`\09r1ȱbK҃w;hkiUeU8f;-7&0E0A8uѯ"oT}/f %teڵNbZO9䬈|'hd3?8M7D51=>ʴa75@>=1Ti6vu~isd[VYB+ z5 ZO.wѐəJg,{ {8dО )[%jl•1E]+ӵ/x/dQ0x{[= :،(-4jIni)YU|g-$` wt 5Mz%4ʕU[W|0ПCSIgԫR|ʟ[JWOҽmhצϟ*M/] *jiZГR %I_N|+*|7RZZ.NFgPOޠJf]%yfrvԫÈqj/ ƥBB{>SD DS^Lď:ikIǏGv~ SvWqv&r;WZRXS{%1Xb);ևm]YZx4] &ƲyqS8U"0PL2'u;Ͱs\.BspunvDzvua/UQwp l6mCnrRm&,i?(O>*ê KVDEuTnaZVlɵvX-c,b> @ƸmDë́^[ gQkيcX:vWω O'#IcvZj> eu;nntayen2-6րC wt 5S|򥵳\%Nj0RN'ϩ*Φ*թ=5,*O?JY_QOx:${7I{ĒLEwsJgrL>rK/R@H9NKZMl%t#d GZMIWScOHN$\KT aL%Iٺ\Y m!C&ث#h"5DGVbiG5E^FH 7Ex2Њpt!m] ,:l:\@T-ŦU[NޑIKos_\ {I4nܧ)dQc* NuKbڷG!^':Wq1WC]5Ԉ;.b6w!Q~P h\/M‰$%~@ < Caqک,#FAnmZ3⁐Fٰcf8?>lSm3˼I*!VtOAE5*КW9R9]FY41"mzjߛ.74gѵmx1#1uRE'^'!ofnj\(Ў5(ޢ|!@ vJ OC#YKu$ӱHI`VW\JXȭ wt 5ơSaatלϝi}:gN_}M,50>]:4|-(Oԟ2UO! 'ϕCJ>(T?|j_WT/ -*'rda% ÅT oǀJz 5SI_%BuF zƊgsArxe{?.Ciqsy-$TT6@y"D*D"hZir%ƓnooB'w knFSŶ'cTJOq. aLɆ{ڕ4xM#Fd I<¦2 $5*=X]ax7Zc֗)y<˶);jJkZ%t#t)l:r/DYXҐ'0M'i?zkAnPfm*NV7P9C.R{xd+.*Gk%D X -(^V8E9k^qA<9^^_ݤĬg; 8~9QO}.=Iqhs[yٸCCip_*͌y靚젏<.o@gpohʴOxJT*!Z?|FըIH Ɨ[cp4n %\E=HV w%7G.\U 6 %w'9`tE*SÔĄd (. |\l̓U}]-/\Zu֫-gtz10&8+ 0̓EMCu wt 5ᡸSNaj[[aW|ӥ7M7ΡCs %OWSM,*ԩ>+iS +oB-*PB|i*O>~,'ԫRWҾJ„ϝU\ܾ3]n7{"omB~+]|WtF+I 86j0-iԴr${ 2I֘fwdp[)2v̨6h7ĴkZȐ3iVŶY+.CK9D,R09k! I){pUA+@(!" hܦc@!meh'iZ 1UsNcbT:6Ё+WF3\[}`JӿXl*{%&W- 0Iqlײ LJ#1dvLO4Z#YG~4l ۖTvkBO .pu0$ 1|xdGTu{&c4rdNiZFPppZֲh@ {Ipl?ՍP>X~^#rJ%rsڑݗcb8pPbpіf+ e'nw8qI&l ݄pX6eI iҪ/%3/~'ba5Pf+dZ-,4lIE5~OJ;3ذcRuh/^sOY@a"iEZ.{YԬI"g%{ wt 5ᩘSO|ikmjܮ~gΒk t)|'}IO>}J'uY.d 2ʞr2f{]4.SJf_{aJ'IkcP~rc/F~f[{d4Fm#Z9R)tZJXL{*zrOTӳO}uv^#4s#[#ZUYoI)QI~b9S>#mF1#e-NKf;SԘ\KO OB q|:aRյkn2Y}ꍡ3"nZW5Y|Ɏ܍7Pf߰l1ʜȘ]8AWD< wt 5ơTҺTwS* 3>tkǵLi'~2ϟM70>J>T'ҾtΒLZOܾR4~'}M-gL`_r;ã3FtaIDͬ$?j6);=D9s ٍʚnx*i'uT* I['ljM9˚cԁټEKG*ƪ2$!53}5w suGtXȔ'CQO0]LvE6pZBYMqlZIr4ƅHOb=8ύK$X5ov6ުm 5H*] vޜ9~?%GKl]rՋIR@~!:i'jC_ |#~a@|.ې+i(V0Dn$/hf]vn[:4/:%&*vUMk˖VɎ"tC$Stv`DyZ`1kIf% ۻUƤ_\E; "~GNj x"T>EqSK[ [9#29Gqq\Af0+&42﫛n =woW&'n7 Znu:V>TbEog۶B.$aI}/FımŬ؅U0!Վz r@ے"Cn%Txm .kSrF9_3l0X3nalu wt 5SN]M/:a U*S [sU['Y#aV2ZP%Z(u|(nb etTD470}FliCK S>~FПCK]+>|`]9Q=&E((:S6}_$fN;Qݥ(H&o*nPғQTm'R's!)|IeaKRJ'r6vh\Ʀ k%RZTss! ߓԞiciÿ"7nխ&l|#ZƑbn4DH[,-ԬTOͺi%=P$gc& 1(\o˶JsnY6{gՉ*:W*Y(̨^ wnT!⪅PR|0#T522krU+ Zؚd#@RpF?w+d|$)S-ԓ9.n];$+qV7/"ؘiYrB0 u%ΕIĒm,4\!**٣zvIJ;a\FO.%?PT3u ĔH+rų46QV&t0Ȭ^MܜnpG8:&13RyQ'\DQ}Y+OyeA1`}TD@ j`IvƜY)h9s rmLc9mw)Vh<ϩJ4I_ ϟ>}ISjU+KJ'|TU'ПR'Ҿ|rOM+TҾ|OSJW' Մ_ ,!,/[2Um sjHcMζԄ ZficϘ}hSHu) ⴭU-"k$NIFE "pM907m=Ub*2lx]nn0$ d`G~ /IfXյ.c6 ]Y 6|a(\MX3cE_խiHr:RrrtKwơxZ]y&'o󢻷JciC=즿kZx7!fEsDnP֙ jh?~'VI/`qLOm-Cch9N}T֐ mE>Gx[MPgʎީu5*Pk^klh2X֎T*FuDzw&Mӫx:9IGֲ%ZLn z&yɭ0#2= (G*)Z}Ik>dSIS0B} 5/>'¥]Ew_;r+ϡT,2|'^J$'bW/&cA/MwbZGIRsqYP NKbw4w_wa<4l.YX$8)X׫ij >BJju+u_/_S` ӵ I 61FL1kBUT_wXfnBz(aRCT UٮkZeyNMѦe)0"w%+u_ uIН)'J~Oi}M+ӫB Β~ni~TҤTVY*P-'ϕR|3Iֆ!SLg:JҿJ*%|3^MCDi28zSj (K(aPweL-iL)fqH,Zen%ӗjc ?Ļk8"7sSgm݋̉ n8DWG_C'dbi/LCc@rUBG)kе+=}N`9w3usIv6s _j6sx]Di V64iSZDzM-M)C˞P k)pY~)XJI}UU6 wt 5fSΩKF3OӛoaG{]-5N4|԰} 5ܾ]*Q?z}W~*O[.=t>ϟ>0ܿKZWϝ*KO>^G Mlm-]$'8nV"w m&ǂwԹ2Mm3E,yc(Z;]r3kZl8j= pE-x>BlB66w32FV%bl;qa5IbjNĥWӻ٢q9\44*tCp B]$K3]L[2Ʋ7nQ5/\BYv(mZW,4lAn[gBcjvnsT_TMvc>pn-NfYz`{❗)d9iƔײN;X1<'JF;~]_ ]Y,nTӀ.gR6r/14 R?`^Ϟd oK$4N\l)2yPֲEK?^GkR)-;nx3x( qȫ26 1vZK ʉY 2]UgvN}UQ׭ 7mXְ.٤^QjZ<{"oش~v$gdPmu⢣d b5!vz @mȑu㹍o65lf]ϘN { ֺPtI!Iq -PC  *L6No wt 5S>tm7ϡBL:|滥aį} i9~Fj_~S1uI*PSIik}J*U6en{^ϡ>4?K 74R[oaQKϡs :@_+Fzrz߷G$dqU kFJQ#abm8^h70䍛" v57NWDDI1kh-Sii^hSYư/cn=QIJXa<9StpPdٟ͘Ȼ=͡s[YaŜtrnM-4鿖"zstd<*lk`JҋB'-FvTtuJ DڸMd9>WEFe"rVIk¤NV֣zG' Sċ Ј*Q5f0vڒ9#T6"ưlC o;9Kv{X xd`S.G#4ZxW9VK}@gjqiL}5':Ȏ"EZ*j|Ȭd +ֺĒ ..!VxqMk:wLBe%7Ms,a%V,nН?0]G]CRU$bg&}t'JFI;[[SRv<ːnr0m^Hib:rWc`٭c2 P/))*v>er?oqp<=Tw&%Sg"_L~: wt 5SSsS H+W]MuO=T7ɟİR2ZO> RdWRtiS iBV4s LEҟ:UOa>EDB֤֫*5>_.}M1XOL@^9)܎S8 !gsUUN!ƕST#b5sw3X9Nj5p@+=m|6qr=^ǖ k%1cZ]tLPmGGuN3?VCpRNZFd-i(Pdem 0F"m[#%)n''%?LJ8gcHFxHGlD;8 3[傎ތ}%][C6zf&Zv3M=eIW;*c+6=<e-dV0 ,a2ICmX\)GthѹIP s.ՎHQ D3{`n`wwۖ+jֲk[Z^kޭԞS<ĀJX*4/QnWfIX6D^fɦ`Zt3ɒ 0HHz'{ Nuwyae5#:T&Ԁ nJvZr.DPJGz̀ߧwLGtOv.#+)Kp(tt! _LnK3 ؿ&n^KlOfV IKtY VHw wt 5S'!SW)>{ Wԩİ?JV>\*OA~+>:4ʔ|+O|'Ujk~:rRXU} 'KJk.'aB~]7$&Q0yYh< PQC\Vkcm\ -S~1,h.{3 kIYp@B8. ^Flf6 ct6M"Y 쒦szٖ[AyB. 1zQS11-uk a1-Z$sYJ/[kb̄r?d[ t[Í  -t\G4`k^JA.M($:p{- H(\L\,Ԉ96(jqiOZwɂf16Xؓeaf nįeZPlZ Akb۪R~"E;/bߏR*'gK~UX9heXk.sp{-N6qR|'O*6-O|Wܾ$]Rwk jꞪ~O…Z]ƧAi>z!ZL"ٗbDvpB5>1lBq ;YyFCF>BӘ+RKͤbi3`hq?ҧUea S.LY rh$+O7,`ti2N5=FB@l5ÄHQH zm4Ӫ4V)mѥMUᶅgRfbM;Qc !\061m|%"(Ib+eYk) ӬQʾK^?}ߋNUwm0 US\:,]cX@,k)Ka)SJ\9|.p/ pY)SȔǒ䭳6k ')kַV܍|]%^1nYۂ:+U1‘WAbQ`RnUI"$3Cʬ9eӆB>}Xt"MvQ4qnwё nI:ضp;0<)  2H،|IŽ]/fEi\USf1Y{x}h.6Y 'tjV|^ >lf|@rYe'I+O1,G{|i,5J',@6;Sh 6#N\rHpsYI_%w3hlC3I%]-͈"6M) wt 5MOҾsm_*|*O9X5П: 3$җԟSe54|niK=2a~XOkbB|+¬J ~W}JT>] 'Ы>\W]8eLbW: :۬mMq9K8mKMAk6̵WSo 8Q,*Vդݑx N !u@[`n%n8+b5ZgdNcFpL21qe: KHg((Kƶؔ;sգ575i)hOR 6rQ( ]cZ"|~vCҥ7M@3lAi1S#+0PNd.DxL%; n#Qߊn?J3B m<}.;`qSY~;`n y;/;/?<j3lD5x!&ǵ5kv< Ya˩rA`l̄Q<4Ml3m8]f~>T7 M5$ ]wZRa?9'xh֞tL(`ҮW{ ʟ/{Y0K 4+o_B 4_?s FwiB|O> jПɓ)}JB)_>~o>*ԡQ M%T|锽Wf;"b[K%h~hV vO%:r;YwBG*F Ќ ߮]x~__asE`k >pX;8ZHJ+J76: ph[ `պs6yQZv5iڤ}^5Ɔ-B bw+MEA{Z U7B1/L֧+k$kBɑͬ7v>2&N,%!E>|#n+W.3xH M)bG0Y`F DHU*EG6f5WVG @swi琏Rf3B+RW6A ooޥ.bJ*4u6EkH8MPl<q[:D[[>v͝xK)kU %Jϩ>ϫR}]Sj%} xOU@]8mN$h7^H=h868Œ"Ձ\fmn^Aha?N$f8|j~j@/WIe$&>r6dw H^7m&hO);y `41oEyV6i.5TZ)Ck]bzE྆'WcK8 9k]uj؛>R 㼖Bv5N9 nȯR-f/1zNcSU:pMHA<G!xAy<1p ">72 &WFi2ٯH5LQr s2 <3OWI=vJw4]fLi#G$a=֣}QgvUy~K;R@ybڼ٥mZZaiio !^&$ciH+QX+;5k]O:AD˺a9(]ֽnrLW*wciro؄ \]n 9hAAHGDa'b=Rzɢ7(` Uxa,V&X50YmVa;yMF5#ml˒UVv2hXyAV&C%9".^H wt 5ѸSf~wJЫ"[Mt5?K 7N„[na>} W} 2eNiiV|_'_;Jʧ_?I]ПVUIXP>}M,'_$sMWX\Iex;Usz!"n%cG؎`2sƪ}" j_3?ow,#TvWA[&܇#e8ih2$,k(& %E\ץ eHI [ћP31䡔۳VG8Dsi28G!:2e"f&R : >-B9QlmzXd P%RAv[Q7V4N{@ ʓ;Ex7FWrܑ֦kT֩RA'R5iG ٢/(wq4-?%g8 ZfX%`m4-!< }"LLFեˤz4ͬC;Zn sW8ǣr518.Kq(û]HPT:˱5!ʥjw$Q-QdF$`XCDͱrZ-i|IRC٤"2^ pO،n] p м_:-+ĩi5 촊c$1}A N-MiǔhW5/7ϋ(HpX* F7!\2$pkU|wʕ&6KӫR zXUU 4} †zWS *ϡ җПp?{ JB'~*OB|gթFT+UV]4Vv}sEYtC)Cڡ5G ֒]@d ƦjlI*(-OmFP Xl]9H(Ang7dx ͈[U*ndkdw=t0XҪ jZC]Ɖ ڎ`E[:RQcB>DrL[Gfd1WP̫]Űb5H֮ < wt 5M%4aNw>|5ПCJi>}JWʡB} Wha=Y(Tׯ2—ԟTM0d] *jsԓ?sYiaV|K¦ϡRZe ^3mw74|s];bHCht ī}MkX)M\F9jeU62gjmivb3?_7-Jpd[Aotu lW;9zgJ ytd,'ɊQڦmb!Nii\rTIm;/砮'MFh:z%V X)G. 0idv4%];|.uNϸDeYlk.d.G>Ss-ʜ^y2S?R5m.GQnl'dA6jDZ ژWu`M5yiL<b g&ɅDhabAkS9M6['#Q#10.P11%+a4ԤO)m"[Xt&Oogxv%JX6 PʜxhVi[V2ksfԭSYSSU{tKLw wM!$pedMvkI 9u*Igq%v#Ngϫ* j_=h_QOx**dԟ7/k} gй]F҄Μr,'.zR/f}`:o^xc@h 0~UxXPoWPXR(NMMb{*GX_'+Sm8txj3Aa{ZD5Hխ+m$g@ZSJr4\ $\$]f;zMBӁz֦7D+όH0@1Pև#Ϝ"z̡*NnՅS|,Cs` ̑vvR];*]m<ٔl rF8W־SJSJM騮;29 %d쉃MV#.!kJ]8In{# tpXYwREe$iEa9 2eOJUua3J$ڷB}j`K]zH"lymN/ӱ:"jюђݐ-К Mp gszQkP$+Ov =n!`C %&L}~cکtR͝>q^:s{{΁~zx%}B FcH~Êઔ!D` 61;fV|6IȺێ6&#ج,-,Q{%[J#jfB5)= rJVl#X DbgK[ٽuzFÅ?f&t41$--Zh: wt 5ᡸSiW|hat)wϝU9ii>|+P>ϟ?#=4/z-(OVΟ+U5{m'ȿLWkҤTS|tU7/Ҿ -*("2Vfj\@Ep`"%ti*xک)pll`V˚PćbsTi v\I)UJtWO;U%"KdJl;9N6(F4rҧ#p"?ϻ}al%fdJ#2,G2/lfd!>[Md% 剃J COs:KZK&.FWt`7&JNQ1@͢pyYǀHj=g{/E뛽HR},-=#L:&2u]FY/!༴8.~Egn]V]sjܢkZմ#c9 ֔ ^tNk\!ȷr/lX"EZ)Ӧ:mvz(-OqR4 nKaV fe L3쪸L}La\H "Iخ53 z׸3T2֓i Ui6&^̩~G-l'DLHac[ ezb}nJFH6;&`i"6e]@[d4ڪ#twLBdbA Gbx_m$$ak #kֵQkmb>|]ר ĕ/k&$>42òhӊ̴s[* 'U | wt 5FAS.aj[ZgaXBӥ1|-].W{[Uei_VMj֩R˪n_ tB3iB|5I_,%OҾ|,'ЫRIџBMKQCSIng`d7/M-j@]ׅňm:Ax@ܪPPN:k5R +ϛa iRFĔnҞ 1 pCAIҗ2-~ HWhcV:T'mBgqHFDr)O RH3)uE3d.K6ֹӭưXe@gH-s{Ձ:tb c5o&0-G_mh5i>TD!:^B<o$|r޿)2(C񼖮)x2 G)#Z&٧$YVQ5k#Zd<}IϟBuYA 5/?r2Wa>{UR|Y=SԟSJ4(Ťumu`nd Vۓ+61 Zg$(:& .rrJD6؆ 4d<2QX77-?ax_Di8GY*ZYN" o;,U=֏0p"9m{8켔03XJ D{i v82n3ec| z4!x5A9CXMj󫪊Ԓ'jXr4Ơa$ +\0Op[n=Z\wqZґkZ5RHw6Ze5=R-}ALv#egֵO1 N+` }Ґ\n(c , r6zKAn,_Cu[&jCe4i2-Wm%&r<_)|]Ji5汱Bր #swnV*f/بLDCT)tRM+[UDAvsbұlKbep R}C G.c[oL0 q4 VYqQҥ;.vknFPcYapcn0twe eh[!$~"7T|r] /փH*kl.bd" UMqߥpD~Fa-NwmY4gAR]mviGQ汭% wt 6SʾTӟ>34tNϔLS93ZSP>~4R'~Bɝ%~0VtaR}M,7NJ]ѱs0BU7S]!>ݕiMX5k%%%) m&̗|$I.)h= š{b;6㒓$&He`s *rrxcd23|"ĽƯd5fHJNUd]oy/`|I3,&dcFsV"sȦ6YM5Rx6 B6>Ia:k"W1n{"=i1SK`sEj7V$FI |S,By[+%mZ˽iV\Dlj]v;rԁ^%IT?NwUtx,Gpd44,x.@5ۺ/qI1ܥla%$؇7}xi/U3bFL$"9$ _^SHv`%N&KfraUĺ* wt 5fSnm/&a ʟ*{Ziϒ\tSogKVϟV-*PJ'aT|r.4SDT'թBJPҿJϩ>~'ПSSM2WϟR`;R: qJĦD&CKiɐb+*&Z[AJibY~r ˱S %7Z(nO@E8.[RTǡMs6[1T0Drcs!C u͇B|B[f-YuRf0qt$$lI< !W-~Knrmᘆ̴]9b`]ү2r|PH?{?3'D6'[tI*4%)":  m y)w:CrO0`ۍ'%$5bf(̚9ІڼU INHL۾,5+ϋ@ _0ҁ"w)Xh25-5hZ5ZZg}~<}J+xe{n=ƽ:wjZf:kH֑LcVv$&Jy$Pl4. 3ēQ0n)+4x=-G4,)xԘxۃdR!UPtZT yCZr2cGYUrբqָˬTB.F>NM:RT{Zڵ,ݵAsp eW `^A^Wr'J*Uʥ@aX wt 5᩸MN_sZ4:s]թS]ϕJo'ϟ>^-4l ]dKZ7Im}J'Ε|g_[Н?Te'ϟ}V'iWt7.*|+T|*P>}I@_j:xOlv/O* -(qԟ4a*J1%l۠*dKQզGrz[0wQEà:1Rޗ>.sùDRF r4PIY;7i; LIݻ+C[s`iPFmvX& [p4PN53W sp溗^8f#dԐΒll֠t@DɦCt5a#5YЅxC[2r[hg,j7*{Ic͸G5c^)=)CAq4 #뚵1/<4Ul4RzEYs urTO#3n2 GgI454(HJ$ك&9#lhHe[(< [6e6vRePCiS ̡pj *ܦIijʡrA^<҅h>Cw[tָƉ!rֲnBia, tZlB#~\yo+Vfn]S^SHGٶE)kWP\C tE>DsE@#*9pnb0d664uVBXBxdT*P(cWuc8`@c-R4kkk kNtF6-#0m wt 5ɰS5|tʓ:vaJį|5/ҿ{IwOTi}Ib nWKb50} 4>g*r zO:2ZϡCL,2dBcc2 4ׅ O>~VTYZWP-'Ⱦ|5Iֆ4'ԡR]_J&JT%~j@5&^~v5AT {= 4D"$eijִН$fNB5sam.[kנS]ir)#"V~1  g$Ep"aU]BY&(#\R@krE*p^CbMOfB(3) H"`N NtKr9n5P}x54sH]LDɕJ74|%ϴ7v<qS1֩$ AD՚!X\XZN1"hB6!ۜteI;AƿSp1!w 1*I+p^7%4.a^ G{ gB~]س~PM:L[2 A`^:8m:}MTĻ83%ߛ]pRm)v͌4t Aum a`P m7CO.@2?qtp3;O(kHnahvٰ;[{4g,n#P9EޝΉSh$n[f+Q\ǀ1! wt 5FɰSөCFgН)!G{]4ΓoB}JԳ N*QSK]'Ҿ} ZOR}Z7++քu0>ϟ>\K ZOR+P>|^oG㦠؀q͔%>1F'8gM҇٤R}y&ۚU0yIar2>"s)VLu֑!(q`,C} N]甗\bƺ  '( YOLNgR4L ]RZHKi5RA*׻q2 Ȱ~u}. e 3JF۷e4bYM]}Uv3RؤI:VV\Cq//o!' +zjS:BoSfBqa~ln/^z~*r(d%z+ֳَ9݌rMg; )>v2a:ӌ\9sM#,FRlTΉRYc{xNMJ[р Ztap=[c0r`D4U8+!,{"T8jrgaOiq׈!-vƭvyYP59U5Wjd ۱Nݠjwv)9SVt/Yb)fK+JaΒRO9} 50>Ҿ~'…kB}I,թ%钫I^*T|74~70S{G]'M'ϡ -(@S"pr,o$6(sKJ0hE\[%J ))1d$9I8t+##:- Z%sZ:Zİclw.| nQ[mKe[Z28-`m cfl,5kՔPL($M2#1;|hf18"#S:Q+D@VKtD(b0[z'wp|=Nϧ5Lyl.J(M_T|-06D$ K+{cB &-mjQř”ZAڐؓ|B5;o$uN"JI^XfBّ/mɹk[Y;g,*Ol٬086X0k l ;mqѥ-9( T<]K3TaW OjN<6?z,/)ZN6ҨtmQf?q}I̯Bd{R1;#e R)"kѠuHsV@y)G)ch>#a$`%x']79%VgeTNVjY} 6'x Dwⶾ2ӥ.$uEa8֫k]pEW wt 5SSMsk{mm1|)įҦ ZOZkiV~utik ¬'*LPҟӤԔFiW.t¤Zi_>_V ie/LbX@2yF.2&lb%|0ILA*wAubV MPʜ3Kѱ)u#:QXI\3>&4nx* ҌcX0/dN[mkW WS$B|qTHlEmaט\6+tTž@ϑ/Ǡb52kNvSp۪,cn0}đ>E'崓R dA6Ftք]Rl2%,Q#K_ Jr$4!`&.BG^He ѵ9Yu`x1T\޳>˾wQ~뀧!WFTUmivmvhyS"jmm&NKT^e)A$ |#wj[m$(HEĤkok2Qjq~b`"I{qAСJtl 8Z0>H )Xd ha'>mC~2z%=%g85rv8Rq]4:2T@Fa9Ya6cWz޴Z2C#(^t,hUgzx –\rc-K8ǃ^t_Af p#Fܵ!ͪ#^kZUI }` wt 5ᡰS+!mk `{ߤ[ʟ0|g0>c}IIiB:4aBVs 'USk|Xe:JM[ ]V} j;SL4_.}Ia~'_]>z1 4Rv@@%Q7$3["ֵ6fO溿vY hF+2۴ڣJ(}၎WE-{7=u@i>Fs bD}z<}Lxr1R/rlJ슡Iu iUww>Jqn$4nRlVIEf͘I!MIȌ!`E' w(B+VSrYf5VNdqSGQiP+2HҕH< !fCinhW6^)Pk-aV<&DN4˩vÃ"FD\~DOR1'Kh&%1)u NN`]NQEJ)j>\3+ $vuhXDD$ nb]j@ Sk/7vC7m-hk"ޫTYKRQbfƑa.$ _ k dӴs,6SEj߬Pbucי1AA![I}F6",5OPصr5^*Tn"$ htn 00?; x?] ;PۑkZf[T֥uئ`J! wt 5ᡠSO9ɔ®+*{ n/ҽϝdJ(НRt7ꤩ's?|4 _} 4:L4R}ZOM,*i~FI?z]KWl9dvv5ˆHBaK$q|Y-}f j:} rLm.>N껸i)* 7Jn6!mcs0 [9|UrqA"aF%vsFv 0 6+DԚ̚ڏi].pJ~EkT)g! D' +Ƌkԡ3N@fv>5;J]o h?: 7*TnQV~ڭZ{ 1jdcl gXw{Ms$z1 kĩ-jcLŁdx Z755M!-Ge+h6q2 71n m1+/5>ImYH; wt 5M޿U *k۪6:OiJ'aB&L'ϡVF}I)ЩR'-gԟ}JWҾe4R kJsT74%8 fU2%hjى~%~ b/Q=IORrEqA+nbA>m;+*J";(dwe+*tܴw:hbDj5,L $%>6&~!M_{1s2fc.1NKuG5DL'q7L1Z:ME$D߄?b~25" WɊlLj7kzm]CCa&}anCZ((ϚfkZ֑kH ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/silence-44-s.flac0000644000175000017500000014333000000000000020506 0ustar00lazkalazka00000000000000fLaC"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]UUUUP././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/silence-44-s.mp30000644000175000017500000004000000000000000020266 0ustar00lazkalazka00000000000000ID3 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 Data2004././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1574016271.0 mutagen-1.45.1/tests/data/silence-44-s.tak0000644000175000017500000013744700000000000020374 0ustar00lazkalazka00000000000000tBaK @M }o ܺ8{IJbܷH2ĺJ(cqRIFF WAVEfmt DLISTINFOIART piman;jzigICRD2004IGNRSilenceINAMSilenceIPRDQuod Libet Test DataIPRT02/10ISFTLavf58.29.100data &4@M  x<xxx<x<x <x x.xxxyx<x<x\xx<x<x ..<ԡ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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/sv4_header.mpc0000644000175000017500000000020000000000000020261 0ustar00lazkalazka00000000000000' .'RK߷S1fYLbeYe1B ,hn ^ٕh3隩*\˳K|9vqZr+dՏ؉ZG,܋LvkJÜ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/sv5_header.mpc0000644000175000017500000000020000000000000020262 0ustar00lazkalazka00000000000000/ vư@RJ)mj)fc,1B,Fc./yz˷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:*֫.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/variable-block.flac0000644000175000017500000002400000000000000021244 0ustar00lazkalazka00000000000000fLaC"  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첚t././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1571038736.0 mutagen-1.45.1/tests/data/vbri.mp30000644000175000017500000002000000000000000017117 0ustar00lazkalazka00000000000000ID3eTRCK01TENC@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 <