././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736809851.8696141 tagpy-2025.1/0000775000175000017500000000000014741316574012673 5ustar00palfreypalfrey././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736809810.0 tagpy-2025.1/LICENSE0000664000175000017500000000212514741316522013671 0ustar00palfreypalfreyCopyright (c) 2006-2008 Andreas Kloeckner Copyright (c) 2022-2025 Tom Parker-Shemilt 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736019321.0 tagpy-2025.1/MANIFEST.in0000664000175000017500000000017514736306571014434 0ustar00palfreypalfreyinclude test/la.* include test/*.py include src/wrapper/common.hpp include test/tagrename include LICENSE include README.md ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736809851.8696141 tagpy-2025.1/PKG-INFO0000644000175000017500000000743514741316574013777 0ustar00palfreypalfreyMetadata-Version: 2.2 Name: tagpy Version: 2025.1 Summary: Python Bindings for TagLib Home-page: https://github.com/palfrey/tagpy Author: Tom Parker-Shemilt Author-email: palfrey@tevp.net License: MIT Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Operating System :: POSIX Classifier: Operating System :: Unix Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Multimedia :: Sound/Audio Classifier: Topic :: Multimedia :: Sound/Audio :: CD Audio :: CD Ripping Classifier: Topic :: Multimedia :: Sound/Audio :: Editors Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Utilities Requires-Python: >=3.9, <4 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: packaging>=14.0 Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: description Dynamic: description-content-type Dynamic: home-page Dynamic: license Dynamic: requires-dist Dynamic: requires-python Dynamic: summary TagPy ===== [![PyPI](https://img.shields.io/pypi/v/tagpy)](https://pypi.org/project/tagpy/) [![Coverage Status](https://coveralls.io/repos/github/palfrey/tagpy/badge.svg)](https://coveralls.io/github/palfrey/tagpy) TagPy is a a set of Python bindings for [Scott Wheeler's TagLib](https://taglib.org/). It builds upon [Boost.Python](http://www.boost.org/libs/python/doc/), a wrapper generation library which is part of the [Boost set of C++ libraries](http://www.boost.org). Just like TagLib, TagPy can: - read and write ID3 tags of version 1 and 2, with many supported frame types for version 2 (in MPEG Layer 2 and MPEG Layer 3, FLAC and MPC), - access Xiph Comments in Ogg Vorbis Files and Ogg Flac Files, - access APE tags in Musepack and MP3 files. - access ID3 version 2 tags in WAV files All these have their own specific interfaces, but TagLib's generic tag reading and writing mechanism is also supported. You can find examples in the test/ directory. Installing TagPy ================ If you're lucky (Python 3.9-3.13 on x86 Linux currently), you can probably just run `pip install tagpy` which will use the precompiled wheels. If this fails due to compilation issues, you'll need to install some things first. * Debian: `apt-get install libboost-python-dev libtag1-dev` * Fedora: `dnf install boost-python3-devel taglib-devel` * Alpine 3.17: `apk add taglib-dev boost1.80-python3` (or another `boost*-python3` for other alpine versions) Other setups are not currently supported, but patches with CI checking for others are welcomed. TagPy works with - TagLib >=1.9 (all versions up to 2.0.2 currently tested) - Boost.Python 1.74 - gcc 10.2.1 Slightly older versions of gcc and Boost.Python should be fine, but the 1.9 requirement for TagLib is firm. Anything newer is probably ok, and please file bugs for anything that fails. Using TagPy =========== Using TagPy is as simple as this: >>> import tagpy >>> f = tagpy.FileRef("la.mp3") >>> f.tag().artist u'Andreas' The `test/` directory contains a few more examples. In general, TagPy duplicates the TagLib API, with a few notable exceptions: - Namespaces (i.e. Python modules) are spelled in lower case. For example, `TagLib::Ogg::Vorbis` is now `taglib.ogg.vorbis`. - Enumerations form their own scope and are not part of any enclosing class scope, if any. For example, the value `TagLib::String::UTF16BE` from the enum `TagLib::String::Type` is now `tagpy.StringType.UTF16BE`. - `TagLib::String` objects are mapped to and expected as Python unicode objects. - `TagLib::ByteVector` objects are mapped to regular Python string objects. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736809810.0 tagpy-2025.1/README.md0000664000175000017500000000512714741316522014150 0ustar00palfreypalfreyTagPy ===== [![PyPI](https://img.shields.io/pypi/v/tagpy)](https://pypi.org/project/tagpy/) [![Coverage Status](https://coveralls.io/repos/github/palfrey/tagpy/badge.svg)](https://coveralls.io/github/palfrey/tagpy) TagPy is a a set of Python bindings for [Scott Wheeler's TagLib](https://taglib.org/). It builds upon [Boost.Python](http://www.boost.org/libs/python/doc/), a wrapper generation library which is part of the [Boost set of C++ libraries](http://www.boost.org). Just like TagLib, TagPy can: - read and write ID3 tags of version 1 and 2, with many supported frame types for version 2 (in MPEG Layer 2 and MPEG Layer 3, FLAC and MPC), - access Xiph Comments in Ogg Vorbis Files and Ogg Flac Files, - access APE tags in Musepack and MP3 files. - access ID3 version 2 tags in WAV files All these have their own specific interfaces, but TagLib's generic tag reading and writing mechanism is also supported. You can find examples in the test/ directory. Installing TagPy ================ If you're lucky (Python 3.9-3.13 on x86 Linux currently), you can probably just run `pip install tagpy` which will use the precompiled wheels. If this fails due to compilation issues, you'll need to install some things first. * Debian: `apt-get install libboost-python-dev libtag1-dev` * Fedora: `dnf install boost-python3-devel taglib-devel` * Alpine 3.17: `apk add taglib-dev boost1.80-python3` (or another `boost*-python3` for other alpine versions) Other setups are not currently supported, but patches with CI checking for others are welcomed. TagPy works with - TagLib >=1.9 (all versions up to 2.0.2 currently tested) - Boost.Python 1.74 - gcc 10.2.1 Slightly older versions of gcc and Boost.Python should be fine, but the 1.9 requirement for TagLib is firm. Anything newer is probably ok, and please file bugs for anything that fails. Using TagPy =========== Using TagPy is as simple as this: >>> import tagpy >>> f = tagpy.FileRef("la.mp3") >>> f.tag().artist u'Andreas' The `test/` directory contains a few more examples. In general, TagPy duplicates the TagLib API, with a few notable exceptions: - Namespaces (i.e. Python modules) are spelled in lower case. For example, `TagLib::Ogg::Vorbis` is now `taglib.ogg.vorbis`. - Enumerations form their own scope and are not part of any enclosing class scope, if any. For example, the value `TagLib::String::UTF16BE` from the enum `TagLib::String::Type` is now `tagpy.StringType.UTF16BE`. - `TagLib::String` objects are mapped to and expected as Python unicode objects. - `TagLib::ByteVector` objects are mapped to regular Python string objects. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736809810.0 tagpy-2025.1/pyproject.toml0000664000175000017500000000041314741316522015576 0ustar00palfreypalfrey[tool.flake8] ignore = "E126,E127,E128,E123,E226,E241,E242,W503" max-line-length=120 [tool.pytest.ini_options] norecursedirs = "boost build" [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" [tool.coverage.run] relative_files = true ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736809851.8696141 tagpy-2025.1/setup.cfg0000664000175000017500000000004614741316574014514 0ustar00palfreypalfrey[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736809810.0 tagpy-2025.1/setup.py0000664000175000017500000000742114741316522014402 0ustar00palfreypalfrey#!/usr/bin/env python # -*- coding: latin-1 -*- # Copyright (c) 2006-2008 Andreas Kloeckner, Christoph Burgmer # Copyright (c) 2022-2025 Tom Parker-Shemilt # # 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. from ctypes.util import find_library import sys from setuptools import setup, Extension def main(): INCLUDE_DIRS = "" # conf["TAGLIB_INC_DIR"] + conf["BOOST_INC_DIR"] LIBRARY_DIRS = "" # conf["TAGLIB_LIB_DIR"] + conf["BOOST_LIB_DIR"] boost_name = None boost_options = [ "boost_python%d" % sys.version_info[0], "boost_python%d%d" % sys.version_info[:2], "boost_python-py%d%d" % sys.version_info[:2], "boost_python%d%d-mt-x64" % sys.version_info[:2], ] for boost_option in boost_options: library_path = find_library(boost_option) if library_path is not None: boost_name = boost_option break assert boost_name is not None, "Can't find boost-python. Tried %s" % boost_options LIBRARIES = [boost_name, "tag"] if "-mt-" in boost_name: LIBRARIES.append("z") setup( name="tagpy", version="2025.1", description="Python Bindings for TagLib", long_description=open("README.md", encoding="utf-8").read(), long_description_content_type="text/markdown", author="Tom Parker-Shemilt", author_email="palfrey@tevp.net", classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: Unix", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Multimedia :: Sound/Audio", "Topic :: Multimedia :: Sound/Audio :: CD Audio :: CD Ripping", "Topic :: Multimedia :: Sound/Audio :: Editors", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Utilities", ], license="MIT", url="https://github.com/palfrey/tagpy", packages=["tagpy", "tagpy.ogg"], python_requires=">=3.9, <4", install_requires=["packaging >= 14.0"], ext_modules=[ Extension( "_tagpy", [ "src/wrapper/basics.cpp", "src/wrapper/id3.cpp", "src/wrapper/rest.cpp", ], include_dirs=INCLUDE_DIRS, library_dirs=LIBRARY_DIRS, libraries=LIBRARIES, extra_compile_args="", # conf["CXXFLAGS"], ), ], ) if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1736809851.865614 tagpy-2025.1/src/0000775000175000017500000000000014741316574013462 5ustar00palfreypalfrey././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1736809851.865614 tagpy-2025.1/src/wrapper/0000775000175000017500000000000014741316574015142 5ustar00palfreypalfrey././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736718969.0 tagpy-2025.1/src/wrapper/basics.cpp0000664000175000017500000001415314741035171017105 0ustar00palfreypalfrey// Copyright (c) 2006-2008 Andreas Kloeckner // // 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. #include #include #include #include #include #include #include #include #include #include "common.hpp" namespace { struct tlstring_to_unicode { static PyObject *convert(String const& s) { const char *cstr = s.toCString(true); return PyUnicode_DecodeUTF8(cstr, strlen(cstr), "ignore"); } }; struct tlbytevector_to_string { static PyObject *convert(ByteVector const& s) { return PyBytes_FromStringAndSize(s.data(), s.size()); } }; struct ByteVectorIntermediate { string str; ByteVectorIntermediate(const string &_str) : str(_str) { } operator ByteVector() const { return ByteVector(str.data(), str.size()); } }; // ------------------------------------------------------------- // Basics // ------------------------------------------------------------- struct AudioPropertiesWrap : AudioProperties, wrapper { int length() const { return this->get_override("length")(); } int bitrate() const { return this->get_override("bitrate")(); } int sampleRate() const { return this->get_override("sampleRate")(); } int channels() const { return this->get_override("channels")(); } protected: AudioPropertiesWrap(ReadStyle style) : AudioProperties(style) { } }; struct FileWrap : File, wrapper { public: Tag *tag() const { return this->get_override("tag")(); } AudioProperties *audioProperties() const { return this->get_override("audioProperties")(); } bool save() { return this->get_override("save")(); } protected: FileWrap(const char *file) : File(file) { } }; } void exposeID3(); void exposeRest(); #define STRINGIZE_NX(A) #A #define STRINGIZE(A) STRINGIZE_NX(A) BOOST_PYTHON_MODULE(_tagpy) { // version info scope().attr("version") = STRINGIZE(TAGLIB_MAJOR_VERSION) "." STRINGIZE(TAGLIB_MINOR_VERSION); // ------------------------------------------------------------- // Infrastructure // ------------------------------------------------------------- to_python_converter(); to_python_converter(); implicitly_convertible(); implicitly_convertible(); implicitly_convertible(); // ------------------------------------------------------------- // Basics // ------------------------------------------------------------- exposeList("StringListBase"); { typedef StringList cl; class_ > >("StringList") ; } { typedef Tag cl; class_, boost::noncopyable>("Tag", no_init) .add_property("title", &cl::title, &cl::setTitle) .add_property("artist", &cl::artist, &cl::setArtist) .add_property("album", &cl::album, &cl::setAlbum) .add_property("comment", &cl::comment, &cl::setComment) .add_property("genre", &cl::genre, &cl::setGenre) .add_property("year", &cl::year, &cl::setYear) .add_property("track", &cl::track, &cl::setTrack) .DEF_VIRTUAL_METHOD(isEmpty) .DEF_SIMPLE_METHOD(duplicate) .staticmethod("duplicate") ; } { typedef AudioProperties cl; class_("AudioProperties", no_init) #if TAGLIB_HEX_VERSION < CHECK_VERSION(1,10,0) .add_property("length", &cl::length) #else .add_property("length", &cl::lengthInSeconds) #endif .add_property("bitrate", &cl::bitrate) .add_property("sampleRate", &cl::sampleRate) .add_property("channels", &cl::channels) ; } enum_("ReadStyle") .value("Fast", AudioProperties::Fast) .value("Average", AudioProperties::Average) .value("Accurate", AudioProperties::Accurate) ; { typedef File cl; class_("File", no_init) .def("name", &File::name) .def("audioProperties", pure_virtual(&File::audioProperties), return_internal_reference<>()) .def("tag", pure_virtual(&File::tag), return_internal_reference<>()) .DEF_VIRTUAL_METHOD(save) .DEF_SIMPLE_METHOD(readOnly) .DEF_SIMPLE_METHOD(isOpen) .DEF_SIMPLE_METHOD(isValid) .DEF_SIMPLE_METHOD(clear) .DEF_SIMPLE_METHOD(length) ; } enum_("StringType") .value("Latin1", String::Latin1) .value("UTF16", String::UTF16) .value("UTF16BE", String::UTF16BE) .value("UTF8", String::UTF8) .value("UTF16LE", String::UTF16LE) ; exposeID3(); exposeRest(); } // EMACS-FORMAT-TAG // // Local Variables: // mode: C++ // eval: (c-set-style "stroustrup") // eval: (c-set-offset 'access-label -2) // eval: (c-set-offset 'inclass '++) // c-basic-offset: 2 // tab-width: 8 // End: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736718969.0 tagpy-2025.1/src/wrapper/common.hpp0000664000175000017500000001643314741035171017141 0ustar00palfreypalfrey// Copyright (c) 2006-2008 Andreas Kloeckner // // 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. #include #include #include using namespace boost::python; using namespace TagLib; using namespace std; #define MF_OL(MF, MIN, MAX) \ BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS(MF##_overloads, MF, MIN, MAX); #define DEF_SIMPLE_METHOD(NAME) \ def(#NAME, &cl::NAME) #define DEF_VIRTUAL_METHOD(NAME) \ def(#NAME, pure_virtual(&cl::NAME)) #define DEF_OVERLOADED_METHOD(NAME, CAST) \ def(#NAME, (CAST) &cl::NAME, NAME##_overloads()) #define ENUM_VALUE(NAME) \ value(#NAME, scope::NAME) #define ADD_RO_PROPERTY(NAME) \ add_property(#NAME, &cl::NAME) #define CHECK_VERSION(MAJOR, MINOR, PATCH) \ (MAJOR << 16) + \ (MINOR << 8) + \ (PATCH << 0) #define TAGLIB_HEX_VERSION CHECK_VERSION(TAGLIB_MAJOR_VERSION, TAGLIB_MINOR_VERSION, TAGLIB_PATCH_VERSION) #if CHECK_VERSION(1,9,0) < TAGLIB_HEX_VERSION #warning !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! #warning TagPy is meant to wrap TagLib 1.9 and above. #warning !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! #endif namespace { template inline object make_list(Iterator first, Iterator last) { boost::python::list result; while (first != last) result.append(*first++); return result; } // ------------------------------------------------------------- // Map // ------------------------------------------------------------- template Value &Map_getitem(Map &m, const Key &k) { if (!m.contains(k)) { PyErr_SetString( PyExc_KeyError, "key not in map"); throw error_already_set(); } return m[k]; } template void Map_setitem(Map &m, const Key &k, const Value &v) { m[k] = v; } template object Map_keys(Map &m) { boost::python::list keys; typedef Map map; typename map::Iterator first = m.begin(), last = m.end(); while (first != last) keys.append((first++)->first); return keys; } template void exposeMap(const char *name) { typedef Map map; class_(name) .def("__len__", &map::size) .def("size", &map::size) .def("clear", &map::clear, return_self<>()) .def("isEmpty", &map::isEmpty) .def("__getitem__", Map_getitem, return_internal_reference<>()) .def("__setitem__", Map_setitem) .def("__contains__", &map::contains) .def("keys", Map_keys) ; } // ------------------------------------------------------------- // List // ------------------------------------------------------------- template Value &List_getitem(List &l, uint i) { if (i >= l.size()) { PyErr_SetString( PyExc_IndexError, "index out of bounds"); throw error_already_set(); } return l[i]; } template void List_setitem(List &l, uint i, Value v) { if (i >= l.size()) { PyErr_SetString( PyExc_IndexError, "index out of bounds"); throw error_already_set(); } l[i] = v; } template void List_append(List &l, Value v) { l.append(v); } template void exposeList(const char *name) { typedef List list; class_(name) .def("__len__", &list::size) .def("size", &list::size) .def("clear", &list::clear, return_self<>()) .def("isEmpty", &list::isEmpty) .def("__getitem__", List_getitem, return_value_policy()) .def("__setitem__", List_setitem) .def("append", List_append) // MISSING: iterators, insert, find, contains, erase, // assignment, comparison ; } // ------------------------------------------------------------- // PointerList // ------------------------------------------------------------- template Value *&PointerList_getitem(List &l, uint i) { if (i >= l.size()) { PyErr_SetString( PyExc_IndexError, "index out of bounds"); throw error_already_set(); } return l[i]; } template void PointerList_setitem(List &l, uint i, auto_ptr v) { if (i >= l.size()) { PyErr_SetString( PyExc_IndexError, "index out of bounds"); throw error_already_set(); } l[i] = v.release(); } template void PointerList_append(List &l, auto_ptr v) { l.append(v.release()); } template void exposePointerList(const char *name) { typedef List list; class_(name) .def("__len__", &list::size) .def("size", &list::size) .def("clear", &list::clear, return_self<>()) .def("isEmpty", &list::isEmpty) .def("__getitem__", PointerList_getitem, return_internal_reference<>()) .def("__setitem__", PointerList_setitem) .def("append", PointerList_append) // MISSING: iterators, insert, find, contains, erase, // assignment, comparison ; } } template struct TagWrap : T, wrapper { String title() const { return this->get_override("title")(); } String artist() const { return this->get_override("artist")(); } String album() const { return this->get_override("album")(); } String comment() const { return this->get_override("comment")(); } String genre() const { return this->get_override("genre")(); } unsigned int year() const { return this->get_override("year")(); } unsigned int track() const { return this->get_override("track")(); } void setTitle(const String &v) const { this->get_override("setTitle")(v); } void setArtist(const String &v) const { this->get_override("setArtist")(v); } void setAlbum(const String &v) const { this->get_override("setAlbum")(v); } void setComment(const String &v) const { this->get_override("setComment")(v); } void setGenre(const String &v) const { this->get_override("setGenre")(v); } void setYear(unsigned int i) const { this->get_override("setYear")(i); } void setTrack(unsigned int i) const { this->get_override("setTrack")(i); } }; ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736722753.0 tagpy-2025.1/src/wrapper/id3.cpp0000664000175000017500000004044414741044501016317 0ustar00palfreypalfrey// Copyright (c) 2006-2008 Andreas Kloeckner // // 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. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "common.hpp" namespace { // ------------------------------------------------------------- // ID3v2 // ------------------------------------------------------------- struct id3v2_FrameWrap : ID3v2::Frame, wrapper { String toString() const { return this->get_override("toString")(); } protected: // maintain constructability id3v2_FrameWrap(const ByteVector &data) : ID3v2::Frame(data) { } // In docs, but not in code: /* id3v2_FrameWrap(ID3v2::Header *h) : ID3v2::Frame(h) { } */ }; void id3v2_Tag_addFrame(ID3v2::Tag &t, ID3v2::Frame *f) { #if TAGLIB_HEX_VERSION < CHECK_VERSION(1,12,0) ID3v2::Frame *f_clone = ID3v2::FrameFactory::instance()->createFrame(f->render()); #else ID3v2::Frame *f_clone = ID3v2::FrameFactory::instance()->createFrame(f->render(), t.header()); #endif t.addFrame(f_clone); } object id3v2_rvf_channels(const ID3v2::RelativeVolumeFrame &rvf) { List l = rvf.channels(); return make_list(l.begin(), l.end()); } #define MF_OL(MF, MIN, MAX) \ BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS(MF##_overloads, MF, MIN, MAX); #if TAGLIB_HEX_VERSION < CHECK_VERSION(1,12,0) MF_OL(createFrame, 1, 2); #else MF_OL(createFrame, 2, 2); #endif MF_OL(volumeAdjustmentIndex, 0, 1); MF_OL(volumeAdjustment, 0, 1); MF_OL(peakVolume, 0, 1); MF_OL(setVolumeAdjustmentIndex, 1, 2); MF_OL(setVolumeAdjustment, 1, 2); MF_OL(setPeakVolume, 1, 2); #if (TAGPY_TAGLIB_HEX_VERSION >= 0x10800) MF_OL(render, 0, 1) #endif // ------------------------------------------------------------- // MPEG // ------------------------------------------------------------- #if (TAGPY_TAGLIB_HEX_VERSION >= 0x10800) MF_OL(save, 0, 3) #else MF_OL(save, 0, 2) #endif MF_OL(ID3v1Tag, 0, 1) MF_OL(ID3v2Tag, 0, 1) MF_OL(APETag, 0, 1) MF_OL(strip, 0, 1) } void exposeID3() { // ------------------------------------------------------------- // ID3v1 // ------------------------------------------------------------- def("id3v1_genre", ID3v1::genre); class_ > ("id3v1_Tag") .def("render", &ID3v1::Tag::render) ; // ------------------------------------------------------------- // ID3v2 // ------------------------------------------------------------- exposeMap("id3v2_FrameListMap"); exposePointerList("id3v2_FrameList"); { typedef ID3v2::FrameFactory cl; #if TAGLIB_HEX_VERSION < CHECK_VERSION(1,12,0) ID3v2::Frame *(ID3v2::FrameFactory::*cf1)(const ByteVector &, bool) const = &cl::createFrame; ID3v2::Frame *(ID3v2::FrameFactory::*cf2)(const ByteVector &, TagLib::uint) const #else ID3v2::Frame *(ID3v2::FrameFactory::*cf)(const ByteVector &, const ID3v2::Header *) const #endif = &cl::createFrame; class_ ("id3v2_FrameFactory", no_init) #if TAGLIB_HEX_VERSION < CHECK_VERSION(1,12,0) .def("createFrame", cf1, return_value_policy()) .def("createFrame", cf2, createFrame_overloads()[return_value_policy()]) #else .def("createFrame", cf, createFrame_overloads()[return_value_policy()]) #endif .def("instance", &cl::instance, return_value_policy()) .staticmethod("instance") .DEF_SIMPLE_METHOD(defaultTextEncoding) .DEF_SIMPLE_METHOD(setDefaultTextEncoding) ; } { typedef ID3v2::Frame cl; class_("id3v2_Frame", no_init) .DEF_SIMPLE_METHOD(frameID) .DEF_SIMPLE_METHOD(size) .DEF_SIMPLE_METHOD(setData) .DEF_SIMPLE_METHOD(setText) .def("toString", pure_virtual(&ID3v2::Frame::toString)) .DEF_SIMPLE_METHOD(render) .def("headerSize", #if (TAGLIB_MAJOR_VERSION == 1) (TagLib::uint (*)()) #else (unsigned int (TagLib::ID3v2::Frame::*)() const) #endif &ID3v2::Frame::headerSize) .def("headerSize", #if (TAGLIB_MAJOR_VERSION == 1) (TagLib::uint (*)()) #else (unsigned int (TagLib::ID3v2::Frame::*)() const) #endif &ID3v2::Frame::headerSize) // MISSING: textDelimiter ; } { typedef ID3v2::Header cl; class_ ("id3v2_Header") // MISSING: second constructor .DEF_SIMPLE_METHOD(majorVersion) .DEF_SIMPLE_METHOD(revisionNumber) .DEF_SIMPLE_METHOD(extendedHeader) .DEF_SIMPLE_METHOD(experimentalIndicator) .DEF_SIMPLE_METHOD(footerPresent) .DEF_SIMPLE_METHOD(tagSize) .DEF_SIMPLE_METHOD(completeTagSize) .DEF_SIMPLE_METHOD(setTagSize) .DEF_SIMPLE_METHOD(setData) .DEF_SIMPLE_METHOD(render) .DEF_SIMPLE_METHOD(size) .staticmethod("size") .DEF_SIMPLE_METHOD(fileIdentifier) .staticmethod("fileIdentifier") ; } { typedef ID3v2::ExtendedHeader cl; class_ ("id3v2_ExtendedHeader", no_init) .DEF_SIMPLE_METHOD(size) .DEF_SIMPLE_METHOD(setData) ; } { typedef ID3v2::Footer cl; class_ ("id3v2_Footer", no_init) .DEF_SIMPLE_METHOD(render) .DEF_SIMPLE_METHOD(size) .staticmethod("size") ; } { typedef ID3v2::Tag cl; const ID3v2::FrameList &(cl::*fl1)(const ByteVector &) const = &cl::frameList; const ID3v2::FrameList &(cl::*fl2)() const = &cl::frameList; class_ >("id3v2_Tag") .def("header", &ID3v2::Tag::header, return_internal_reference<>()) .def("extendedHeader", &ID3v2::Tag::extendedHeader, return_internal_reference<>()) #if (TAGLIB_MAJOR_VERSION == 1) .def("footer", &ID3v2::Tag::footer, return_internal_reference<>()) #endif .def("frameListMap", &ID3v2::Tag::frameListMap, return_internal_reference<>()) .def("frameList", fl1, return_internal_reference<>()) .def("frameList", fl2, return_internal_reference<>()) .def("addFrame", id3v2_Tag_addFrame) .DEF_SIMPLE_METHOD(removeFrame) .DEF_SIMPLE_METHOD(removeFrames) #if (TAGPY_TAGLIB_HEX_VERSION >= 0x10800) // Commented out following comment at: // https://github.com/inducer/tagpy/commit/fb6d9a95f8ed1b0f347a82569a13e60a75c7e6d6 // .DEF_OVERLOADED_METHOD(render, ByteVector (cl::*)() const) #if TAGLIB_HEX_VERSION < CHECK_VERSION(1,12,0) .DEF_OVERLOADED_METHOD(render, ByteVector (cl::*)(int) const) #else .DEF_OVERLOADED_METHOD(render, ByteVector (cl::*)(ID3v2::Version) const) #endif #else .def("render", (ByteVector (cl::*)() const) &cl::render) #endif ; } // ------------------------------------------------------------- // ID3v2 frame types // ------------------------------------------------------------- { typedef TagLib::ID3v2::AttachedPictureFrame scope; enum_("id3v2_AttachedPictureFrame_Type") .ENUM_VALUE(Other) .ENUM_VALUE(FileIcon) .ENUM_VALUE(OtherFileIcon) .ENUM_VALUE(FrontCover) .ENUM_VALUE(BackCover) .ENUM_VALUE(LeafletPage) .ENUM_VALUE(Media) .ENUM_VALUE(LeadArtist) .ENUM_VALUE(Artist) .ENUM_VALUE(Conductor) .ENUM_VALUE(Band) .ENUM_VALUE(Composer) .ENUM_VALUE(Lyricist) .ENUM_VALUE(RecordingLocation) .ENUM_VALUE(DuringRecording) .ENUM_VALUE(DuringPerformance) .ENUM_VALUE(MovieScreenCapture) .ENUM_VALUE(ColouredFish) .ENUM_VALUE(Illustration) .ENUM_VALUE(BandLogo) .ENUM_VALUE(PublisherLogo) ; } { typedef ID3v2::AttachedPictureFrame cl; class_, boost::noncopyable> ("id3v2_AttachedPictureFrame", init >()) .DEF_SIMPLE_METHOD(textEncoding) .DEF_SIMPLE_METHOD(setTextEncoding) .DEF_SIMPLE_METHOD(mimeType) .DEF_SIMPLE_METHOD(setMimeType) .DEF_SIMPLE_METHOD(type) .DEF_SIMPLE_METHOD(setType) .DEF_SIMPLE_METHOD(description) .DEF_SIMPLE_METHOD(setDescription) .DEF_SIMPLE_METHOD(picture) .DEF_SIMPLE_METHOD(setPicture) ; } { typedef ID3v2::CommentsFrame cl; class_, boost::noncopyable> ("id3v2_CommentsFrame", init >()) .def(init()) .DEF_SIMPLE_METHOD(language) .DEF_SIMPLE_METHOD(setLanguage) .DEF_SIMPLE_METHOD(description) .DEF_SIMPLE_METHOD(setDescription) .DEF_SIMPLE_METHOD(textEncoding) .DEF_SIMPLE_METHOD(setTextEncoding) ; } { typedef ID3v2::RelativeVolumeFrame::PeakVolume cl; class_ ("id3v2_PeakVolume") .def_readwrite("bitsRepresentingPeak", &cl::bitsRepresentingPeak) .def_readwrite("peakVolume", &cl::peakVolume) ; } { typedef TagLib::ID3v2::RelativeVolumeFrame scope; enum_("id3v2_RelativeVolumeFrame_ChannelType") .ENUM_VALUE(Other) .ENUM_VALUE(MasterVolume) .ENUM_VALUE(FrontRight) .ENUM_VALUE(FrontLeft) .ENUM_VALUE(BackRight) .ENUM_VALUE(BackLeft) .ENUM_VALUE(FrontCentre) .ENUM_VALUE(BackCentre) .ENUM_VALUE(Subwoofer) ; } { typedef ID3v2::RelativeVolumeFrame cl; class_, boost::noncopyable> ("id3v2_RelativeVolumeFrame", init()) // MISSING: Empty constructor, gives symbol errors .def("channels", id3v2_rvf_channels) #if (TAGLIB_MAJOR_VERSION == 1) .DEF_SIMPLE_METHOD(setChannelType) #endif .DEF_OVERLOADED_METHOD(volumeAdjustmentIndex, short (cl::*)(cl::ChannelType) const) .DEF_OVERLOADED_METHOD(setVolumeAdjustmentIndex, void (cl::*)(short, cl::ChannelType)) .DEF_OVERLOADED_METHOD(volumeAdjustment, float (cl::*)(cl::ChannelType) const) .DEF_OVERLOADED_METHOD(setVolumeAdjustment, void (cl::*)(float, cl::ChannelType)) .DEF_OVERLOADED_METHOD(peakVolume, cl::PeakVolume (cl::*)(cl::ChannelType) const) .DEF_OVERLOADED_METHOD(setPeakVolume, void (cl::*)(const cl::PeakVolume &, cl::ChannelType)) ; } { typedef ID3v2::TextIdentificationFrame cl; class_, boost::noncopyable> ("id3v2_TextIdentificationFrame", init >()) .def("setText", (void (cl::*)(const String &)) &cl::setText) .def("setText", (void (cl::*)(const StringList &)) &cl::setText) .DEF_SIMPLE_METHOD(textEncoding) .DEF_SIMPLE_METHOD(setTextEncoding) .DEF_SIMPLE_METHOD(fieldList) ; } { typedef ID3v2::UnsynchronizedLyricsFrame cl; class_, boost::noncopyable> ("id3v2_UnsynchronizedLyricsFrame", init >()) .def(init()) .DEF_SIMPLE_METHOD(language) .DEF_SIMPLE_METHOD(setLanguage) .DEF_SIMPLE_METHOD(description) .DEF_SIMPLE_METHOD(setDescription) .DEF_SIMPLE_METHOD(textEncoding) .DEF_SIMPLE_METHOD(setTextEncoding) ; } { typedef ID3v2::UserTextIdentificationFrame cl; class_, boost::noncopyable> ("id3v2_UserTextIdentificationFrame", init()) .def(init >()) .DEF_SIMPLE_METHOD(description) .DEF_SIMPLE_METHOD(setDescription) .DEF_SIMPLE_METHOD(fieldList) ; } { typedef ID3v2::UniqueFileIdentifierFrame cl; class_, boost::noncopyable> ("id3v2_UniqueFileIdentifierFrame", init()) .def(init()) .DEF_SIMPLE_METHOD(owner) .DEF_SIMPLE_METHOD(setOwner) .DEF_SIMPLE_METHOD(identifier) .DEF_SIMPLE_METHOD(setIdentifier) ; } { typedef ID3v2::UnknownFrame cl; class_, boost::noncopyable> ("id3v2_UnknownFrame", init()) .DEF_SIMPLE_METHOD(data) ; } // ------------------------------------------------------------- // MPEG // ------------------------------------------------------------- enum_("mpeg_TagTypes") .value("NoTags", MPEG::File::NoTags) .value("ID3v1", MPEG::File::ID3v1) .value("ID3v2", MPEG::File::ID3v2) .value("APE", MPEG::File::APE) .value("AllTags", MPEG::File::AllTags) ; { typedef MPEG::Properties cl; class_, boost::noncopyable> ("mpeg_Properties", //init() no_init ) .ADD_RO_PROPERTY(layer) // .ADD_RO_PROPERTY(protectionEnabled) (not implemented in TagLib 1.4) // .ADD_RO_PROPERTY(channelMode) (depends on ChannelMode type) .ADD_RO_PROPERTY(isCopyrighted) .ADD_RO_PROPERTY(isOriginal) ; } { typedef MPEG::File cl; class_, boost::noncopyable> ("mpeg_File", init >()) .def(init >()) .def("save", #if TAGLIB_HEX_VERSION < CHECK_VERSION(1,12,0) (bool (MPEG::File::*)(int, bool, int)) #else (bool (MPEG::File::*)(int, TagLib::File::StripTags, TagLib::ID3v2::Version, TagLib::File::DuplicateTags)) #endif &cl::save, save_overloads()) .def("ID3v1Tag", (ID3v1::Tag *(MPEG::File::*)(bool)) &cl::ID3v1Tag, ID3v1Tag_overloads()[return_internal_reference<>()]) .def("ID3v2Tag", (ID3v2::Tag *(MPEG::File::*)(bool)) &cl::ID3v2Tag, ID3v2Tag_overloads()[return_internal_reference<>()]) .def("APETag", (APE::Tag *(cl::*)(bool)) &cl::APETag, APETag_overloads()[return_internal_reference<>()]) .def("strip", (bool (cl::*)(int)) &cl::strip, strip_overloads()) #if (TAGLIB_MAJOR_VERSION == 1) .DEF_SIMPLE_METHOD(setID3v2FrameFactory) #endif .DEF_SIMPLE_METHOD(firstFrameOffset) .DEF_SIMPLE_METHOD(nextFrameOffset) .DEF_SIMPLE_METHOD(previousFrameOffset) .DEF_SIMPLE_METHOD(lastFrameOffset) ; } // MISSING: Header, XingHeader } // EMACS-FORMAT-TAG // // Local Variables: // mode: C++ // eval: (c-set-style "stroustrup") // eval: (c-set-offset 'access-label -2) // eval: (c-set-offset 'inclass '++) // c-basic-offset: 2 // tab-width: 8 // End: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736722753.0 tagpy-2025.1/src/wrapper/rest.cpp0000664000175000017500000003360114741044501016612 0ustar00palfreypalfrey// Copyright (c) 2006-2008 Andreas Kloeckner // // 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. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "common.hpp" namespace { // ------------------------------------------------------------- // FLAC // ------------------------------------------------------------- MF_OL(ID3v1Tag, 0, 1); MF_OL(ID3v2Tag, 0, 1); MF_OL(xiphComment, 0, 1); // ------------------------------------------------------------- // Ogg // ------------------------------------------------------------- MF_OL(addField, 2, 3); #if TAGLIB_HEX_VERSION < CHECK_VERSION(1,11,0) MF_OL(removeField, 1, 2); #else MF_OL(removeFields, 1, 2); #endif MF_OL(render, 0, 1); // ------------------------------------------------------------- // APE // ------------------------------------------------------------- MF_OL(addValue, 2, 3); // ------------------------------------------------------------- // MPC // ------------------------------------------------------------- #if (TAGLIB_MAJOR_VERSION == 1) MF_OL(remove, 0, 1); #endif //MF_OL(ID3v1Tag, 0, 1); MF_OL(APETag, 0, 1); // WAV MF_OL(strip, 0, 1); #if TAGLIB_HEX_VERSION >= CHECK_VERSION(1,10,0) // MP4 TagLib::MP4::CoverArtList mp4_Tag_GetCovers(TagLib::MP4::Tag &t) { if (!t.contains("covr")) { return {}; } return t.item("covr").toCoverArtList(); } void mp4_Tag_SetCovers(TagLib::MP4::Tag &t, TagLib::MP4::CoverArtList& l) { return t.setItem("covr", l); } #endif } #if TAGLIB_HEX_VERSION >= CHECK_VERSION(1,11,0) void addPictureWithOwnership(Ogg::XiphComment &cl, std::auto_ptr picture) { cl.addPicture(picture.get()); picture.release(); } #endif void exposeRest() { // ------------------------------------------------------------- // Ogg // ------------------------------------------------------------- exposeMap("ogg_FieldListMap"); { typedef Ogg::XiphComment cl; class_, boost::noncopyable> ("ogg_XiphComment", init >()) .DEF_SIMPLE_METHOD(fieldCount) .def("fieldListMap", &cl::fieldListMap, return_internal_reference<>()) .DEF_SIMPLE_METHOD(vendorID) .DEF_OVERLOADED_METHOD(addField, void (cl::*)(const String &, const String &, bool)) #if TAGLIB_HEX_VERSION < CHECK_VERSION(1,11,0) .DEF_OVERLOADED_METHOD(removeField, void (cl::*)(const String &, const String &)) .DEF_OVERLOADED_METHOD(removeField, void (cl::*)(const String &, const String &)) #else .DEF_OVERLOADED_METHOD(removeFields, void (cl::*)(const String &, const String &)) .DEF_OVERLOADED_METHOD(removeFields, void (cl::*)(const String &, const String &)) #endif .DEF_OVERLOADED_METHOD(render, ByteVector (cl::*)(bool) const) #if TAGLIB_HEX_VERSION >= CHECK_VERSION(1,11,0) .DEF_SIMPLE_METHOD(pictureList) .DEF_SIMPLE_METHOD(removeAllPictures) .def("removePicture", &cl::removePicture) .def("addPicture", addPictureWithOwnership); #endif ; } { typedef Ogg::File cl; class_, boost::noncopyable> ("ogg_File", no_init) .DEF_SIMPLE_METHOD(packet) .DEF_SIMPLE_METHOD(setPacket) // MISSING: page headers ; } { typedef Ogg::FLAC::File cl; class_, boost::noncopyable> ("ogg_flac_File", init >()) ; } { typedef Ogg::Vorbis::File cl; class_, boost::noncopyable> ("ogg_vorbis_File", init >()) ; } // ------------------------------------------------------------- // APE // ------------------------------------------------------------- { typedef APE::Footer cl; class_( "ape_Footer", init >()) .DEF_SIMPLE_METHOD(version) .DEF_SIMPLE_METHOD(headerPresent) .DEF_SIMPLE_METHOD(footerPresent) .DEF_SIMPLE_METHOD(isHeader) .DEF_SIMPLE_METHOD(setHeaderPresent) .DEF_SIMPLE_METHOD(itemCount) .DEF_SIMPLE_METHOD(setItemCount) .DEF_SIMPLE_METHOD(tagSize) .DEF_SIMPLE_METHOD(completeTagSize) .DEF_SIMPLE_METHOD(setTagSize) .DEF_SIMPLE_METHOD(setData) .DEF_SIMPLE_METHOD(renderFooter) .DEF_SIMPLE_METHOD(renderHeader) ; } { typedef APE::Item scope; enum_("ape_ItemTypes") .ENUM_VALUE(Text) .ENUM_VALUE(Binary) .ENUM_VALUE(Locator) ; } { typedef APE::Item cl; class_("ape_Item") .def(init()) .def(init()) .def(init()) .DEF_SIMPLE_METHOD(key) .DEF_SIMPLE_METHOD(binaryData) .DEF_SIMPLE_METHOD(size) .DEF_SIMPLE_METHOD(toString) .DEF_SIMPLE_METHOD(values) .DEF_SIMPLE_METHOD(render) .DEF_SIMPLE_METHOD(parse) .DEF_SIMPLE_METHOD(setReadOnly) .DEF_SIMPLE_METHOD(isReadOnly) .DEF_SIMPLE_METHOD(setType) .DEF_SIMPLE_METHOD(type) .DEF_SIMPLE_METHOD(isEmpty) ; } exposeMap("ape_ItemListMap"); { typedef APE::Tag cl; class_, boost::noncopyable>("ape_Tag") .def(init()) .def("footer", &cl::footer, return_internal_reference<>()) .def("itemListMap", &cl::itemListMap, return_internal_reference<>()) .DEF_SIMPLE_METHOD(removeItem) .DEF_OVERLOADED_METHOD(addValue, void (cl::*)(const String &, const String &,bool)) .DEF_SIMPLE_METHOD(setItem) ; } // ------------------------------------------------------------- // FLAC // ------------------------------------------------------------- enum_("flac_PictureType") .value("Other", TagLib::FLAC::Picture::Type::Other) .value("FileIcon", TagLib::FLAC::Picture::Type::FileIcon) .value("OtherFileIcon", TagLib::FLAC::Picture::Type::OtherFileIcon) .value("FrontCover", TagLib::FLAC::Picture::Type::FrontCover) .value("BackCover", TagLib::FLAC::Picture::Type::BackCover) .value("LeafletPage", TagLib::FLAC::Picture::Type::LeafletPage) .value("Media", TagLib::FLAC::Picture::Type::Media) .value("LeadArtist", TagLib::FLAC::Picture::Type::LeadArtist) .value("Artist", TagLib::FLAC::Picture::Type::Artist) .value("Conductor", TagLib::FLAC::Picture::Type::Conductor) .value("Band", TagLib::FLAC::Picture::Type::Band) .value("Composer", TagLib::FLAC::Picture::Type::Composer) .value("Lyricist", TagLib::FLAC::Picture::Type::Lyricist) .value("RecordingLocation", TagLib::FLAC::Picture::Type::RecordingLocation) .value("DuringRecording", TagLib::FLAC::Picture::Type::DuringRecording) .value("DuringPerformance", TagLib::FLAC::Picture::Type::DuringPerformance) .value("MovieScreenCapture", TagLib::FLAC::Picture::Type::MovieScreenCapture) .value("ColouredFish", TagLib::FLAC::Picture::Type::ColouredFish) .value("Illustration", TagLib::FLAC::Picture::Type::Illustration) .value("BandLogo", TagLib::FLAC::Picture::Type::BandLogo) .value("PublisherLogo", TagLib::FLAC::Picture::Type::PublisherLogo) ; { typedef TagLib::FLAC::Picture cl; class_, boost::noncopyable> ("flac_Picture", init()) .DEF_SIMPLE_METHOD(type) .DEF_SIMPLE_METHOD(data) .DEF_SIMPLE_METHOD(mimeType) .def("setType", &cl::setType) .def("setMimeType", &cl::setMimeType); ; } exposePointerList("flac_PictureList"); { typedef FLAC::File cl; class_ >("flac_File", init >()) .def(init >()) .def("ID3v1Tag", (ID3v1::Tag *(FLAC::File::*)(bool)) &FLAC::File::ID3v1Tag, ID3v1Tag_overloads()[return_internal_reference<>()]) .def("ID3v2Tag", (ID3v2::Tag *(FLAC::File::*)(bool)) &FLAC::File::ID3v2Tag, ID3v2Tag_overloads()[return_internal_reference<>()]) .def("xiphComment", (Ogg::XiphComment *(FLAC::File::*)(bool)) &FLAC::File::xiphComment, xiphComment_overloads()[return_internal_reference<>()]) ; } // ------------------------------------------------------------- // MPC // ------------------------------------------------------------- enum_("mpc_TagTypes") .value("NoTags", MPC::File::NoTags) .value("ID3v1", MPC::File::ID3v1) .value("ID3v2", MPC::File::ID3v2) .value("APE", MPC::File::APE) .value("AllTags", MPC::File::AllTags) ; { typedef MPC::File cl; class_, boost::noncopyable> ("mpc_File", init >()) .def("ID3v1Tag", (ID3v1::Tag *(cl::*)(bool)) &cl::ID3v1Tag, ID3v1Tag_overloads()[return_internal_reference<>()]) .def("APETag", (APE::Tag *(cl::*)(bool)) &cl::APETag, APETag_overloads()[return_internal_reference<>()]) .def("remove", (void (cl::*)(int)) &cl::strip, strip_overloads()) ; } /// WAV enum_("wav_TagTypes") .value("NoTags", TagLib::RIFF::WAV::File::NoTags) .value("ID3v2", TagLib::RIFF::WAV::File::ID3v2) .value("Info", TagLib::RIFF::WAV::File::Info) .value("AllTags", TagLib::RIFF::WAV::File::AllTags) ; { typedef TagLib::RIFF::WAV::File cl; class_, boost::noncopyable> ("wav_File", init >()) .def("ID3v2Tag", (ID3v2::Tag *(TagLib::RIFF::WAV::File::*)()) &cl::ID3v2Tag, return_internal_reference<>()) .def("InfoTag", (TagLib::RIFF::Info::Tag *(TagLib::RIFF::WAV::File::*)()) &cl::InfoTag, return_internal_reference<>()) #if (TAGPY_TAGLIB_HEX_VERSION >= 0x11100) .DEF_OVERLOADED_METHOD(strip, void (cl::*)(TagLib::RIFF::WAV::File::TagTypes) const) #endif ; } /// MP4 #if TAGLIB_HEX_VERSION >= CHECK_VERSION(1,13,0) enum_("mp4_TagTypes") .value("NoTags", TagLib::MP4::File::NoTags) .value("MP4", TagLib::MP4::File::MP4) .value("AllTags", TagLib::MP4::File::AllTags) ; #endif { typedef TagLib::MP4::File cl; class_, boost::noncopyable> ("mp4_File", init >()) .def("tag", (MP4::Tag *(cl::*)() const) &cl::tag, return_internal_reference<>()) ; } enum_("mp4_CoverArtFormats") .value("JPEG", TagLib::MP4::CoverArt::JPEG) .value("PNG", TagLib::MP4::CoverArt::PNG) .value("BMP", TagLib::MP4::CoverArt::BMP) .value("GIF", TagLib::MP4::CoverArt::GIF) .value("Unknown", TagLib::MP4::CoverArt::Unknown) ; { typedef TagLib::MP4::CoverArt cl; class_ ("mp4_CoverArt", init()) .DEF_SIMPLE_METHOD(format) .DEF_SIMPLE_METHOD(data) ; } exposeList("mp4_CoverArtList"); { typedef TagLib::MP4::Tag cl; class_, boost::noncopyable>("Tag", no_init) .add_property("title", &cl::title, &cl::setTitle) .add_property("artist", &cl::artist, &cl::setArtist) .add_property("album", &cl::album, &cl::setAlbum) .add_property("comment", &cl::comment, &cl::setComment) .add_property("genre", &cl::genre, &cl::setGenre) .add_property("year", &cl::year, &cl::setYear) .add_property("track", &cl::track, &cl::setTrack) #if TAGLIB_HEX_VERSION >= CHECK_VERSION(1,10,0) .add_property("covers", &mp4_Tag_GetCovers, &mp4_Tag_SetCovers) #endif .DEF_VIRTUAL_METHOD(isEmpty) .DEF_SIMPLE_METHOD(duplicate) .staticmethod("duplicate") ; } } // EMACS-FORMAT-TAG // // Local Variables: // mode: C++ // eval: (c-set-style "stroustrup") // eval: (c-set-offset 'access-label -2) // eval: (c-set-offset 'inclass '++) // c-basic-offset: 2 // tab-width: 8 // End: ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1736809851.865614 tagpy-2025.1/tagpy/0000775000175000017500000000000014741316574014017 5ustar00palfreypalfrey././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736718969.0 tagpy-2025.1/tagpy/__init__.py0000664000175000017500000000735714741035171016133 0ustar00palfreypalfrey# Copyright (c) 2006-2008 Andreas Kloeckner, Christoph Burgmer # # 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. from typing import cast from _tagpy import File, ReadStyle, version as _version version = cast(str, _version) class FileTypeResolver(object): def createFile( self, fileName, readAudioProperties=True, audioPropertiesStyle=ReadStyle.Average ): raise NotImplementedError class FileRef(object): fileTypeResolvers = [] def __init__( self, f, readAudioProperties=True, audioPropertiesStyle=ReadStyle.Average ): if isinstance(f, FileRef): self._file = f._file elif isinstance(f, File): self._file = f else: self._file = FileRef.create(f, readAudioProperties, audioPropertiesStyle) def tag(self): return self._file.tag() def audioProperties(self): return self._file.audioProperties() def file(self): return self._file def save(self): return self._file.save() @classmethod def defaultFileExtensions(cls): return cls._getExtToModule.keys() def isNull(self): return not self._file or not self._file.isValid() @classmethod def addFileTypeResolver(cls, resolver): cls.fileTypeResolvers.insert(0, resolver) return resolver @staticmethod def _getExtToModule(): import tagpy.ogg.vorbis import tagpy.ogg.flac import tagpy.mpeg import tagpy.mp4 import tagpy.flac import tagpy.mpc import tagpy.wav # import tagpy.wavpack, tagpy.ogg.speex, tagpy.trueaudio return { "ogg": tagpy.ogg.vorbis, "mp3": tagpy.mpeg, "oga": tagpy.ogg.flac, "flac": tagpy.flac, "mpc": tagpy.mpc, "wav": tagpy.wav, "mp4": tagpy.mp4, "m4a": tagpy.mp4, # ".wv": tagpy.wavpack, # ".spx": tagpy.ogg.speex, # ".tta": tagpy.trueaudio, } @classmethod def create( cls, fileName, readAudioProperties=True, audioPropertiesStyle=ReadStyle.Average ): for resolver in cls.fileTypeResolvers: file = resolver.createFile( fileName, readAudioProperties, audioPropertiesStyle ) if file: return file from os.path import exists if not exists(fileName): raise IOError("File does not exist") from os.path import splitext ext = splitext(fileName)[1][1:].lower() try: module = cls._getExtToModule()[ext] except KeyError: raise ValueError("unable to find file type") return module.File(fileName, readAudioProperties, audioPropertiesStyle) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643926215.0 tagpy-2025.1/tagpy/ape.py0000644000175000017500000000230514177051307015125 0ustar00palfreypalfrey# Copyright (c) 2006-2008 Andreas Kloeckner # # 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 _tagpy ItemTypes = _tagpy.ape_ItemTypes Item = _tagpy.ape_Item Footer = _tagpy.ape_Footer Tag = _tagpy.ape_Tag ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643926215.0 tagpy-2025.1/tagpy/flac.py0000644000175000017500000000216514177051307015271 0ustar00palfreypalfrey# Copyright (c) 2006-2008 Andreas Kloeckner # # 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 _tagpy File = _tagpy.flac_File ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643926215.0 tagpy-2025.1/tagpy/id3v1.py0000644000175000017500000000302714177051307015310 0ustar00palfreypalfrey# Copyright (c) 2006-2008 Andreas Kloeckner # # 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 _tagpy def _genreList(): result = [] i = 0 next = _tagpy.id3v1_genre(i) while next: result.append(next) i += 1 next = _tagpy.id3v1_genre(i) return result _GenreList = _genreList() _GenreMap = dict([(v, k) for k, v in enumerate(_GenreList)]) genre = _tagpy.id3v1_genre def genreIndex(genre): return _GenreMap[genre] def genreList(): return _GenreList def genreMap(): return _GenreMap ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643926215.0 tagpy-2025.1/tagpy/id3v2.py0000644000175000017500000000401014177051307015302 0ustar00palfreypalfrey# Copyright (c) 2006-2008 Andreas Kloeckner # # 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 _tagpy FrameFactory = _tagpy.id3v2_FrameFactory Frame = _tagpy.id3v2_Frame FrameListMap = _tagpy.id3v2_FrameListMap FrameList = _tagpy.id3v2_FrameList Header = _tagpy.id3v2_Header ExtendedHeader = _tagpy.id3v2_ExtendedHeader Footer = _tagpy.id3v2_Footer Tag = _tagpy.id3v2_Tag AttachedPictureFrameType = _tagpy.id3v2_AttachedPictureFrame_Type AttachedPictureFrame = _tagpy.id3v2_AttachedPictureFrame CommentsFrame = _tagpy.id3v2_CommentsFrame PeakVolume = _tagpy.id3v2_PeakVolume ChannelType = _tagpy.id3v2_RelativeVolumeFrame_ChannelType RelativeVolumeFrame = _tagpy.id3v2_RelativeVolumeFrame TextIdentificationFrame = _tagpy.id3v2_TextIdentificationFrame UserTextIdentificationFrame = _tagpy.id3v2_UserTextIdentificationFrame UniqueFileIdentifierFrame = _tagpy.id3v2_UniqueFileIdentifierFrame UnkownFrame = _tagpy.id3v2_UnknownFrame UnknownFrame = _tagpy.id3v2_UnknownFrame UnsynchronizedLyricsFrame = _tagpy.id3v2_UnsynchronizedLyricsFrame ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736809810.0 tagpy-2025.1/tagpy/mp4.py0000664000175000017500000000255114741316522015065 0ustar00palfreypalfrey# Copyright (c) 2025 Tom Parker-Shemilt # # 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 _tagpy import tagpy from packaging.version import Version if Version(tagpy.version) >= Version("1.13"): TagTypes = _tagpy.mp4_TagTypes File = _tagpy.mp4_File CoverArtFormats = _tagpy.mp4_CoverArtFormats CoverArt = _tagpy.mp4_CoverArt CoverArtList = _tagpy.mp4_CoverArtList ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643926215.0 tagpy-2025.1/tagpy/mpc.py0000644000175000017500000000222314177051307015136 0ustar00palfreypalfrey# Copyright (c) 2006-2008 Andreas Kloeckner # # 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 _tagpy TagTypes = _tagpy.mpc_TagTypes File = _tagpy.mpc_File ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643924906.0 tagpy-2025.1/tagpy/mpeg.py0000644000175000017500000000015314177046652015316 0ustar00palfreypalfreyimport _tagpy TagTypes = _tagpy.mpeg_TagTypes File = _tagpy.mpeg_File Properties = _tagpy.mpeg_Properties ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1736809851.865614 tagpy-2025.1/tagpy/ogg/0000775000175000017500000000000014741316574014573 5ustar00palfreypalfrey././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643926215.0 tagpy-2025.1/tagpy/ogg/__init__.py0000644000175000017500000000230014177051307016666 0ustar00palfreypalfrey# Copyright (c) 2006-2008 Andreas Kloeckner # # 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 _tagpy FieldListMap = _tagpy.ogg_FieldListMap XiphComment = _tagpy.ogg_XiphComment File = _tagpy.ogg_File ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736714765.0 tagpy-2025.1/tagpy/ogg/flac.py0000664000175000017500000000234414741025015016040 0ustar00palfreypalfrey# Copyright (c) 2006-2008 Andreas Kloeckner # # 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 _tagpy File = _tagpy.ogg_flac_File PictureType = _tagpy.flac_PictureType Picture = _tagpy.flac_Picture PictureList = _tagpy.flac_PictureList ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643926215.0 tagpy-2025.1/tagpy/ogg/vorbis.py0000644000175000017500000000217314177051307016443 0ustar00palfreypalfrey# Copyright (c) 2006-2008 Andreas Kloeckner # # 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 _tagpy File = _tagpy.ogg_vorbis_File ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736809810.0 tagpy-2025.1/tagpy/wav.py0000664000175000017500000000222414741316522015157 0ustar00palfreypalfrey# Copyright (c) 2022-2025 Tom Parker-Shemilt # # 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 _tagpy TagTypes = _tagpy.wav_TagTypes File = _tagpy.wav_File ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736809851.8696141 tagpy-2025.1/tagpy.egg-info/0000775000175000017500000000000014741316574015511 5ustar00palfreypalfrey././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736809851.0 tagpy-2025.1/tagpy.egg-info/PKG-INFO0000644000175000017500000000743514741316573016614 0ustar00palfreypalfreyMetadata-Version: 2.2 Name: tagpy Version: 2025.1 Summary: Python Bindings for TagLib Home-page: https://github.com/palfrey/tagpy Author: Tom Parker-Shemilt Author-email: palfrey@tevp.net License: MIT Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Operating System :: POSIX Classifier: Operating System :: Unix Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Multimedia :: Sound/Audio Classifier: Topic :: Multimedia :: Sound/Audio :: CD Audio :: CD Ripping Classifier: Topic :: Multimedia :: Sound/Audio :: Editors Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Utilities Requires-Python: >=3.9, <4 Description-Content-Type: text/markdown License-File: LICENSE Requires-Dist: packaging>=14.0 Dynamic: author Dynamic: author-email Dynamic: classifier Dynamic: description Dynamic: description-content-type Dynamic: home-page Dynamic: license Dynamic: requires-dist Dynamic: requires-python Dynamic: summary TagPy ===== [![PyPI](https://img.shields.io/pypi/v/tagpy)](https://pypi.org/project/tagpy/) [![Coverage Status](https://coveralls.io/repos/github/palfrey/tagpy/badge.svg)](https://coveralls.io/github/palfrey/tagpy) TagPy is a a set of Python bindings for [Scott Wheeler's TagLib](https://taglib.org/). It builds upon [Boost.Python](http://www.boost.org/libs/python/doc/), a wrapper generation library which is part of the [Boost set of C++ libraries](http://www.boost.org). Just like TagLib, TagPy can: - read and write ID3 tags of version 1 and 2, with many supported frame types for version 2 (in MPEG Layer 2 and MPEG Layer 3, FLAC and MPC), - access Xiph Comments in Ogg Vorbis Files and Ogg Flac Files, - access APE tags in Musepack and MP3 files. - access ID3 version 2 tags in WAV files All these have their own specific interfaces, but TagLib's generic tag reading and writing mechanism is also supported. You can find examples in the test/ directory. Installing TagPy ================ If you're lucky (Python 3.9-3.13 on x86 Linux currently), you can probably just run `pip install tagpy` which will use the precompiled wheels. If this fails due to compilation issues, you'll need to install some things first. * Debian: `apt-get install libboost-python-dev libtag1-dev` * Fedora: `dnf install boost-python3-devel taglib-devel` * Alpine 3.17: `apk add taglib-dev boost1.80-python3` (or another `boost*-python3` for other alpine versions) Other setups are not currently supported, but patches with CI checking for others are welcomed. TagPy works with - TagLib >=1.9 (all versions up to 2.0.2 currently tested) - Boost.Python 1.74 - gcc 10.2.1 Slightly older versions of gcc and Boost.Python should be fine, but the 1.9 requirement for TagLib is firm. Anything newer is probably ok, and please file bugs for anything that fails. Using TagPy =========== Using TagPy is as simple as this: >>> import tagpy >>> f = tagpy.FileRef("la.mp3") >>> f.tag().artist u'Andreas' The `test/` directory contains a few more examples. In general, TagPy duplicates the TagLib API, with a few notable exceptions: - Namespaces (i.e. Python modules) are spelled in lower case. For example, `TagLib::Ogg::Vorbis` is now `taglib.ogg.vorbis`. - Enumerations form their own scope and are not part of any enclosing class scope, if any. For example, the value `TagLib::String::UTF16BE` from the enum `TagLib::String::Type` is now `tagpy.StringType.UTF16BE`. - `TagLib::String` objects are mapped to and expected as Python unicode objects. - `TagLib::ByteVector` objects are mapped to regular Python string objects. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736809851.0 tagpy-2025.1/tagpy.egg-info/SOURCES.txt0000664000175000017500000000123714741316573017377 0ustar00palfreypalfreyLICENSE MANIFEST.in README.md pyproject.toml setup.py src/wrapper/basics.cpp src/wrapper/common.hpp src/wrapper/id3.cpp src/wrapper/rest.cpp tagpy/__init__.py tagpy/ape.py tagpy/flac.py tagpy/id3v1.py tagpy/id3v2.py tagpy/mp4.py tagpy/mpc.py tagpy/mpeg.py tagpy/wav.py tagpy.egg-info/PKG-INFO tagpy.egg-info/SOURCES.txt tagpy.egg-info/dependency_links.txt tagpy.egg-info/requires.txt tagpy.egg-info/top_level.txt tagpy/ogg/__init__.py tagpy/ogg/flac.py tagpy/ogg/vorbis.py test/__init__.py test/la.flac test/la.mp3 test/la.ogg test/tagrename test/test_deb-bug-438556.py test/test_framedumper.py test/test_mp4.py test/test_ogg.py test/test_tagprinter.py test/test_tagpy.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736809851.0 tagpy-2025.1/tagpy.egg-info/dependency_links.txt0000664000175000017500000000000114741316573021556 0ustar00palfreypalfrey ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736809851.0 tagpy-2025.1/tagpy.egg-info/requires.txt0000664000175000017500000000002014741316573020100 0ustar00palfreypalfreypackaging>=14.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736809851.0 tagpy-2025.1/tagpy.egg-info/top_level.txt0000664000175000017500000000001514741316573020236 0ustar00palfreypalfrey_tagpy tagpy ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1736809851.8696141 tagpy-2025.1/test/0000775000175000017500000000000014741316574013652 5ustar00palfreypalfrey././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736200594.0 tagpy-2025.1/test/__init__.py0000664000175000017500000000000014737050622015743 0ustar00palfreypalfrey././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643924906.0 tagpy-2025.1/test/la.flac0000644000175000017500000023474314177046652015110 0ustar00palfreypalfreyfLaC"  @ǞYٽ*t> H( reference libFLAC 1.1.2 20050205YnZEؑa tÒeh7οX|b=qssf^wk8΄)}dPtTRMx\V&Q$ (r,Hd!]\u>$C1zB+*vAeTN\g:~b1zTNQ=+^Zb{ 'r+n\a=RWd#(;˜w&8%"|&QSItF}fE#/iHSA;$c=ar[яx'eSw 罐n@!J vעm0:~o}m6vBW$dixi iR)0`@O]v*2e˖dlp. &^%Ep #=PqHgD*K@#&O83,g="oiEY؅m uD%0!8/(TNOr؎Nj Ws/&7QO\yy+{/e%iunbd2Fp_BCM/1vLt-ɨڔ qqKXKc~-*T~m Z DSS- 62'R YWFSU~*xߵH)nHTݛ'jrtF^8!m-ט*5'±d 7,eFИMd LAG kG|"3q3~ *\nXN ' ɉ,F2,t*)k\ig$lPtBaFBgAb2^R٩hƑgU?W=c BAyޣ6k2y)Dvҕ*ȷ/"fS[ڄؐ;\ȗ+3 ".jmHA3O|I BdQ-<`[Ȩ=v Qo.bN5=*g#}glG%gkJzK:<$2^ٜqW[edd%CbbE(c K#E"_S,oj  AJW4g  ń:i۰P|&0te9\<7)\.4f%G#7)I-)] gam)(Lv?+2]Ƞ\ 'ЫVcvwc m4CyD‹%KQ/elJlk>)׶Sbtd BԖ  ^7J Cv&5JJh3^bX f-/~uDW H߃b 22Np*tKJtD:y E6nPX`|LpUh$`6f/m8m圶E3"CrWV̸T`$*?īvxj$R)U%R 7Z,/P\oT{'D#&T_B&BXv]Yx{w*^;)ZMeҝ&[yύo҄z\\JYw-V#zZw*XSسj!m=7:;i0K> B+ߤ,r2д‰qv%O^(L*8RA ̓>N9foJwD7iaGeqm+FwE!r0΍f|hDW:L{[!IvG5){cx,l˾yL(ޚՔ2⴮fJb4D2Bolq|hlT$pS/+ḮjF H o &.KM&agm8j$(;KUGuҚayuGdz|D?&^|P>J9tsί~(48{?B`il+;@cgþ:J Ye YV5:r?J+j8c8~`[4y|l,mҴK߼kʋ^3]pQ[>z6NWVPSkjȝ>F^zg8^1E\Z$M啞/"`XJᮑS{WyFDͅY <bH)J^c4dj)vSDT ]<*W.]e(KKOid 1fHGmLPߍόefW9_ V~d/ߥ[gtIYA5r ;|﮽^&lٱ%CIvܴVn[H-[zD@VXօ|{Oɛ>p!CY؀o:RN Ba*214-ο3-岹"bĘѶfKs_ܭ:-L-+Y;NL'9:gep y(:*lIEo:e/MIJnvW,\i8Yf Aii>|ұgII9~EmNg> !nS?\Dt7'Oo7xf;7]> 6 DlcGM04nNFVKT^V(ƃߪmJ1Mu}uS3) κMX $. VifZ@9R%]:kއD[&Zk.vta}/ o1tpL~&yف6&9J?kme*5Z/ZVOCw!Uc4\cW\MA_8u"Lc 5˝6-zFdGеH%E+lkGktgbU& Dr\xY5mxؐ`TݴŭXJCH4L7vE!ۏ=|K+ 1_&I}y!_Oywr2L+8KP)?|}5R>OTVF켖?ewIf[]5b㋎y^ Rqz^>TTsAok$;G&e\.>r2Ugۣ>zћ4;,BG\;PLxVܕV'%ĈdgLƞKWJMONQю87p\TbNdܤ-U~4T@TVO4=3!Qϫ9"]oW4[y?=}ջ 3v|U\"pк,QXHi':c3)N$;gw%N^:$ *ΨsgvVv~IɎN{ ' +qf.O˞:stڵa'Z֊>iЛE5kTaa}~1ۦKdך5fkZF;* }]-^_^a+N(U]I0PpNH)2|xP@=zjVb&R^)7LCGWҝfdjb`D~PIF:I wE1IŸ;Kiq}N\^PlAn vȡ,}2ٿMޒjrF~]ftjej(TL9(vН<#eQa2]'oz[}g?6%[s5R<82s8oiQw9UsGv4Tكt5#\=ْ sK\h(|&"~ /9P>/O _EĬb!p{5WH~zΥR2*הؐ\Jkau"מfjbI-g%4浹`i@]᤬4Cc؃.ޘ^ֹt}cu)#ۢvԈrd`򎭝 +oFTvQN$wjzry|s36 "*CU6k=)0]Iw-)!bR*dRnR$nBZD"=9 gNp!аLTͅ]x0IJjd ĤF `mHf{wtT%4pyCE1WlLw>vGkާML]}SO*޸ΝJnR WI qc2eͫB:C&p;.ID=$ ϡ$dObt2CQƯ> JD}>Qu3:49s@fbj5!`u7&&V${t ^P?d^ҤLVV E,Bv$lq6fCEDv _$BrQ C骧$)hNTDZ۱oz)!ahu1[$ӟbNxUyTWDЎC[A$] ).VtVr-5[]64"%Hg,&wk*(%őzDLr'N'k"W7bT$R4钊>ӒS=(0dUcAqYf't?^:̭S|~)2fm31 ѐ(^%n`+#6 F(Ā% %dBb)'hu6;vfq$H#SO -A1GQeS 1}X2/tWbkCgwT'clW?imfr[c/'鱕XiSk]JJZhyp%YA|.]o"Jgu8'oq0\m[P"tЫ"()uʐv&*zHDLHu{r8R\hdjvt^ ^#CYb2/ F ڑɪXNxnEXUeEc57K/t>~@W!3 ̙26\BkbiHEqG!{aW/߉뾮,7YvO/UL{\=JʉiǶ&h x?[cކ{^v.W`ԈеDHN/OLF#Ĉ!4`V\kI F*p~*+82LbRhoݏ7xKU89%QgbhYm4TFꌃKԬh9[tF B% rb3ph-$f"veXimB{ݘzlָsu.OGr60A1ǗC5ʶsGZ]*U?'K٣_YYjvj+R$?StG0,@f HD MKBt㣢 X1e0@1nIUNsܳѿ7] ?_E&%2d(Qc)K9A%qz.ONT}eBE0>@aܩDφ=;:޷D>%I P>j',Q)]^ X2Pǃbשc6ۖbUfroCV4& /-eF2yIorы\DVa$|^K.6۫x)OaoI]t[R2g9$I Fk ?v$[ZkIGZ .@W@X6#(^R)&%!M!xeCbT(Usԙj^ӦQ}Kk]įZZ -pd-$XK΢E jmZ >2(\J|lY2 G:>>"kx(A>t\]R}-b|<5OHHj1n!:eҧxDe1$K4b=)6\[D Xb!H(\ZQY;gU)83a*4 @}j/m%6HJ?N-eԫHp@psa?8@?m˜x9m h-xZR$5Ą.N:n5Ů?>(KBwqUhBs4JTܧWq% XbaWC-WW-_ل0,@w I*I @ ο1: RBk:8_U,EgIGR_.(T 'C?V bX" @U +Bп~|uPQGH+B%-H'.Vps +UKlT2Q>5d\.5NblB]$^[`U 8'eh=)zu?cĚ:7U0ɽcmrc|yn. h BU;vp)I08 y)[Si&4N ?A5 RL&qEC1`ocqK?5M˛JFVC*/ATE TS"p Y%f(9d F+;y@g^IH*ѼqCW_I+M"~UV_+h${D湋)ƣjr *"" c zUa¢wpAM3mb{+/tU7©!B'*Sߢ!'Dq> bWZ!\B0Hނ)h1A3Va@I2".hCHK/KmY8sO,Кۻc'49eѴh-{S3c臍AG,=T뀟i':B^Ѹh~}A~+0 cBq. &?4l/~S9EX&M!RH'NQi (!(41T0D6SQBѴ=0,睱ţzsdt6]xKA thڍb1Ѵ=uʚ/F}Hg`Q䧺 .7;o BXP7N&rs*'XF:"0$~@9쒡YP Oz1.xQB4w[E!D+ш1}E"(A$G A[̀ _R6I!4ߪ"h]OI&j5Ah 5\RNnբ %!I+&̬!`SfVǖ =jk;Y 7O)7Ɍ*˩`jOȂR)%WS#qTȊ誘nC>m/ ,XkB)uk>IDQ0i M B tyt26Ɓݾ5ЬɞzĶb2ঝ2t8Ah!FQB,DKFY .tB@lag* .$;t2QvtrufMLEN8)X:"Q)OF0p4֨m)էȼ_:ܟ7I%DQ j( w#Kq[]bBj~i b,"Mm5~mW,Wʼnf_hS^{BPm)D2wOėȾr"/dNW"o浥h1rou o]L=f@H~/ݼȍ$#y4ߑۂWV\A~4")hgae\lUmz<[U04v6t{i& trc]vI_bc%:z8X6F-U%J!Rc{U],C"VrK; j>c*?9׍^눛iz ~Z"'&on?!vCSN5yҔAv*k:RfD2+C\ xjm\yxo>l%gj&TB6-Sn[y2yT8{z`|кT'_]uP;А>|X !)ՇftB*M+SJ0E.Aمd@OrC4 GA丢~(XqDv(c  $hdIsBpW\ 5f;XČOTv\3ei װAAHG&wKI(|^y=KyQ7tpk4,Z1!K_tQpq $ؖ␛TP_$H'w+o*yytNBvE7d< d?)(?N A.25*?Ekw"֥̉M.w5Mg2)jOY?-Dآ{Fycm{å![zLu5joeDe2^9k]sT*K-uWOi.t0p:R iﭗMܫ $);: G~ABV MV2}vG rPMUeۿQoAWS}RfN _f~ށR$8RaDO+%P[=kHl;XI*/(_)͢P'Ռ]Zj'b+ .VL}SY&Y!tJ?u}2tkdu!0,i9!?U 74@iIVujk@HH U=RX+fήJoo{v0]Y+Zi?HHm vގ+4־4*yiڝh0p)\$PuP6D0Sokhw&:db(XFhO9K/R\ V 7ZPrPWĉp# ,&:]ElmIHH'a 7P<ly0 8 a Q0(l'93M\ 棩B{Mf3%<£D# (%mu-z3)a,/͞LKSlC( eCa?7%)/kF5 iK$ gGz{Tl/eQI\Uq.fq5RhgWT Y ʑ/ :d6R g!m@@Y/PL8?I-u{US{I% aX4QJӴw# i>z:JaJ&me[%eu;lRHS9rK5dVN̉x~.-B+r#/{j3H!3UiӴPeu5>`xtT!8!U}?tUMe&7'V{Lޯ\T#v :=JgEiFJ%fE*WEԿKq]hkh?i\y0~Yd狩B,l^"B"V3wK/:+\b\Kh`8SIl͒d/ 3рr4h_ϔiiy]斲y5Y5^ו ;#u"wRO \cʶ"an23k> e} >|e2))tsUh=YEJo:KIS'q'z5\7{T7a~],#eô}.,Wz>{wb2=??gD+3膜AM?Dkp #jbć6!Sto~NґL3q~n? A,8[SO9,*i{Q;9peou3tL.C%C!#qu2f(Ϧ9IsY]ˢ)U/z)f{"+ɰX',c != 7۠ܕHq8(ێ-JdN  7Qj(Joa?[E^3hD.F)TBLr`OwKH9G5IE}bmXbF֞*`T=(n!W=c]MK _kDޘmZy4fۋd-H},LbĨj٫sD:DK9ԏX" BDsM & B* J4rԶd孳b3qfM+c⇙-8DS!,gLGŞn J5H[#Wx;,˕m1oEu NP6*EEƪ2HWևN_SUL HR12q `d,LF/ZT.4,z\&6 j VZyM6HMR*Ej{sdCNŠ :zzEzToSJ64毣ҕ4 JB`G$~Mm /E,.6L$mlHPq\7Չ>f ehhd|GЊL|Oypo-0kF,]faj_%"s%7"~7Wl ["sֹ\h59Ho 8'vMl)Z nCy&^zI:{i"5iFͿI%<9t3:J/[+SJ>|!mi7C0uL<"]ӧz͖r}(Mv~"/x&قb-$K[IQj@"&3.W(DH~{ ᦶ#M76>Iycy&C܅K9Z BXC# ))WZ%ZW)o֨Z%`+֔ɜIvLly Zl(:r bKo+&:O_kuK1p,HGZNWSK^BIMHQ?MT1KՃ'f#\ӈ6&^+vKP7S%89Jy!VίymCztIegsvUcO.<>>Lsf(#o"Q C +s1of^dKN2%"-|T=+& 'ݕ3م߾`͠pfd֥,/JQ`-=zT6 QjD'`;#9U:seqoُm˃Sfd|5)UErF% &c uXFb+]M+Tr׌z&%M.;NX_٬!0%bW;#hW腔#OzI )52F RoQ KI;'=]ሙ01u$>'T| [fs0H/nU('F\rl(?RVYM_9;;}vkvg-mX1j%^HX9y%YDl())5#m8! TG*Mx2_UDN>&29௧'D/[C!K6`)mID(@tm"2-[ҍ,wΡ(GgFwM9 2{E0vyQmHkDlS,Di&h)"8}8^-#<7U6e)d2+b/ZY_|]O&ͅo ÈܩZj-*JW;R6ypaB12{ {ڦ]A4 -KXdWY(Ԑ#ȟ__"[SѫTUy&92Ş>F: OLH; ?ֆK&ba─ &%JXWnnk5⮱SWҒ@r-2eDe6GNBOf0IBU"c'~ΒYEDk 1]4u!^LBrb*L:U4auv䶾_Ԯd$o#mᮈ S[8s̷G3`ȧFtLI)9mqKO;q/%i{ަ:tLV ޭsfi:%/ӥ:!q'&-pmkz6xO9Oބ%Qy2|p4g{?\x"f/S }=Y(K* "B ,\ '^kK<@;X>QC#R8IEyM0ϖ[!V\ ?k7 J!nivUA@Od{R4ڝhqAKtcڭE R37 qBA('a(k7$!͊(( `W3JD)kQc((µ9fELs|XO~D!}d*#|KR-Z( ݘ>fz[G9aБN8*䷤*.$3*iSoଇc(P^xN}N ؋^V>WM{iweQ1ȿ\gMXIro=m^If3%&˫ĐEשG)TZ"I'lNqb]DpS x?Ib dm4k"Ah鬓u ii6uRaGu8}n.EjhT<˔*{Φa)טۏil*gL'zcb o7Y|$#F.Hj7sp.[ h?e[pT4Q6nαRy%P򄼱*#04+“s " J8"*)v/C\y;Ƀ-DIrp\$# ]mx,[2):n p1NbK'#DAYjdSK])RUoD5Fo0_# I<$MZRThYBµ_dfzvTZ_2Q?89XSZd*yS˲ ㄋ糊A!N]Ų^z.//ASR';±+l5@JMl!rLm#1$!XNyZL)4Bpeҳш%;"J ff. H"n ~y͘NU9 $[6FfZH8V,uE:].ڱ,A)xGNg,R ZO!I i/iUkiirش{+8pg \$Z8RV⚎ D6m4"hO6] ]h>"ظ6f1|#~]YjgYYoDh(o1zOICN`pM=ʤ ~ey^nW9 ̝a_M:鈦ϱR{s]tk> {+nacA(XºsT'(av%P2F(EzRľgdJ3,~9:sIItܲbTyri܉9PUnW@|xc#ИF~JUqŠEmԨp_,sʛ{_oSW>MP UwWٱqگsdWs~=H[j)ɺa*ulji-r$`es*#_s[(y'7}=fQ;dc=&S9`wO$Dpdt1D#/*1kr,ke(SVP'̈ET=BxnK& Od`p'&ĤY,/ ޞ1{9LFjz5Όd}= zoǴnlg:X*4K #.WL "Vwe*HBO!nuζy՗;aE1>e\72dARa!I Br7sӽsMsTJʛE8&khj<*M#PDuOo(i*.ؔ5ZΒe5A D߀BBc\H +Ϛ!덄׊U鈺}4 Db԰ 7" 2" a3Dh[;t> \qɘ">kJO5-GDU~#'h>#'*Ŧ-ΑI.)v3 zj}Tl-gSRsNْ @&U2]5%'LMoZ"Qw)|X4 l9%5B:F阂UMmec_wjbX4#X^e)"̦`"q@w`iyOI3nH E? {a,T[\#9^\oFNq-tHz޶qu=[v'E4Ϟ;%1U:iVa˩TdYNH,t|\\ F,LbʚrUR}AhK~p?Tϔ+!lG$@3M!NHhaѴ` >ќ"t61UˋE9ҙ*t3FR'HRP_g= ӱfkP}9͖P&}j*1NX%&R{,|P<^4ц\vLRfrl]ϗ%^G[T*WE7e- z>IwFP8Xtr&^a {Ж̕@N+gcS\91ݓ[MIc6{sQQxhH%2Ȕ# ʲ' bA0I;X»`XCK:*$7 h_۱3MHM (ߧQ\a ,Tߒ&K^je.H:D]UZ}oI?V<ʞoE'PZM&Y-KJyJˮ(ii;eWVCȘ/77{ UYr4s\k2t}RO.śu*%_ĻȎ?8i]wGԎBZXJ-T?5~KMA±I(]Qs#D]vnsa%gLu')X\P#SXu LH);]n-#J\* I&gk4.BJd{SM)g, bh&s3 mNUwH1Dymc!xyjٴgc|iw(g4O BD1"qJr\ 85\Uo WVo)3NC[bFX΢${2]M )Z:Jv~2ϣ)tAh6$U bO.-8Lv%=64<2r(>qbv8|k ش0 #,K ehMbdD!tƩ{.ϫ~uL$:h=mUY*Wjԁ#N[o_{jYl9*$nUx rmɈWMѻꭒDžTd{S4ݾ1QhI+OMjO~w.Z =2L Q2F&i*LFTr"0 REw -WA7 }Úo!*br(7-6z;vjJ6s(6|#TWS-K.eR$v6>huۍmCiU[Bӡr)'{]Z {ڽ>km ɴ#m!]3/}RO'20{NR;0Q%%1Vse{[ۡPj"Zi. ~9c}R7#/U/ >Hk1>ji#F3%`e0zT,Ǽ!tq c;M;?C-؟j,lJEҫ"9pC} H"08CaĆSUB8T!2* ^"3 -*Y?*dn4d22#%j(˰sv27)N*hc_*6G ϻ'AzςrRHu!iWuU˶c+{QN[ԒLR h0[]|bNn}lqjI)'x$. k1t1H+ ahB `'єTP[ðaespA)Z ^1) -ӤXc,ES<*n;$X"&]MS!ħSbXQM#fi2*"@0#M̶uN M1%?]Ց(^7]V)6~y O>--6cKR"K27&bv{lf4s|>X^8 <G18VC*@.x (]#r3wY#5M0馎%PWҤ7I:V 縱3OQ q AWc4=u"ȇW&RM8kbR&wRB! 2 -MҔP:DtߵL4 'ibJ;S99f.K=H*\14CdSĠ@6#%֋'؎) ! Z c`DX2 [ MEGj6Hlv Q71&!>K $WRS̥i^wϱ~ؕ({8$BCn)]9udtnJ3ꔯhЌ"dx)+S>f( Ҭ0{6ZٯխCDg$e{̈aiYsƟO=杜߸L3,כdᴪ"3GSrymeK I>@{\s3o(4Ő)PC;H nSt@)q*S DhqylG)C'Xy•+Hl2|e&V!:Gjq(7,:۪봃C.Ӛv.|},Y'o pe.|u' HDbh 5YKvs QR]`dςL%& NmJH  ^& );>KpdNF2SR:/SPK%娬dOe嚉OW2) LAl&@Mf!rpFz b;B@XTD`ig4vcEܧiDB, ? ~uH`ZTfuljPP5I"wPβҳ^pF^Hə*{|w,( Hĝ}KR"0-;#=UsK&,s3UEng1ϻeaFWT'*5`INZ3JϞPvRekJB Z`QJ!OF*D L<+ L_ /x@[iᚢ9>ۍf _ndGD Q /1))x[aL%g7Ztw5NʅNDQaeC5bxp.$2\h5[,IKG1$&9/ۜ@j5後v)}R_zCAH/W^ZGM*.@{{<3QnЉP"!`O3ZGpMM"C4&d4M"H[}M|u#lra@mpW],P9('5~NZ8UA%ktN8 Byv#:Z;nO٧aY c'q3] Bf)cUmi[٪’PA.[T3#җi*loglVOOl(? (Ȗ`1 ])(+LMCa`!FOލ! Dr"ߜs龨c |#WtHNz a^lM5A4sdGL?//d3YJ[_  ۽EM#7 >2/mg;HW%m6ݹz U,mо2vH"Ez JE?Y N\bu~SIM ^#>VC$[kF"DÓi;;|S-&N: ~#0J][A"M8 =q*&`)xp+3mތ<"PDB 3uȓhacksFucƲŧ{3]FZ8o.W>|jX :m&o%{4m&jTL - D(jJ6T wOIm Q[tbsoŸ:g@VJo8>5:Hπ,_Dg%EDXlCpNI[`1Ղ%{_-Pk*sٸ%7'nD>|“`A' =7v:Fmi*(ǗFE{zW7RRhf =;hhPO0͈"{UǁI/\Y#(tW\t5fjmI^`u h}9(鎈KϢS'&j;<]Vg%e)[iNl703D$*2[ &A=J38I/ | wWYZfWXJyoz!Tʀ:2<,TO M(M(4UMO^7 (Ȱg ;aԨUbE3e,0)BAۺ١lrUQ>; tL#QM$0^n\{T¦;k5{WADxGݡѥshK1*3 tF8? B)26\woPJ^Cr";1 <6b*@<$ =T'W7"dv u۩6>[mrP)B%Κm5^WӇ'P| Ǣ=I)h}hvF BͿKK[2$*dxU}:s[4LAp׮8sVp~tQB*D%nY<3NmXPbX|]r+0EHYpGS$t(BcDJƒhp9b(N2k5ӖZ6fDŪ)`173tn5Gh4hzUn&JkGKHS)rȡY[!r{V^5gFoG6"[Ϻ)O#/'h͢%HNFӖ 3XE-'#0G0Tmv*L,( {!7*O$ j3hVDAu.BLHt$)dk7ݺ6Pkfk=bÌMHjə W+ďJi;Sq5f{Dq5i[ojnIO>'5ilTǫ@8՘kDP*DeD8D~T'Wչ{ 7yO5z=S?oKڦ&JgӭWRHNI#IR!pWu *tg zkG X~v?i T9Ll@!rhYR< tzfA*^ WJ+pd 1̀,w{)L!n7'.+̀>b)C# uw+)?:2鮬m8Zѐ$}IbdҏڍĝN7XӰܩ5d# E ӕlUZzZVdfZGad$qݎfbԼ'Caؐ*J&&#įש@vB GD[٭ay  }cb8w/^}䋋V/vWnP({v$# )Y]lG_ԺmX+?Y(} ט!)i_, I1Jb mZs=$$2y2!VX%s#Zʊ H}: 39 y+>wkVƞr"G_8 Hf juA_p$Hn,X߹'!]Xt~nJ&$Nq 5UcQc蛴@I&sN~WڱMO'\?bc[o,!Utkfy8*vxue7>!NU2Vn9YuU]%:J'_+1'ՖO*yvxPhN*fPe2ZLBEQRF5"|MtlBr,Q^;ɘvrB6Hɯԏέڢw aJծ.Ԍ-T(F"(YD;ij}4h-uQH 8-K M5Nb}ԋ+J~"(6f"GҎ;OL~_XP TbCڦ SBZoW#W#jK#>sh,Wr^VtrߞT"x(YoF"\#}q\88]rGߓH!0z3OߔCd&6[=#+uA+"+Sw2TY>; [d1~GE~cՍ2AyaT0v)md–>[EÄwr@.#ȶ3P©!KVgiWQ(хpm d%_ޞv ci<=++ О! $"6 u6 *ZFrxo DwM0Lo5.~gx]iTb< zX1g3/Oɭ=X2 gU0AOΛ?Lbx(ءH KJ#\^pX>6afSStogPQ`Sɠ؜Irm4&WS!LJc1 vQ: #!Pˣ%;o4p=Pkb$b1@U\Y`1@Ux-j0Oб>NǡJk69]*6d?{^yusllO/MFܩȵɍMrIz{VNxHmyx؝CeӨYC5qbVWm ad*'(^s@udžYwEY<1WV6B2}^;Ą‰j.XzX.P$(Ɋ/8*xwAA?֛=Nm{~:\XʰQʉ.CMs |Lqq1MQMLg#w>Ь,︺U}n+lrꢯ\U_}M~å@{]3gQ\kVO=QumTZ wvBI?HFM\=~4M,QNcA^EڡP1b i>? B1iJ曻YMلC6FI?$h*Qi}Jk3m,EoOi.$,&ùVD"\.}O#8y *CMt!7.PIpRb+dy7\2, IV9^㟞I Ne.ۑ uᓵkdur.bZ\>I# lZԞͺq$a]K_|DܦÝ@P2)+&HT&TYS h΢FEOfE9P(=jiRev5(֡yqUExbDmc&O|{$RΒ s?Hj,U!GH?~N,BT~r|2N P;G7ԆjQnF ')P_fE{)H܏^9Hw~ǿjoλZh'v5|E/!o뒤^mWM)5@չ![cS4e Wy ȘKa빖wb CtB YYTS;mRv?hb7TU fc@~R۸{<)8#u ?5Ev_z|ۈ[af+Sn w(ZzAR)  # Ygy٫IqGY9|޽ƂFn:zxCf*Wyyy9OO}7Yɏs]d/oܮnyl$Kgƽ aTK~RIݾT飅ٸC0}Wdqd؟}0HE;Jr*Sy,'ȇ5>QP󮤌$˗3aa9j-ˏ:H RC"CrE!ұqw Lkxje+KQ *_/H{ۓWY2kQ.]EӽVZ0J4;ٔ) rCL2SħF-aV2\T瞯A!".ydk5\Ft&^C%y5Guϝ̥҃E.K&3(C( A2?ҥd@7}.D&I VW(ZMfL9_E ^Q@d;n&IXNn|7smMl"H%RGxTrKgiP%2O$jP_3^iYZfOwl wBв_(ҨJ҂zbY]o-FJSًW ޮ\=[v]gы tUci=/o# &m$x&ɓxKw[I:w68~F [Z6 _nFJd:Uq@ )e6Ab\B>U1zh$R b`.QJ6&I峏z#&5Q6_##'ԋ6BNO2nBߜz'NLvml{'9,JVp`1ܳsj,ẚZs!UCu&Drnc$pbC]"j$ɒ\Rm5tzDڱS*C6*p+ٴ8Ypx>eD$Tug>"EMqOW0,x;yfYZګr Bm5cEK+ݍF\rAV_Qy'>o$X/zgY`RBĹ+v2i)@V+㬠M_ wV3]iT2* ٥"'eEuYml@ȔzjH|M uwJX tI)dWq9 |R4iϗGA¢1Gs/;\YbaLs2YyF+E^(["W2[fսHc' !wE$I;ȓBI)Tg*c7A(YQ4dN,zOƕ8iq j"S^@˥*G& {bQe(;ZcqPIBUvC2#CX%RـFȋ(Y}Bh,YCPrP8iQ& nm6 ϾsO\S^F1}+nCu Ӊ\mx1Q%)ZàGI,<&3'Pžߍ\ <; [m~DBb@k2cq) JO". (%=HI-xn]tI(8c#Q;"~x3goApK]\Ho E[ !_5 gaL\ [ѤbrЙ: 84G d`3{Ѥp{tO)U~|#H,zS$4n ԼFL,^'K. ynS-) R dEi>`E#x̟,@ 9e5]Ja]LU-J![H̥&8^$d/eFx!D&7A34{|RƓRTQB (o!>=UF )W:[b)s$U'ļfE/DAA>;/Vq1.;b̈Ox-Z#NFqiİSV:.JkB ,"0R{VȾVm΋ H#ɳ-(,RḡY) כ-h7F2m$J0 KBXM v׷MoOdU7 LKwLu$+h:B'OQ N9"%mı9E" &mAT|a#woR%νHv!Q3 4?^ꧢES\o]n yƞXiW.=E2]unΰGi%psnoAuB/AӛdJTi>RI](t; BFdeE>o|/HBj5&T'=n[,BJyC +UIb{fdշmXQ* LRwcԸ)3eEcPn|lP )v|N1L9JHvz_*fr {ˆN`24DQ% }oT#99]Q=1 C@/ 0>dos/*"F*E69M|aefw_Q >n%9D탻J|Wϝ;wF۩I%Sو򆲰[nv0\*ѸHa'ηhw`qh69t]G`XAp3KUa*z:i^JªE(+܋o_=1$A`1jp to߉?DYK<(;(7gNP/& yH @  oKֆvb2 c>(c<,RD'B:ŵIwЊ|w_[B;Ȗ KD$s5)2yˠ" @R1d.pLT)@S]h'0POw320Hylm3cH cԢQ._7ALZ /?6C!d)5'15Lm͗b)LH ,)~=:t.CɩI °@#z s$Y<yA8T* F戻E -nI}y[seIaw/{A$3%X72IWz b1< A~Ji.MD*pa943y R\"y]t MS s5A5]#ePZJ)D,8R4 ƒb$(|xNPRUmzZ)g F[J_H SJFB%y,օyR;uSN܇%ҴZE$R }} jIU-DR7D ,Z(h$aofi")anc%81!#}('7WZ:8U8,:"΍%HFChF4F> g$FtJȯM~S&p)SyqrR4vA䡹*Íet~[s'-_Xoا1LA@Fx.ugCȢ Yu5 /+0<`|`\^-!n#(6n;mD(:Ҥ;M#&q+{T };VTDֳrS XNG(r) u" 8IGAm99d7x\3jYK¸M>BM>V3%0qL( +Hp973 ZJ՗%lx֞~ky^Xv:QlL!kdjGp 5+ސ H7tP݈(@ * #$ D"FuqLk;Nn_๪Nͩ_p"p^|tp~::qj&9S_Oid[*$I- (opzbo.3:7䌋BA5_%غ ºdIRi0"M!HAb~ al`D! K!6ەMZ$pc8ޭMcMKHLj;vŤ.5|™PsR!㗶!B'I(弟 b CTUqYNm~0^7-WDC)ގP拧׀)g/ 32NF@ '.RnBXG2d!覠]N06CNchvx%{fc,OiR #2= \{LV[Hb Uă3) IpHo,0mNC)Yd=Dyn)4'U lܻσT[-zjBZVy;Éޤ_PT)ύ8( Cvx8*E6 a.Wa+,)R) pqq7&^܈V1ΫDQZ jC M+&Q+$a<AfZlq&5kF!"0p dh9O2ә8{&}.Fs1 Ă c_ FYRז4 F7'oћtpTW3#AE}T8ʐ vpU9zĮfh=%830!Y5f(7S! E&-8; *|ؖVsJa 8Yܘc9 #s(EȀo/oèpPlw U:m+YiS蓊/UyR}P,LFI8:wu̺T(Bb,V7u!8͏w4`GYRmF.U H/­!OR%a_"KeK#RPhTbLb5} >E2-B|-ۤ 6ݒfŽǞ,GŲ\4*$8n(cU l^^vV3J"y:H>Hrζ簫nKeCD_/؛t_d=vq?(Iu,u$ײe^&)e͎Ok:C1+rvG?fR07VC/ƁleevITX#* LSŀ 0!I/ OsQPC0ApNM3_57$"aY C'sڍx4( P{ fL$MA8θ='ȡW&"[4ܗ8{MʼF;D$$V$]k$"]p$ʞE33b!̐GRT`p60oe 8X5wG\C4#"ɑ\BKl&9 '` A\BߎoМ"9(Cy0Idv<{@(,X9T3 cBmU_(>V/P1_~ʸ9L,5sO3PԚxIK9y(dU-``opf#֌qQY@4 _%!b8g)H96^#R FL%,EAhETHgF,:J~AB<'#qܝKsm+m?k&ۻ*n5Bcm*$|0(imY$^Q3djjc‰N%D`2xd-+1sA13}[k$@Kqr|I o`A \C!n9U<6"ngx`h"0ZH۵s&#EKy/eVەHA+v#tI䝬G6kBM١FJ2Se<_!R3V:׵: u݇#%ЧFu%w]->M +^uvUJ-b0ڬAF^5F4J*v_哯 w0K+3D<Z4pPI554)1$5dۣЇaQeib!\N?/ [d{VAMڪ:s8A;̖fRU̠bk*#KRf5Ypb: ύK|?ş ;5@. i>d7&u)/Eg`A(O}|s%H +/ĤH%z(12\A' jiGx|+~ŗ'Aj8/BD THG1X9uS\XWs)R6HZ'}0#6ȗe8v/ЍX2b 슨>[ A 2 Qn`%yQZakB<`0MtOB3z0",Y^!a ͠[ A7CnCBVZ@RVBׅPBQR{f;ω3F7)C` (odo/[&p{IEiև좱)6,O@VU6 `C#B5&gڱgXG!_6$ÈL6!̨sBYkeI\7HwqgE2 Nreb~s7CDiǹxrқXvPR1׎"e'Y[yqW1Z>q92Itr!ɘ@oҚ5SHEM';-Aɤ-%?͹uFVh^hI$MDl g>"9/D=])%fsCNGO)6qV;u=?_EoDbMru2F9tC"+iLcK7:q1 T&l\_YR9qR< Y+Q;o.~.5i* ϟF^e2;`JC}xG$xx'qj֜a?kgvc_i<>SވdV~{=?2;b^=(rNKNqms X@-0S9BpKL e9,2SHW]9!} DPKz Id70WZ4?Ѡ1*fd( 2 ](3'c+] o(շrbQ_Њh@+<6P_ILm܇ZKwxP݈r ޕKų1ոqu]) :ѡ,[Ƴ_BCbʙ&"]Ҝ{bI Ph@wѹA!ʰ=h責Wv\dE5LI! cUfGU$Ѻ(D(>#=IX$f̀Y  m (8Jֺc*Xr%Ֆ_J͊pP*Z čMN" ඘ʣ Ǹ-dL/hEo1}|@R@G,L0@"\4*S x)@jK&lw=ig`L )gbiT$k*^qU/$4 l DArlP*3.UH#r"UBM ,ܱ[4D)% ґ@oEs"w7.FBPr N47,2u# B YF}C* p N(/(.bx8,'M:=T;Ȏ&b9f W,W ȿRw.#X6ǫ4z+Ȏ5]0(?AH X3;B. D`% ̡]ebuJ&*w"C+M-iD֪2q=>\x 1*PdMgB!7^Dot.!4͡l7- Nr6aHBN:@1%NvC.F~dt%4m/܃^SK$9rfra$ W4׼}H0B@1!x'|XV"# j" y{Sk;.肈A-#bI D*INAHt}NJhZ6o!sÍINoe+^-.:tul-e$>'"6aq߼OS>v; < D `VGɕ'_~'7=. >I^g8q@$$Q5%/Mg+[WNDāI35 & 5?!3q8HwLU3uY%n<ZW$4d9[CH6"\r7gz˱P}}X#`NUVkXn[i]T!|$V I1iH\? AfA8A*djo)sQ_vf1"ۛda&uxZR2COc527j:| qᐑ̠:L!df`~)9Pο8%Š=f^M#gm:k& b*aD]{('EήhW#N+5)?"+ڳu$~:Su C[;`(5Xw!uQ$).Pd2epu0pA䮀t=%C-U;uW-$%vQ>~"4A?lHAÁx܄3*j-XN%Q kG++\a߇mzuTa:ViڣNehs(㉏GK6Thwj䐐!KVR)C!G~(+ f;6ZLw6y>AS !0@7f>%DFf}6A A6 bgavvW'Q-QF_اHqt1|鬴EN؎Wmid=a4|CqD#Lƣ0r6zj> T"C%4|ePH+mSë́rEHlT_5׀2Ag? g'Vϒ ʒHA=n-K:,;NhEaܷ_ĐIadu 0IE񨸎="X&` ?`(\i (Val2;ȭT%YY01Y.oU|ETQc^VKoJGq'ۭ$OUu2G5l"zZWBvDw%B`1 C0+:+Mq\^Jq_l{3gTŋq83W,+ +dS9ŵS2B1L4²P5-YЈ"œs`G-un܃u~goIƓ-99&G!qz4ʔ΂K'4UpΥ;܊Ŧ"LbBǂshH4y-Ry lC8 #on9 [꧎v@5a<:iCZFyLKGyqQe֎J[:dҷ%_ɾU_tE) MDl(r ( шH34Nt5^"uA/zO8{xEVyZ4o譙2:-( i|-7H.F9=*"q7k/L9! e1w+xj . vv8=_(v8R0{y.'Z[Re6?)#HJƖzHg{!h]2XckL^TcCS#Q|/ }pk2E/OqNLOz!'^SR-̨??)+ )]M)LaQ\r"a$w>Dxxh][y`Id(Hakp cx7`p/xŻyP  6 x1wM._4?"D1ÈuSjbtU>K.4 2pIf6L㹬,s b9wy_WZAN>pU4D"y̶3D\ukJƃkq.Y*+37DB҉y~/ZR[H9wL| ^na?  sy2pBhAL nCʢ#(<;&Y9+Ùb JUdjl-(+kCMzIHe\\0<*Nt]Jexʚ:~kh+SFUT#`T=v4i&/`7;ꅞ3?cܹc }$ko,,:#Up lgM^eT4M$J!̒dYxEc\oVVdNNKkͯuu4kLdd8Ԟmӹ|Qc!|GES+RpTdnLqP&=|T@02H6W+^jK'"ϹJc[z$K[k2EigcH:Z1U &fjM" +WNٟ1UImn)ꛙt?J/.jI[w𨺬dxk?\TH5+:rIF޲Y#*I*+uNҜKX`#E-ќG/#)sjh|" Hʱt~$'R> _/3c f>hymt{PB1ɬ"p?YL1`pl˩8 ,&Rp< 0d"`B(*Όa m0@%v$Mo`>SS/I03L#\Kg}ˆ)!!n1pLyoD'`N@?|lKi_I`E hߗmW:31ßWy`ZH3Nrp@0mBO~Mp*20)`U@k/y;PJӬ?Đ&JiJiz9]!ΣͻTg!Gv}ϻNdq >|5?E+4SeF 9#L%5j4-Mk7l/I 7N)T:{6=dg_sIjk%qH9%kE;3Њ3KƼY`!;IoMU븥NMOҠuZ^j`h7ּ+TIHKQtQN65tEңSi׎dRsV; yk'vh<z=DP2Y  -wk ;imk4D>+ۋc(IZvQWk*&KXLPjT_eoCQe{ +HK?|6*ԊW?d-hM)t'j(#`Sl}e3r=0FK̶b{eC5'OW.-3 >HVI!5gMX~ltW9\ʥv1s8$+yŮM$/Ii\ɄѤ"w%) oR&88^hΎd (~RmqB !'$ԶIH/'., 2c>bZ[u־[(]J [5ZGjx-5MúrO#7:'s-Zy5`h;[ƌbOAg#hR5.j ?uѱ0n1HMnìeG^I!`Sh6ݰAKZrzt!9ľ˰Z6қz݌#zysIW]vu6lOB[= ¤]EETF/sTKT"]$]`OMXC!;֎i( L̖vDEJ kKv7*ƚ62&ݽGILI& 8B]sLO{B^œ!0-D0g"5$8^-M|(=XeUG]0T U*-GcQqN%#`venCfi#[lJ~-9YL<-+2?3?n9asTy%ք{yިXt]jp%3K2ytd,Z 4ND!Mlp[L:NaLg`+([DR!cuBa74:urqQ)LU](v۔w쨥NNܯ沤⃑R5щK+a7^Աn\(k+=T?ɉk;UPO/u U +NFϵjL[HV:, SQ%qRC,&DdL/Jv݈%VcyՇ]EJGJAJʁ^21=OKf&Y«Nn+OB""bB$imjHn>' 1ȫwja)IycyG:åݩ_YOUG ֘F$sy 1tuyqC1C$v?4T=P#.9+ tb9Kkd.>=VO+ok/O\oߧK+w/[kd@Hc@ME|qxx &P-˺ɲ\-u`q$le&AXm-NhFEGMCv2LErE;IJ}x$ܨz_r޺iqĚyk<ח-f> ͯSH1ז}vJɎ:d WįFLL*ϫe1l,tK冃ˣA1~绞}coWfLq'd5͛AcqX:Z]4݄equ/hϘ UIbZ: jsi&l+kW5G2n޼6Uؿxl)*GK!* D a@U)LJCϓBH2#u܇]+N7h$ #%p_5ZhTU+4hTBmEx&Պt$S$U߳~]oĒ*A|ЗߟĥOAc!ɮ:Hjdoo%uBQr[0>eN)k>]i<r㼸?Zg=*4b;E[k$Ɉ@FԚъYo,wv6Jۑ&BOe$pY1*k{!g5;`}PqCO^n HGV䩥iknH; zVA|#b rxnRJ\t\K.j*Vbn M;#]KTJaOpG*] AEb4-!k2j/^el&SP)KEb-Y n*^ |U2*G_qJLRy*@Sɕ#LPU<~$^4JBeC`2=]+ꮾBߢEy ȍD^kQ(+([;t%\$ `_䢕G BFNo9~iՑõ/\f /DQ/ڄoXʍev&ZaxCء %$`Xho7:U4SyIn-Hu ԥ*rHZeg)~ H~ OÅwI]8U ]Bum?PqsmoϒIϝ։at\)ZvڤJLRj- Y<~wW}0+\IEZ)o"d8A=a [nKԅzGRCmؔ{]ϳ")48i= 5e>gb^F>reTWaq$f,vJs5fL,HoSJA 3ZUS-44ʸEgL"=)˱We7`N>ѕW,EʶZr?4_jUn"\I^a~Rga=NM-n;R`~9"eY9UGH:6X#/EKB]LL9mJJꑷٕ\ErGSŦW5-9oqO6͢rQGX^̜KN.bm6~Q*me5_2̄q3G^B'S|[ K#;OovccY8cĩ43zG1x"V Erir|GyGR?Ѕ;S|EK"դ&R1aҢvRD- QHy8umռEBUZ 6\cЧa&1l$ߺ۽G\,񶏶V{߳r[S)m ~|G-H~ZUs0I!`R b޼P=|B2iQjFB_$P;Rk6XUuj@TO^mdB"Z(BNǭ5V^ڠ#wK|L~{Ji٬/ȴf"[^( d:)lV^YO}7mSLP>DqWĕ3feD_cDyFɥV=J8r֕BD!B ߄v+eQk]IIа۝N?UEy!޺A3߂Ԃ={n% %w3e zYJt%'Pqk&HvObN쯾$m6[!~J#u ~ ):jzBIg-a]OV.fRHٜ=9ء3@yxK&Jڛg$7u1ф|ŝp1l3?%M˨^ȽIS#=)QfV0p,z^R,`'uY$b܂Qռx ,Op+RV@?]/fED s/ З҉h nU AuLdBIҡN>=<ǟsva@mv,2 vQk|=Kh#K[%s#TwWcFK[d~СUJ#Fb)Ix3xYyJloXŞ~s?vw1"@VU)ilJ<)~' Z3UP6۔+F9~;SE?AE1ۓq`JeOu.豻yUd%$O(;ZWGfrqj@g]Yz ̚Y;}=FJȲ`3=+RA?CFR FZq,Sy'-A~9 l'.2cfAE  o8KWF;fM8a}QyԈ p٬EwihR61kԃP ϧ_rB%#=ϋJ`|:Rf vޘS}.rV}V~Ќ*+YITrM8m>hHYeުEe!0?5|; 4/{9-)C]܉C?)+^JR$di$ϛ)}+ t"vVSʺWwQL—Y(\X#UfIxxY $r2)ww&X^aL U )%܃SR6sz7FŞWj߇GfRmHV%JS^mOhGK$97w>-ɫMZn5c7%T@X2WyWtBUA4)psl&->O+UWϿ(B7s∠o s8Q#uPxbo/)VU ^;a#:NcݵeʹZMV6T̢[AoC"MEzmq#Idu;ۥ@ͦHj 2B@dfA~$CC5@-m]俲Yv%\:ˆ2{7hଐBȯR*BU+?d[ ,,q Tt"qԈi6!x5bW:K*n↞?GƦ2܏=SEuFv̤vĄm= nnI k0oqzh=_eD8Ud3Ca=a#Wʾ9ZD̔eɀIi?$i>P%^XւJ=2 7ٝd;YDB=]s0D ln ۴*pO]]_P 3iuBg DKvlj-P3U"7bEbZk1[ў>W[o 3~vJjA1Y&5VF A,Шaʼeu36.sл0 2'7t@.Jm $_FG7![I=EI!)ywy'p2H')//o1hvGAxُ/N}#İtbIFC&Psu)f)R),S* 傖`FѠDm{BMuڈXHV!̙őOc׸X%Q۾ 2#}mxY8z_&0%R 7ӵЋ$9Zp.ze8ADfh Lx\ǚAiae?i%O*"{)تɏU䬬  1Cn_|ٿ+G9ae8HF~f&$v#Ѻľ L+žu2Ǒ+( {|Qr NOä D*asSϒa]%lʗe!kD QT9T?Lw;yv}^/\ֻ`E;_8+mGE[U.T/;F[JKg}åm,J}4Q$WTbꞣ⩰^pHy_y/JRN5#g9&-DI=LfXطd*An!uF/Jc-F$s@Pa.+ـs>&UkIIeQ ⇪_&Ӊ2BüS{ }99vd,\<"}%#c!-K3방ȜBuWggcWWGVX/yT|KLRN{/H'rOIVBAUD>{ Q"D gJ9:3- U湖$(6$O g"P[lXSfvj-4> (ľr-؅XW`-Mitb#iC{-\nM5.ĝϺA@KP>Օ񈫹|6-,tPh=$߸fwSU!2t)yhiΙͿRG*KVfX-.4 +ܑOP:iNj|%;d?e{2FUoOP#ktVF~ rOtݲ˾R1&(k]" ĺNvU#oj1Gjob xN,?< BA뭕C>U,˂$˪1jBaJ %hQ(lJ /&[U.#Aw7/nBd#'gpԺUBOO|,ݚ QM)?9>ۤ̀ӨM%ZBǡ.r3';Ap دZ2dNue]_5SXe258PrˢpZr ) 5ڥRXKJH" R:_u_(zJwNImBDE%dө+*b6ist-⽢>XQ/]u?p>)C+y\2}u0~)$Ў.T^O9*>n4!N\M?JnbRב>}F6ʭD"VE'b?˕^$$u󺊜iK؉9^8F^ S %HSmH E "I;\OHZ'\5&J0V)EY=C(89 \BJ*&{>֗jEP35X.|Vm8WKuJ]P('',uR^7hX"aKzp.$OI}椴$n1nO4"1/z4rUh4Rh x{2Ɏc^F"?0DZKӀ(מ*~/B$O"~ՠǤV,._2bo[MX}9-H%]uMV˔.*uwC#䈡>w(Z|HEŶTv@PAo[}/goFn/RTLULW4JYg˵* ]j%,6] ijDc1ִ.EQHo߀,z}R&mVc=RQT~'-5o=Z͖?鎼`g}mz6EMLng\[!m(H}sy^Bq;L=19Qvptt()h5b'_sYnZ&w>OVl4Mb!szQ$K2ΎV zrRZnnQps_zaz#Vdz5OcQşb>{$[( kAV$ "Vr: /Y[LQMXtsmQ늀αId]gמ;agmK4PVVcp;{jQd#t* k<ƌkvMSt"|+8XEOu)Z"mRbbǂYFT)4)ZMPŐu,Y *r {G 6^h*ZςNv+__v1CT:ߋ?OqMx#lf7[JbLeS=,(ZSq]5!$% [馉IQIYx:9ZkR2|8SuR0 FN=6ԝ顟;,V_ÝY2I:z # |iR[$X14)DP)c.lj0 | ꊥ)[tŘ/:wڹB]hj +zQ%cgeF&OR.ra/Jfuj}o(VZs})bn(d#̧AdzeAE{ZuApf2]7=\ 5wLb/:ɵUx&P#vK$u1Z%Iy6dIw.\&@",M%Ϭc#WԔ0Tirl#8HLFF]ڀ|*K 9 q Pv[3 cPFK''׃dcvк$=艍+8U7_(;5"=#a,#e|S,kSz07'3ϋʩYlײP,%C V q`ic!}MZP(tyn#A8A dK9FF0?Ӑ'h" 7'V|4%p0ME i`ۙubia%_CeF +hIhȦԪכ[wj&㞏dC0 7CƲ 6,F'6wv}z6IN#MtEj\*j 黕WS$SPWsu$7::0F!'PL4;VdXBf/C,:NwW]Yax[dc .ezsUu] 6~4'>WGYdOZBHӌ1;-o#c`3xmmblLM&dު {Aбv) ˧jY2x޾[nhyB._蠉BT9i8MbIE)I e!6Ml: ~ `Ky\K[7[xU&%E%-u{˹K %e-ˌTRSs>9/CS5ÉV:J2AB[2c2p(hzuߺ%!/mPh&֞ 5V2Kf%TTI^hYSϵ5-$:$qe^ ,RZ֭V6 !`k1EVi!z+TO)|A&O2KE?fOq>L$PEfBFN(cWs(M** TaJn` y`#>]d9G \Vm'GΧz&qx뚶@_M]/h WUDD)Z Wژ\+:A#|}e"7 Es>0aƟkek!Jzl e'V/+GN+MlЪi8u-;FIe7/M8}-72d^%!./ҿ]38K+Z'V+2Eƀ@xb@+aWlHc[qQ|f!u&unvhG;S=5}k6xyQv,{olx+zF)ˀJ^LNaǯEotoR$SDʈLcDž~.:|$_)gʕԆ52qy3!k=Sh$V겗b&W=g|hN&[RVLVHC[ PvE02.Xm~:J]2 HU`MJ,j7k)v,i)COJa%G񌒇Ag#J :]5b0D vĠ(R'uPTmW<}-tk|Vz~ݛ F|=O>I{`1W2zUnDԔΪk mb&0~hy3GKbJ-?eo[AW+]䴻b'?k|9=GБt>3oB!:F/U3ҼU eG:cG1qY~c.o }PF6[8zugVNDR!9,DBj"ϊVh|>m:k"٩r&O{]-F'Z u:fwb9c`A ; CC5H}ØR. vmb[;Ҋoe|U[AT?Txo~o_-j0N5 qs#Z#ֱG jQpe!G%2y2twn{yv veY-Q-jN3TB~>,%V˧$7:HoFdmy"5Z)h&kr͌Bk`r : X͊ m`EÙ\DSdjN 8@KɐM-h)QC?+SO~$Hp+WwYg55S_(#1w.. V[5]H8b/>I7km@'}0&GMv.SsP7SV-]dLV^gC$|d}|1t>_JՒ2SafjN$0[T*W8{.YxcR')<8 *ZunBFH˞TɉbC ށ"Onp^r/j ݏ"^Z`VK q]$^D*^[;Z=WsZ9J~С$ouv 65<~ '9kƗYq$]̗G i.cPY$SGl.d:PYdI %(]B_' jZU8\O%Gڏ 5VZmIF$lI_$K?*#5.~u:f)H]9q|$brWPJʙr(ӐUQ_$3o?O!yٙ鵍z|(N-۔\5NkkeYQiY4W< 1&YUkp.&ׅ@7OZb9O cOaNeQ-gC!3tm5!P$rnюy MSK9v2ih$wlm'IS aGz21Jzzm,e*{ vEX!4(m<.)g*:|c`<i8o!kG+A״fr(C"W!5,ofDaܯS6BiPx%H(\Q$#׉v~BiIMl .[f$R @fu{yHƁ&X%%IkB=^@DhGWUl 1oVضI^c` `kX0 5`@)Q> %[dجp7ƃefr/F4 ]"1/"4Am1Pl3CAXb~5R}62z vENVmsXZ 62R98iAY(`g[s垒tchkB2c{I#DUhĸL#m]p`&$F"n@ lCHJ"; @Kf1>.S`P'+A2I cQ},Ng`%ATZ]"OQSJc t"SLĸ2F"ܸC'GS޸%АG'S8]`[^̀n>F i?6Ff%Nj}Ԋ(Y3Vުqj^$4}a:!A-g pZ@Od &%( 8_1}h!z@ 2@Ǡc#Le kӱ&E{%A-}RؙwaerH2ߑ:0 $jE *iIjB=n8ڔ A;ilaP famN$9+DaC.zA% .duUbq5+z,v dx= XD)WFL'.6ǰb2j\N2ύ&&!|e^0P%,'!8kC`_(PfL I?a'PMp)f?I XJ!dOvЊY1_itrI@Ip?Ւ@W&Ջr"OrA@Ij)KONIzl>< ngjB }CQSENUa**j91j{!ޏݪʱnn-z*rj(? b=X'M%EOZ#5iw@+ hvTgvN!6-o܎uCs/>E%u.6mU)j_ J8 _5ro^tw46d>%k݀ԊA/qeJ~"t=+b䵕Rў)fv/߄V=HEk G̯'9UNJr?V;[~1F`G.YV3VBA*g6QG)lR zWZy!X^,f,>%NMp'h?k钶o*Gojn9hIRjj]M$"(%Ic}e=bj0glm*$O;Ljrv1b!s!7lF"`:cnՍݬ2v%ѳ_*x*.(dw׊ʏ8y-f!UN1^헓_)z:12`MC2/f}XRN2&,; ڻZ5 o=p)lguC@2 wO.TwRجtSu1d3P}\Q.;?.5YekSQe;6bIw+u"D& DRx`/ԵXVIޜتf>QTi$d^HMmAi(Z)_62r"'wܗmVAX(wبoR$Sv8~UӸ&t !'`b}F;"crf W y6ngYT9WXcYb-IWo9-CQġVi7\z1QI>U\4srĝ($YOrxi\ɤo\~{ >Yk;h!0%;Kq@򙜈,!8l_c C-*[5"gѢ`b bᘬ~vF^"sR yF09)B9i2O~)s-9ʼn3n/!2 .M#i[Mؿksq]\A]sO,fV rdB؁Df7dx~"y M0[  *iB*5#%aR6K+FT@ڜ􈀝aTWl]gs t #+r'g/\ Np#S0`(D:b.z01ʬVDa_z$:d'161e/iĜ}$N6='D@:&J4S(\/mA*" a'vIuк*Iu)A$T~=buRBQr2hft Q#q2Q"Gm11qAـTfh>,^(<A3U) j ʷs1ATӰ"_VV$]W%ue)܂&]=g5󾵄]S|%$7lLs3Cp0Ez,s>Y:#$T$ [hA?xƘ )ݏ<q.z挙 W)0-:jAC2ŨҞt~Bth^f1`) R@Hz8avzbZHh3!!IQ,GXСjd j=lsD5qA4g=啷v}ռp`Pϥ3|̞)hzYЪǔ%kdX)VKhV~fxau*5$@-\2Dq"eυIaРddd$KbP< Btko9.V%Yߛmˤqh6,?hb -^0Ir(:}ŝsky i6J^$x4{L&iSys>EQS??vm_/kmZ͊mE܁ 0Úcec'm"j]*gl㚬q!n5:64Zpc!\r}*ux3I?"(u$'݇s!6YGK$RxsڥrjPUea'bw%&lqgZ2f#u΂a>F*,dm*Dd$[E]ToZ^''p29R˓46ōTKH1!608*% DBiBM6'bKB(;E/'"GXy<>[uesg#NɼxdK(mes[YN8Ȋ]|cQ#$Q4zWi5ڛQMo73N]ȕ]ar$r'bCVz쐘DZΏP,z;Bf1Q-N'rJTT* a1.e$z($ LĭYHiWq*sB1RlQ7[lDwF6r8^$؟Zx}qԓem[#K9*s緺} ;LrϹ0iן|]W4YwDsC{>9m[Whs;pPL.=GzhP\^0/+A)8Jءy׬BB/{|4x7>LԔI_>뭘X_gC2nYpm:=)ӿVF 4\^^P}RVR>iOm/籫A2j694b$|w'/#SV1\ƱV\ \ۍS9+M)f/vBQQK PV;###4 A@\1EQa/A}!$.53L!71LFA(ʳLF\ ] F@1I'O+ }Eqi HK>6"BqH!gR)gO,qIRɟ7ZE"#ieϩtJةA˄J y&D,u~!h*TBT2tLiE h'2PU$x!7?;2d,Z0-ѱ21iJݸVSez dX=PŦ3fܼNd-OBO&yC[r+?&z'<'R6E泹3uC >p0(TY񰼹ED)5@XeѠ"0CmíF WWB%P n褶l> \<g@M9$M’n+P{ 689Sՙ%7Ka+y?(c rZ; 24Zw=[ߔBdaEXBfMvKP|FP u@hN>I2JN @]@HU0DG4)'A^M皲 77!b%NJbQOc'a Ҝo;c כj"1cuĨEe:\Ժګ5bӁ\aqcI- ƠwwFv@H|-to!n37qQz%ߡqE ^No35hSH9JwzV{óv$>"ޕ溢ک*(BwZ%ӧl 9& Tlc ~ )>N bBH4818#aꦖ EyDG}OMjA t5)l{JL‘T{aJ[yϡ*|+~g@[Z\囩Xc(퀽iѢ51 #rw`2fm 1{; Y+D𗠍,$M򠛻SB"E1.B ԎBbH*LUu]Q@Np\FenQ{`\ԇcKKikPY2Dӕ5ls0f癨=r)4л<#k`ܲ+0V% ? %Z( .Azmݝ2Ptbgs_9O+l$t} &Ұ[ԟ6~t[y @P:$Y-W68S.3$= a(AO!,"xb2 ApM춴m;F1|/?>KX ?FTT9Ggf6tA)=tܺ u)\솦 pL{NlbYJ+ȕFH^M)?LQp'99hkOY$v* ONKB?$@p z?Š"_8ӥI9ဤXi~@&CH@24%P\e[~ || 84>V{T[򾆻FuAJ^G g#HA$R=K$4gknCyJ3J\okQjaG-yJZ!K+ "YKl|WU4hDŽ%^=a\MF?B_k \s.Qv͸UDHҵC #Dˋ4 h54{^*w1QqId/8^\Drز VV,pSr>4;7y=ST>㍊35vUgs^ j ˋ%LG3 DЩ.6_2kZ0%(WL0cs,p63d`aC61IH`ǢW n9v* &E 2{iU Zt2~g% HpDU 9YY XH?cz`m{!WO6r#HRoҥAѻ+NJiRT>%B;.DW踺_%Z[M)]~ |Q}LMu&#%Ue*F-eZ|ȰD.'a CM1fھ_&v.;%~I?%G߫3/uowoք)(b{a2 -ۇUj3FK!͗0jp7K(.mxZe3eκ4Sn'q埖1G.Ӓ m흴1| |CˏWZѷpq1B]T}݉}}pm3).|Ўu/j(IJ☾7k:\#8!QI4u2g@vL*lnSvޥ9r Z+ ؽbXZײ7t*QD1E5SLEжCP!鎼LNIhc;¿ɩjkV%WêAtn z̶\)UΤ*@39Yf7"d زoԺpzmr+륜 7{dk W.f`G!rM|RQKm3FeͿUYA@ʧR-ܛeʕz``: =M|̩ИiO6l[5ɖ*b|&|IUeD l)]]vM~r+"r2&* aY^/${Yhr-;@|U5DƬ XU/ !8(D~G*kJʔLve5zNOdKQO,q9E+Eqp;"_vKtK;)_T}eiDM 7hFv&8zLhEX]+:gĮ+'ӁRrY8oNVj Sq]6YH4y= +3H5[;)^U6ò4k{CE]ckЬԅ}Zr-.ZRĉ&l6wj"$vlFﬞ&Vm5|m-gFDk05ERve֍HNCōe:T^5@|Rq~YOgf6)xh[*H{jdq*mU[h^4/cYm=ufe }v_JyE_1#bͼMڶX*(Avvۆب*v[YDiH h/&넾Jfe>h) {5Q~EKoE %56uNB߬#f'sLE.|ߺǔZkJ윶#7hmLJ恮KfyJ``̎\@V5ew tC B1D=ן0ABw)(hQ1eeq̩Fx@"nK kcZfkch瑻z]4hȍŸ=VJ,3E2NAjZF+lHND6djJ-z)~<$9hFͪޘ4RJCMK_V*s,nZ[>ہ (ըՓ"Wki<򤟲-"ұ>n4+h`"P/YkYIHBg^(7onl@-N('Mzdrh:-#*#ԚN`s9ZZkU#h=!؈+(8[nnA5WI:tR?\ֹ(tdC!1ADS)swBUtJ27VքEG%}eck?X"S4Lֵ"0T5+HqVN'%W4H& )g](92(NTyP_=ڧI- +A ;]k&sR5KyFӴ:"N6UU$'8y,QB#X!|8R㝪.ƢD 4R2IiIyq[ oJd DSCkl ba|w-[9bQhx%"q JF͹rՓH,^ţpnAFGlT/I^?6vG?E3CYcFR:аЅ!{VrRwQRP"%v.Ag7Ԏ/\a/_DʩB5)&qE:re1[qwIgu1{he] E;otO%d*ahocy GA$Z?32)^DC5ډxJzmULb9bϟ$1Wq $葼]^8ҝ{gM d8<o%:FcFSS<<+-mޝ7RY'LpC+3>TG6 kO rblhvȝ,%V94C$tO9U+ Bsm!sRDO4&֖,abrrb.:Ɍ]zh././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736628220.0 tagpy-2025.1/test/la.mp30000664000175000017500000007117414740553774014705 0ustar00palfreypalfreyID3 TIT2La-Aaah (Flappin' Booty Mix)TPE1AndreasTALB Tolle TunesTDRC2005TRCK1UFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblahUFID blahblah,(4 ; n j@ B&`0 !2dɀ @2dɓ&L @dɓ&L2d @&L2dɆ @@!)ob><b HT00X;U y nO/: y J93SPNv;r|CB @TFU}ۭW(`{Vf3vezj]/9 CrJjӯ gXA3쵾kjx=-J,qKs *  8'  ,=61p*dB 860H:EtB!@$xh?|&#'A;±`Z:Ė26E"I#Kҙ]$V$8u٩L)E ȭt'cњ,oh cgXsݛ(_rL

f=L$G L"ZE 6AQ4V"X;T䦢UЄ`rl,TNiya Įɾw_M+jwlҍw>4 )8#0*w28#"1*#Pr y*m %%9ɀh 8 "< 5nfF]S4~ 5@+JB@"C10pBuN›F.HL?ZWN7T B1 9Mv*DĈt@%"xD>ǹ֕Uj&DT%Ɛ tF\h!Nhq EBv*J!']Jh&@ʔ8a$%-ll:'Exu~Kwk9~Ʊ{;'̪K~%&¢ͨNLh svP) .)ʱ& ܙP HC` Y"&'Pu,G lzh\F k2&Ae1e%'6u&F#TT{[/qU'(zJ䝌^;aqFP{,-CA#!3@c<uAP7 0` ` P $ @I3Y}$((C ju?tɄ2ǩiR<$ԭv.$U3\C6םudãfcm!bcKYAy581H@\L@= H?4,(!DA X#ΰ@0}t-޶!"&`0`p ['0/i ʼ%9@ &[a)+`hyDH4XyTȴ1ƉIzYs60ʛRڛ*`0lD0lEF^?{ͧY7ے!!L4PH ^ 0)/LH+rE!DRňS@33'<%  !aLSX&㴀lTNU.^( IFWیP!V#@̈ å99+(fE/6#|? 40֝VT&I* @ 3 L[JL̆dH%c!X4$&"BOP@iP!F KVJj0 PPPJ- 1an/ի7'e%FO2LTqT Q2N ¿f%Gc3^)f7*[:MVR-GEqGZ&&佄DN0ē∌>3D4TB" C7+qlq}A #8D% RPЙ0X\#}B #EVP09$ #B]>/sgʎ̩Cjո hS.^Bݨg/W[!U0ذÓs;5 Hu;hDgЈ  Ɯ-@ V;l,440!Ȉ0u|DI[  /:G@J LtE( \T-+b]2-bѓl殾ʆQ/ę#k!k2-mi;.^x`v#:w:RTD.iʫ&ܥ{Y .C{Sr ( 'hGzP!` D l4@ `.dlJBFUAGӉvQQjľnvA$S0(Dǐ>LFc_$D\.?, rJ6PTejcebW@uP90`HB &D cb$ d7£I`֓K}҄L/{san2BAD a HXDW4nKXKbqG8-53yjlԍP'iK7PCk!ƲҰZ簠iz/ zMj):ӭMGS ô8 . .`#flO4ni ¸"IܡV`LP*@;" jAJ[!q]3xĴLpԸ㋂]ijYA (0lJ:/8[~SE{e+8 ʴE git"u SE @SDӄ  I`H$"Y(J` <~@@AM3i%Yf! DRW^1Y΂.J氄&6F`.8BqP?9wce)|7 ղFMc 5BXRX}ͥm}|éU2F`dy@K#ІjI|O(Qh.b NAXкxJ,t*ah`c;1#C 9й+1T j4 t"A1PԳ+qe:$B2Qpi aW[š.Cd !{ S2b 40XFX8DLddV{ٱ%0/mؗ8s>L6bQ)7q$6:0w4nD0`afo&`Q@1)XeC4Idf+X YkN^̣49򗓜Y[}.s\ii@9ʉlvisv99C0 V0`]I0ɦ1 ^/ SPzB ffl KaL RPc30A"CoJ՜ܦrEtycaZSBU3kjC XDrRvkwwznW>,ZdwkH) \zÇ))t&  JpeR(./kd& `(5@ dpBW3WTơ0ᙠLMаn"(F'!r#K:")pˡʬ%*R1HaΫ R=ԃY;./qT)e8A!03ED)1e130% xI.Rǃ*8`a|J)|`P$0HI˘D QHX0k05?fS_Kc LGN/@M%+GHLq{ŏWZUwfyFOljZ@/=sMWZ_>TYO+oڔXWI F`BP AXK`  c cTf2Rd" 4s<DEc (d ّ*яHtO(sVQk@c:(!qMM'old}ˆOY>ΤP5 ݰDlxM.Oq˙*z^y`4BG< GMS!R(00P }pPx:b$ 6jp@)c0ܺW;!,¯sMXdSĬ|0/scK?8!ݫ3w}ܿ:(%o4)a\(荾>_/PPkc-V%,y`<3H271P0 J (f*"fPjV+T]#Ժ*4>XahX" V<0XC)cD (~2 HgpX (VFmQ#QV[zw"I|?GĹޝ4fNKVo٥?gH oyfEDz>44 h8`nv areU@~Z "#01F00Nf1 0=2;TL(H,8(k4Ws}n6!w sďhG1sr&^iOĤ=)0.fEݱIL\Ya]>V2_v{ͪj~< =ƠT] (/q"c޶aX1% '䘁yɂ` b !2 L Q XGF' "kj ɂc.i;؀煂D*h୬Rͫp΃: DA Y[]2ѧ81B*ubՓ^.',**sj^y^ro&B&fTBő°804/HAji*T`8 3Hƒ r HA$6L@HYiIQB' rnu" M@i8ĥ;:NJ% ݡbDbB(4WD[vLh2|\`~ #s_4Ůq LG[фbY5f @&0`S " Yd [PgP99`V<*آPIJ2٢!:C%ihCp>M*F,Q6K&cən2oŇcDW9 _ ԉY842<'2& b.+y4T$q0"3sFX. dΆ5tI$U +$  $cO/w9rN}*DKZۯUf#Ro\򠮖}G$Uݽ97e62dv>lY?=pKݫήƣS>Z|mvmҮv]mj9Fצ, QPU SBh %Rُ uK ~H[Q5a1f b4`PlTe^XY[I3T!KkgmTن=D Q,?TKmJ>PGs1wqQ=]T-D!IӇ"l 0m& H QƃYU6N/BĨ-z x.t` eg%hZ *L Q#<h nViĴƒD J-{X֭Vl rPx+9q*/$)9$~</Ge<[ESHqA@DouIv1U 12$ni $D!d <+G8F H:P $Hll)%7. zdRakh*qM(ޢli8C Fp>dB5 VUkT,$EN_5p:Zhptk.` yNa>f(hx p0Tj3y 0&1s0!85(BhaX@x#4NmBfܡ9@1r&'bfX_K܇YުKR[R;譍]zNCi^+YMU$(HC |9oQW~ޯ!ir"o6B+NT'HPUˠ"6d1@Fh F5@(Pd͡q@Ұ I4%!ƨaK(<,p(/Q hJXʲLE%fC(O&ٓ"͆pPf !ɪ%c%f{fʯu&X{{kz縙yep8L.\x6y;PQM$s0}90h5p3|0 2Hk (vѸZh)ߙx4Y,,*ґs5-id.uB!` pP%p#hmP8c3 *&^g[3p2wפSt̏򷖐*2,HSBM8لv4`)!|+];$YWe;EsBRVXd#VCeyi(+T6uݻRNTr9%kC ytDfL.;FfZJ To[ߡtֲSowԿs_~,( ̍@ FcN& &j 9 :`8-$d`B\ P0"zr40SYIyU3& `,pNyz @Ra>:y2̹~|\kNwk]b;32i̹*I 4|S9q^_֕pL I /ͤL\8\/xX2 ;$n  `2 Ja.S,i"e1fxTI܁$Wi̾s~y(٬>]Ηtzjon?gm M M  %Υ L%}EL^"abќoђ08 &`jLAy 1a){6{C*9"9&LvQTŜjVJIy5GgRrٹjϹtY廿'+uL|-~H;KnawCJ AD "HtIC 1&Lpœ$%A h2LQ IL(l0L@PA 1 !MP!շ(/dςeܙ|uYlj T7m"'{V g(~U5L՗M>&ϝ.ߎ]aJQ+B2AQ/t(1$V ŗxS36 E䶮{2ag}fs6t )~ sV(  `Ì Dʡ4] A) @TZ` yb ^H>D0:$HĄQij,MA:XjZQf9Cvٓ޻߰ d45܋'[nSXagTdyk+Gz?HD fq`agdZ,`F ``B &Jc~@t`` A!@ĢϮTrɛ&/hʽ*XϴYj_"҉ r*O^'TMCڑ1IOjTNP\D˜0;%4sk:7׫ķks~$-Dc|1{յܰ~QcP:3"s*!ɅT@ E@DbfDPb00dy.0e5fKnH5 Wj2I/(hi(@K #8rM7AW,px@rD>c'Hp/<]Hl1:㵹]T<%m=ET*|7O`QUBC "S03P}8AL  ~fR `Y( *1<8c%(/d3A}74`ĝHq85a%XƛB7]w@%_1 ,<643H!w3$BiR9d4C|') k(i8I}4OV~e1*I\,9`iز$pac` ٦0fzzfwM9-*4*`<S1 06e 00s0, `HC pED0CE8!LyԽ*Oe/Jܥ"lq 8KjpS o$G 'mtb $]HѢ)1R,y"tx~XPs5Iz'Ե#T4Bb:㏪떎{[8管#W4PmAڣhI^B'n:cAc`vJ8LBů^i0XZDtFxgu53d(<:ŴShyӖ=+z{:U0#`1?0t16!-0`$ A1a>T LF(؀ q\qeZM|4nU,/i &I 7V J @H|^$(4͔ҋF]MãMC >E B7(WGx@0K H )Ƌ$Ai o^mP)% fQkzmrdw09XAFU˧j*1[$;1`@32-%8ˁ&@'PwQ`TR0 IG"Kk;F `#VmWK)E4Xݘf @R^bI G`Ɋ: 0*08H2{U/޿Qs\?_{hmiI.+3rUH3L HG10*isႡ92 R9(8(bAc+500*p냚u,i Ψf!r,TaF'`X& cMd&H.u,a{'lVtdLڈ x20.)z@6xCR hzW[QH[u)K^ߟ =y+9Kj5~5ߚksW⪺zSXDRNDz"h-1 V'pYRWnW^k9eP'oW_Oj1B`pf`nn``P|[ ez`6r鎖e[a=0 Aڵw(i-2AEu"KJI+}  BoS0P!'}Fa*SqapC3KpYֱZ\J:1ʩ)V]C e'°LOPbKy5ӐdL @2LL(QNdha@C 8gx%P:0 12#Y@$ ɟXąݰ!F: ТtA,Ke:ۊǥH0Э5pHo <:H&s3Lm#F%Й~_̗j͌"t[9;gv>尊׆*ʓfe>UrKvUMuf`o C## @3|&`y"8_oEM..q6)Q%="j0<"`UFxMAWM29ےZ@/y&+`#҆"ǢiyU^cl\|BsZm!{ޫleBWگ?׶m(kHˮ e(R`~o`Fh(:`rFV Xa @Jj(D8.~)mR1I+`a$N09r.\h-$a6C>^>⵶ev!M_ ZC-y^̃ _ʏx0'Zsg6bզ6g ow!mo*n6E' fZd)QjIH`bx$=2GB2FƄk2NCE%1T &4u5u0.P@17h41MFHy½>הK1i'\Q -26s,GY>C6~["<IC$ *0c7,eU)eBYݎ9$_6I 1wࣘsIH_LɁ(" (p1/AxSfa)q~44&{ 9ƩdlK41  CkD\Ho;1/}y $iS2[2=V.خn^} s|@V-.f{nf8'2PiKUQZ P04+1ɀ0 1@)rG#FQG!"Q%ɱ1#ƴEASH0-Cj(=0UG$ (_L@(a( ԡEw*Jg1 EGŕnMyR|?(ziS6ᐫJ̒ffuou'E ZΒy7jnDT\{SB#!2- K3cCGXDpcs0FV(*Qg8'K0H*FYH^fm;KY\xOsxXl$F6rȝCVeXwѦ[*\(O6oPz{C}u څgPymzDԆ;C2EHLҀfLL <0@ *0ݐA=Q2N 8*%=BƼd S,@AiPsw X$X44ūh/D]h9^V\dCicJ`J$<5}.NcWs#^Ql2]_Xj/u(ת70*!Yqxg\cG4p \[L@xHL-11@rE$ɉ:Mokfw!&ZcVaSM^#>+U[3eSnc\Ӵ*y!5@ /UlCڟOUϽa.j{οS^s˷`qGv#V,6| 1 xF#!a`~`@`P3!Òal[בK2/i b%#Ʉ 'K p &Z@zVPf #((J,0c)/ @ZI` )%-^1uJv U"Q\(ƅ &T@vgs{߫}# ˒gʷ>k'VybP08)Q0070\60,0p" 鄀)`0@- RmfHFA7^VxpɁ1:HzHxh3E%qDvz ,OFM473&i)mZN9ӯ.jܳ$8}tqb"k Μ8(,6ͽALC\ቁ { y !8)V30nIոg x[G0./ * ܦ DB) OVH, !~ƀ]!P[ 5 A}d1HkȲ.(DvVP ZmCm O ʤ5{ku,Bܓzoe48Z4@+0'U _ h0 & bvdPf ``%a>3am2ǐD1ce >#d HQ٨$t5ZVfPޭ١g൵󕌠ĕ(ҺfHJ6HH7c)CL JC?6ɛm3i4Bɽ{u_Zno K]xUYSg`p`mڄ $WC49@87SCTր0`V`*`(+ O2N-P)1tHQaMaY `;Ze(u6_y#PJ6 mBrnI=n,SD%|iN)F^~$tIj֪uU ֚oZ~@hqL@|z`Nh`p0 B㉖%R L 1 `f -0 g00 ы.d1S7E|r?Lk)RA:$x˙B $FQP,먨/U.xP/k[վZqa l 7X0@*<[z002(e0(:10sYC GT3" ⃗1W2N )'pY8EJ Çߩ<&5BWN^Uf͹VIH )uFcIܞc{bL"; >6PuI:iTzыR)NS<;FùJrZ.RWݔ$ 2,h"xV A$ibႡh dBB2dþL}ٱ#Q!<4p?!F& HTJԱX%K̪̱Wt6܎Q uN@*=,A Xu.?MgŔ]?[˶5\*B!׹*&G61B5"5P`EA-1`! L`RO62db, шYW0N/JѪI!L>H$*6SD& &ǎ^;23^1G]D)n*7Pf寥ZM$ C h ?Hи3Lմ0$ ,*47bᮒdpT]| É}:gG:ȁ'n1\?nk p+(:c }IiZ%b$L 0I]|T%UW {:zV@SDMJ ?2s#A`Y.!(SyBPX(rH8U|m/hYuCPB"d:탛y,/moR*I! A>j΅ FLy":WR٘gm8ޝ+@4q ;m^Zzdt+q% 6Aȧ`6vg1Mm\{7MGoLE9뙦E)LVѭ_Ԙ`0|TۅH0 DB˺{ 41f.`d:[$J35s!P] $TIs16 8@jX0m,V䂂VلlҒ:hAr*,Җ9H*EYax;VmF}Ƒ7^6IU{W|\|MTGxT@cTezѫU , Š "pD¥I l(!!20A!42!8a`, _(S%`(xpόl:"G,ioJE!:*1  2ocï+6+}~F/%=w-&VU:&v;a xOj]O ˆ#efܵ~omnL?rl>fg`!xʶ޿eT}JM$ E'Q +00-LʌOϗaږ+? }u~um%ғm1ONզ?Uk=ݘhICgSli թX$0L.0,6=0H06A8koj ."Iz4ͦ Xs1 DH8Z(<*ҔYÀ- 8̊;҂ QPg: ,jN3$_D#  B02)SvXUeA$OH 1 AtX%4 L\p( L t9jFx%%a*m fE%ZP t`iVm-y&IC1"X0*^fbT 1`o@r#Q9%;,e6dYRgb$Bm,zR/H*(_^[x˼l'R׽{{igUZ)-pfH(`*]0_># 00 2($``+SPq[5Z}3#1 ad!Mh%g q!D2R) oXd5RX֊$$U s|N?%I:2ߙ%kray*[Z6/=*LpLzSUB@s 1LD[.XLD&<,Z) 5@Fk$c.i-C'%T3J ZVKHahDiM-}״0xp %894 ZXوm[[+V(~)ӆlgjWM}Niz#6Ő\BA0|d$ zIB L%D LUf,aNh,  ĉ&)*jmXp/ f lPaIPy 9`dqeRcM$*,ʏ1tB 9JV RFUL%x7ܥ U-YJ]Hh&zq|Z~k!~ߌU I5x)0Dr630)400=l80T58Bd3?6Ѱ 0(ViO0.z3%錵@_<PZd6@#Sh@1v (6iP/ 6ٜXTYj2`KGI bȃ"&|MvS%ZeIrPVG&M+\'{MjH) 蘵(wtE'g T!|ЎL@0p8Q5T[n>ouI#vN"|TjR`8wi^$6G't^ZFDSXy| :Ý+sE`mоyw b{E7alIcM^ÃK@tc)Z=ܬ2] c٘@R"jUL1Bс c L €)Z`͢RF"O.N)Q% 9vS-CEa~I+/xL<ѦFЈ Ԇ(Zkd( ,SN 0o9on,aT͸SY:왆Kf5uVΈ!Lػ&хiAiI( `S !`p80|sHPs@F)-D^TI(o#D匂֖}bv yԦW.Žj0)rA#DRb%!w2LI>R-w>Ye/Q܉I zF4ݣ GOJDw}67&vMӦZC(,521 qŦ#5'|PB0!:[=0 ߃_,.+)5fk,0_l-RCJ:*h"^4$ڱNYU:PKjq%0N!%z(aR=863ܓ@{iuqkx+ʺ؜I5ڌ!)`!BаD1`Py[ "C`0 *Wr^3T~^N3kZ@ޱ䕬PneYq߸JPy‚D؛Tv(KGa .2Z;=XFI'l9f?fNM x1Ju+fU-*&?y~٫6޴6!F:4j00{&iT)j/OTTsDbXt  AƓrQev&Ni+̵ďYev&D)E)C

C\1$A7i3^!v^;{vu 8o fa&b:$h5^$,1c *(P jn.H,I\-U&Zbҹd e4%y+H]0`oH8,t1) D&|7&VmZ˲nBbX)"*BH B1\Ie86Kjug-is^*v)9,ⒶrHn lJ)0(02bPip1fK`@oL 08QEFjhLA j XR]S)lҚ=ȩ_ȝ U?=cW:e?~aa/c{8p&&s8ugm: !ʯej)JBx!Pv>Zm52G yQ3:etpPrV8 A!QZ(4腅L8dsHAJ "--1%H nL6@k*,}eKlfJ578̵R 1INRe"JI%Ե֚KMrM6It]H#ugUZkuENĪKE"nnR U$ p |wp=wL  @ l KP 1̡ .@(pgFn 6K3Bn0`Th$(ʁ<&g +13'H` ;a@4L,َxuFR@@Q0,d)$l)x)oYBM˙qMZ̞{[YUf quFg9kiS(ϟ N# Ԧ yeYJۓ۫`(3";_2lp]AnrQܶ79t`Xz N;,K5F[pЍ:Bj;$VK5 4:K( \Ԇ86$p5S2ZA2)`TIXt R$`/pR]j=P"p*Gh  ,h=pl_h벿9 0̐pL=ZB]RI)e&*LAME3.96.1Ĥm_4LAME3.96.1 4TAGLa-Aaah (Flappin' Booty Mix)AndreasTolle Tunes2005././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736807386.0 tagpy-2025.1/test/la.ogg0000644000175000017500000004726214741311732014744 0ustar00palfreypalfreyOggSGr3qvorbisDwOggSGzi zvorbisXiph.Org libVorbis I 20040629ALBUM=Shake what'cha gotARTIST=Andreas DATE=2006TITLE=Laaa-ahhvorbis)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,r4Gs4sjQ'tUn~*PQ}6A`G_wz-a"|=:ZN+(qXQ1O(Ĝߨ,X$zgozQ!跜=)-tWࡪdY38и#óeh.yJP*8\6'DL>w9dG' !#7 vGd8Ƣy)VhD2t (F w_+vziluG!{qfrG%+H8=p @HkӇLt>^Tq| 4CtԥW!ٛHw WV'Ci)4peO+ĸZb@l .@!4 # ?0F-;AJH H.V$St),}һ1\䎍0kp+SshiqMAq[UP$N}Ziܭ<ߣLKʓLѲ,ĪMjƥ?ưV%}5` ֙~y|L޵Dépñ'[Fa9t6a@{փt "Э߬DMφ@g V{р`U_:gL`7r; ҿ4(EDhtǁp 'E!mQl:s̖ãoATzѭis:,iM1,˅RGOPA u ˥]X335~+ԮlW۬W' GNG)oa mSt'@|HA91>L 2ٚ[3..aҫ i֥?c]lB@j>!P4i%Z `[T֢͑tQFP.;JFީM>ؗi@~ДqTJ;vJ@:@va65|\R@C;w(nGjLhs*{mɻJ74Xt"q3S Kd4!O[TH˘|K"l8,U1^N!iU(bY,z; 9޹9Jy%0L"XfiЃWi(`ِw5g_-hnS/6K$5vⱨ#c}XND>EfcJX\="99R7k@[so,X͟CgZs9͖/Hl-$ij 7 o"wἎdNo5khyMVӚ,p9'F]XypbpgM9E6Tګ9}]oPoH t N3LVUh葽mAX*^ĸ4-i׌6zfb#9WXNjLorr10ʅ\MwRSVi ~)5`j SazW P4ET_@3:28|2lلTZ3t+..bUMMӒcjVwged;|t I[*3~h;"CSh qضW73R(mwt=Uj2Jk+|WIu)au }wb}g &FR{jzQCMI%3 e*&Y_>>P Pa7m"{kbBh| )KBdrNp:y'L}UݝTZS?T`eQ׏~ yV85_62-V DlR0 $+{WIOh;tgmG噲3* ,XKbNzl@zW `k@ #{!Z7Aޙ5xۋOć/-\4$$lIaXry2 YO=˙)Z(;5@Ѹj.vȘcI{[_q q56#|<=ׄ(5_D֜5wzqӰ)M K(n {[_) cdsoM2^va{-oүEKϴA-;GDWq׫Y^S|S`v9࠯$˥ciG b/U:5Hn/h/,[,_!;~*Bp1z7h&C<;:p\Sx-nf7_'P m '?s@*p_m_%֛Xz8۔@u'}>UE_NUJES1Y)VABa>%~]?qU;;,e9tMyv9/+p7 ޿)mH ɸRj:|ն\|bx/u/ h {z m]^ֽ~0$̇,j(p <ؐƴ$_yo X)r ,;S/ ]`E@p<0 ' D?%u$$9C͘QW)OAJC!hOc@rKRtyu4:9;8QcVRt6Ț)V߫meVP;7=Ÿ">` ' p4F-ku$B40WFz;d ĉo;A xšB *ٰEB"LfR #i)Z!1 uk)5v|h02: ի>Wpjj?oev(;#b)=1Q 8Ɂ81,3eU6ڕ~q#?,`44&q9g`Ҟͥ/x<'31Y5ֶYД7GkZz4r_}xoAȇh =A!aO?M߂SD҄r4 O4:Qul_=]}n玼A ?:K[y6o?Kd<f[2[Pwlr02[>z])*EY2+/Rg+6OggSG,,,34n'wͺRd=Y*jh &r<1iUg=C>wTMvaTͺJA go; v*:(rߢ!zIXgwȗMdfro8ۿZEWc%9tO=e;7iNjS Ĝ8&8_p@ts0ϧN49yVkJp4KF Qf(M&?mZ$5`vÏd$UWPPGY_X"tnoDm٬bZ}bO$Y[1# )-Cz;ZHM%l hj[%o4Qb9!AֽW3V_Ttf>55=}&|GsЁ~:5,`{A/(RL)v_ܺ -jķts=mܪ>f:>L ْZ~ MD/'({ !-Sb- ûb'xE}wzWl1~ UV'F1xM֢ ;:|T\F Jm@0`n*'sGbvr:t j#U*[v,-J-b0?gݰ偹@FP9{m0(ͤҟ 2faժD>z[pD##M8Š{2r]:{}ڀ6h6^E^٥6=>0]i 2N :$g mpH MS?pbeӪ+ؔO8"оp/5 ;TM(G{EX [%^I9~PT6~ ]bNJ~1A*m(r,X´Y8+WVxtf+5gRS[;EPFDn~e6{8b|_|Wa\Dp  M;ëS? h<2Be/F]@~0 !Br1q+%K 9E;2*V }ӊoSADI7fG& Hs8߭<țz"䴡RPҴC{] -ree%/pŷ;• ţkx/{ͰAhu% ͑4@=kYPR{BtrXnsXHX aftz*3@6)c΀APCH6:ɱw[&Ṕ (}-Q=w3DGp?׏YA@p"zN?LB'B"fx]ag;}fǻ{'v` Mv}҂ЀX!N&41Ŋ;X%x _F yeJ};{l6Z߳!\aXeǩ).X|ECl ӓ%*Px zFGr[6?ZPY]0i[%]ىEINb?0 Y;a9U[J1gye2bZۥB^I1,Zi" )bl~u%aiL6d&I$Nˌ J=K/o=͂…zLTV~;Ċ^x-2.!V-E+ɕDb\ q!a7B!]!T޸-ƘT 6mdӆ tlṡx'[M_ڴD9 }ɺٷ:u79>u-v5}>'kRL+&@gdǖ"ѳ'cƽDTӬ`Xe<ںPM*T9c򁸲Z}q .gmHS1X^k< b H^*dqtO~"20ܥF#9sRX=cW3,u&~5%v.f ]` Ec6Ak R @YrgGRHؤ{9`*U ?fROXώpJ@xGaf~"}ʼnfz߭Bg"5)}?Ji H{' ْrKҜ C&LJ+Vs$ OEpϘ&ozi״u lŸdn%ca&t^م#-ڊ@cϚЍUIJ) 0] ưֆ(Ced"u_I:2㹟<5JFW%~P 1a[yΠh~: + 8^2RSVloyƅ=S[x()2o:,l vgq-}D~( N.R־s5X/^-<" 8+&*@&y82 0=ʡ(߆L! zӊǫsHq{?TCS&u`mMiF<}{Ng"OOs"Op{YȮ[H [u͖xV/9ニ]Da,ι̋ڶ+ M>Ԍ?W0^-d15t`:x 2F[3gC1x-፫@\ݺHAږdrV~5`?@BzZB(Gt6N=m[ގA9C18T5'fTUBx2W &'9Ƶ0i/ZnTCde0Tc]?H];Zvt B y5%G<{sxTia4 Ȣa`VZ16]]]3SzuvcdgierQq{6OfdðrG,Gmnga ?xdE VDZOeGRTwu9 ըR@҅Ž|8x)nt/#Rx 6L<^euw;I k~6Va? @Ŧ'H" XSgΰ#AˁUt$v3N߲rL^+J _>޼d <x,pxlΘ_T3oXW !^44$=AoĂpU3!oQr`fjIYnmBc'#4}ʳođqҲJ x5%v&$fTX(qq` D#,p'ޕ%[սzbSdѷlzxeֺ|P=В6k zKKJ eg[`gЗ":j .%SnF! Xl^Jfŧ\@P xr@U=(t݅3.ɀS뎟c+h'r^-֑4!C`$DkpmW5ȡ2R'Z95Z)9Jn|"4SbH" m hYV$pyFaodkS*_CD'n;gZ d4?5ƥ:W.ޕFr>up3X^6/<2tJ+$ kExe(*4veʷU Pr8wU8hAl1bp8s&rUxDXi^,]#Ρlǝ-=eb9/Nb1PζqhC`W{5eA3*Ib >$ڟVF5nB3fВ"ܰ/OggSGtӊў%bN$d`Rp K'Ja5jr0ZQ18cl$PtI > X3X6TTzE棏Vj#L1"fz)1P?PaaߕH4FZJ0S@n&grF8?  ~)t(k{p#˿~H *Ft7FTF/]OX^H*O4 A.69eu񬨸fTa r .~tC]0 \nN9e(=w#nuYklu^u NiO%}5(k\w^ۂ0 gfes3@yz'7jWb|р m#9qZZHFLT敢k->d;ŨYj2tIJhK X 33D4>zJ5|$O0"N< K&E 9rWP Ԛi)&^ɍ6bM39X];0q#PK3=\5ZKn@c\:`Mڠ}:-ug#$9#ɴm9O, ugX֖sh:AjRV ͫ"tmw^κ;U sC]lNMz:"Wڭ,0&Mm7g2?? fpl4i^˚,bҒyQDqҮ$m XWy:t|'>OkyȄvjcR)y8 gjvjP.Pi$66b?WQp$qawtXU6M<#}:|yN3/ (:M+.{z]ƒʤn^Y1h^45ۯaќ>5q._=]I/֙&Ptg@.NbHS^t{6&2"@ =[,S"U(;'I't~|~TIH29A`^1[bL <k(YFDCIC9{[hUo}Z8+ъM \ ~) (J$ n*/ RAEqP~Jޝ 4y ~2{f$n,]v.ZIRE+u 0IYO@b7$D%)xKǍ?ya ]ABފ=Uh:dȡ^R[+]f~h'[h{sa1#IW"JR0"{ ~uyOZ@ J 1N1`If> w$2ޖ4H"Yzo߿ZBew/%;c<Noe/ 6P8Ym\ve%:9) ^ٟnk^sKV= @[Rn_G)j=l4ӎD|foq/8S13PcQ zPã>ٕ[@VQzNBg)R pW3a OLDk#y X:"La +Bͻn >?$@as߀YAGs0$q<>W@oq@HѴ.Ar~kcB#>ݑ@>d@>$[]Euk۹~ٕ֙x3x(~ #ۓ h@Rnm&+U%~.Xl5}S1Pֆ+=@tNdnP\{Jh+uBz|iNk'Z =$mD&5K){'XmpbFaN52uTVH@OG6"xFhـYц>{ѓ^e  0: |JSvcPdM*/5c}/eIxL_BnC- KY..}P ^As:[jp;qPL1v~ STsXG9S GiP(Ғv˵ v07|5y)TzL?M*b"\l4F\aeN~i,M @7I9 kZUZםa󣔲t;56Gc~ЃAsH_]MՉs5gd~fTf;n{ٮ#ю<2x*+Qm=n-<@I@)JMfD F""|O~w c6=4~hDiÁ㛁 >م%>HIv6p_D G m:23S?)MH= $|n5(t\ԙe*Z^lwP7ۆbrh'#^( ;Es '"zN3B.Z8Qkۣ2o1Ws6JF2` vl*<W es~UVe#GVs(6? ~5%zQ@xy`Cl&x yh43`W{&Վ\(-[)EMTJ#6 // Bx+8|grP#YlEȷV4#1ɚ8h?YLELxTVO@bxڄ ƫ :S"uWuairR¬W-n V/jSŅCiP~5L~@ ;B;(B38)`px,r.FQMQ?T&O)(XJ1K=~Hӵ6lEuu躒-I`|JbvyW?Hpr#f w{5 j-HM?|_㍙雹CnT5tK| 9d<|I>x/OC~.:P@MP=ڼLjc^T@Ha]*>k8YĽެJ.2A>meHJ Ӽh:?}Y9u\4 }wۂt(5Eq-GBMlQ^(/o~mFjyb&3:m˙zT66ljII+=bx8uo$j_{du#ۯ;;$@ "|`HV\B6khsWe k$cۓ$:l)(0Jp(J<.:^SpROqBv:XQtsebA8#"W`:C[w 0k9X>Tۑ~ov]IûW1%J%DIxܲ+Gk: ږ#vXo(= eyf2%ݦ_F+f4i,Pߜ7rV.ᰢp:c_ڻ6Z&OggSG|~6]v󙒣$h/:HHe5 ;-+ .E*H"D-@M S>wFk[C3ܭzuL~W@ TH\H?Y!1(:h0_GGs$cIѣN*UW[+q)vݖ+_hu)+tp!"w5*fidwd0_[Fd晎Kw$:=Cv9HS+NZ*!'^g'2.{=[,Gf%_4 fН4]RJPQr 6C,!anʸEC Pέ*3@ޠZH#ji-K]24@뿻2ZDֶR$g#{'u5Vfw*JW3Z#o륃'Xab RM 4!z:F,-*Aڽ~"o#ru2u[l2cEv!5*5Kf߇"1k~= ^þC#D X]EY3SO :$2ʐq6T—9[ 2Qi*C*ެqa0!l`wh⁒ ,y!Mzb֓4 I\F*-+;p8< JuǬʢ.CZ(_=>n6G ^5>%xc{] },6|V^>(O =i `׉eyzYOG%/`GQ^/0*^q/qǤXN4Tm>'Ѐn$:t*,B{ ^{?cg Ef~ C2тҝkYޘ{O؃?NH$5ʣЊp`.d?K;ʙaY-#6 ~1c}^r{|Vqff($S#T`G &x< ̮MwٴNHk}T\btx#~hM[{\EGVQ'A7ӥӜ]$.ބ\emڍfgpؕq00Y_16QM.?n7X̠M$&v[`@'!\<1ʘ :I$x(lOfC< 2un!j7|^ 6x}gC6IhPGuп]T̢̞V|!@R9? 8D{WZk *vV4yqۡ^{=JTiIw~9M0K1 ekV"}ke7Ō029]t|&&iO]Tř{IbtG'ig7W5#łSZ}[fҤcPS 2?'ɼ5]%?[fȠu^m>U6AāFT~1Qoϗ6Ll_;5E #22k&jvɦ2W_ |o.!'D#BM"଻|W2<ͼDw aTmK5n y2%%"(G2JNE+yvfkpHsBaT\isy\.T"W4HDicJ_@ Ȗ:Jv3~wgd~D=S4Ƭj %k j1ɰԸ< ? >)z`~"ՐXK597T:?7ɰOljYnTMJI=X+]m5BB)w@ջhI4 m8m|:vX?;ȣ3N7xWU2jl|4=ûB̹ ̽Fnϝv=hr ɴh400/ >hV U>l34hhў?[/t*d;6TUmZtqjVw7!;~)ַK] na[v '(4ʦHG ~#Y@#nO38"Uۑzi)el/A[Ep+ Vu՘AhW9:9RL~{I:`a|LQ,oƔs~I0JŨf F׺ycO5_6SWd2\ IϡogsUYp3+Mɹf!3db.08EK3b-*Q$gwgTTAl s*tc"Cjb^ e(kdoax=4y7;M^4-&cxM["././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643926551.0 tagpy-2025.1/test/tagrename0000755000175000017500000001237614177052027015543 0ustar00palfreypalfrey#! /usr/bin/env python import sys import os import os.path import re import tagpy import tagpy.mpeg import tagpy.ogg from optparse import OptionParser songs = sys.argv[1:] patterns = [ "${artist:7}-${album:7}/${track}-${title}.${ext}", "${artist}/${album}/${track}-${title}.${ext}", ] mix_pattern = "AAAMIX/${artist:7}-${title}.${ext}" opt_parse = OptionParser(usage="%prog [options] root_path") opt_parse.add_option( "-p", "--pattern", dest="pattern", help="Use PATTERN as the rename pattern" ) opt_parse.add_option( "-m", "--use-mix", action="store_true", dest="use_mix", help="Use aaamix directory" ) opt_parse.add_option( "-M", "--no-use-mix", action="store_false", dest="use_mix", help="Do not use aaamix directory", ) (options, remaining_args) = opt_parse.parse_args() if options.pattern is not None: pattern = patterns[int(options.pattern)] else: for i, p in enumerate(patterns): print(i, p) print() pattern = patterns[int(input("Select pattern: "))] if options.use_mix is not None: use_mix = options.use_mix else: use_mix = input("Use AAAMIX directory? [n]y?") == "y" def canonical_ext(song): if isinstance(song, tagpy.mpeg.File): aprops = song.audioProperties() if aprops is None: raise ValueError("MPEG track %s has no audio properties" % song.name()) return "mp%d" % aprops.layer elif isinstance(song, tagpy.ogg.File): return "ogg" else: raise ValueError("unknown track type") def is_directory(name): dirstat = os.stat(name) import stat return stat.S_ISDIR(dirstat.st_mode) def force_us_ascii(str): result = "" for i in str: if 32 <= ord(i) < 128: result += chr(ord(i)) else: result += "_" return result def makedirs(name, mode=0o777): # stolen and modified from os """makedirs(path [, mode=0777]) Super-mkdir; create a leaf directory and all intermediate ones. Works like mkdir, except that any intermediate path segment (not just the rightmost) will be created if it does not exist. This is recursive. """ head, tail = os.path.split(name) while not tail: head, tail = os.path.split(head) if head and tail and not os.path.exists(head): makedirs(head, mode) try: os.mkdir(name, mode) except OSError: pass def make_fn_suitable(str): return force_us_ascii( str.replace("/", "_") .replace("?", "_") .replace("*", "_") .replace("&", "_") .replace(":", "_") .replace('"', "_") .replace("'", "_") .replace("<", "_") .replace(">", "_") ) def expand_pattern(pattern, song): tag = song.tag() if tag is None: return None expr = re.compile(r"\$\{([a-z]+)(\:[0-9]+)?\}") def variable_function(match): meta_id = match.group(1) if meta_id == "artist": result = tag.artist elif meta_id == "album": result = tag.album elif meta_id == "title": result = tag.title elif meta_id == "ext": result = canonical_ext(song) elif meta_id == "track": result = "%02d" % tag.track else: raise ValueError("invalid variable in pattern: " + meta_id) if len(match.groups()) == 2 and match.group(2): result = result[: int(match.group(2)[1:])] return make_fn_suitable(result).strip() return expr.sub(variable_function, pattern) def get_new_dir_and_name(pattern, song): dest_name = expand_pattern(pattern, song) if dest_name is None: return None dest_dir = os.path.dirname(dest_name).lower() dest_basename = os.path.basename(dest_name) return dest_dir, os.path.join(dest_dir, dest_basename) # walk directory tree all_songs = [] for dirpath, dirnames, filenames in os.walk(remaining_args[0]): all_songs += [os.path.join(dirpath, filename) for filename in filenames] # find out all "canonical" renames, i.e. without mix directory dest_dir_counts = {} renames = [] for source_name in all_songs: try: song = tagpy.FileRef(source_name).file() except ValueError: print("WARNING: Not a media file:\n %s" % source_name) continue dir_and_name = get_new_dir_and_name(pattern, song) if dir_and_name is not None: dest_dir, dest_name = get_new_dir_and_name(pattern, song) renames.append((source_name, dest_name, dest_dir)) dest_dir_counts[dest_dir] = 1 + dest_dir_counts.setdefault(dest_dir, 0) # judge mix directory and perform moves for source_name, dest_name, dest_dir in renames: if use_mix and dest_dir_counts[dest_dir] == 1: song = tagpy.FileRef(source_name).file() dest_dir, dest_name = get_new_dir_and_name(mix_pattern, song) try: if not is_directory(dest_dir): print( "WARNING: %s exists, but is not a directory, skipping\n %s" % ( dest_dir, dest_name, ) ) continue except OSError: makedirs(dest_dir) try: os.rename(source_name, dest_name) except OSError as e: print("Couldn't rename `%s' to `%s': %s" % (source_name, dest_name, e)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736628231.0 tagpy-2025.1/test/test_deb-bug-438556.py0000664000175000017500000000114214740554007017334 0ustar00palfreypalfrey# former crash bug by Andreas Hemel import shutil import tagpy import tagpy.id3v2 import pathlib from tempfile import TemporaryDirectory def test_deb_bug_438556(): with TemporaryDirectory() as tempdir: tempfile = pathlib.Path(tempdir).joinpath("la.mp3") shutil.copy(pathlib.Path(__file__).parent.joinpath("la.mp3"), tempfile) fileref = tagpy.FileRef(tempfile.as_posix()) file = fileref.file() tag = file.ID3v2Tag(True) frame = tagpy.id3v2.UniqueFileIdentifierFrame("blah", "blah") tag.addFrame(frame) file.save() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736019321.0 tagpy-2025.1/test/test_framedumper.py0000664000175000017500000000060014736306571017566 0ustar00palfreypalfreyimport pathlib import tagpy import tagpy.mpeg def test_framedumper(): f = tagpy.FileRef(str(pathlib.Path(__file__).parent.joinpath("la.mp3"))) t = f._file.ID3v2Tag() for frame_type in list(t.frameListMap().keys()): print(frame_type) frame_list = t.frameListMap()[frame_type] for frame in frame_list: print(" %s" % frame.toString()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736714765.0 tagpy-2025.1/test/test_mp4.py0000664000175000017500000000353314741025015015752 0ustar00palfreypalfreyfrom pathlib import Path import shutil from tempfile import TemporaryDirectory import tagpy import tagpy.id3v2 def get_cover(f): tag = None if isinstance(f, tagpy.FileRef): tag = f.tag() f = f.file() covers = [] if hasattr(tag, "covers"): covers = tag.covers elif hasattr(f, "ID3v2Tag"): covers = [ a for a in f.ID3v2Tag().frameList() if isinstance(a, tagpy.id3v2.AttachedPictureFrame) ] if covers == []: raise Exception("No covers found") cover = covers[0] fmt = tagpy.mp4.CoverArtFormats.Unknown if isinstance(cover, tagpy.mp4.CoverArt): return cover else: mime = cover.mimeType().lower().strip() if mime == "image/jpeg": fmt = tagpy.mp4.CoverArtFormats.JPEG elif mime == "image/png": fmt = tagpy.mp4.CoverArtFormats.PNG elif mime == "image/bmp": fmt = tagpy.mp4.CoverArtFormats.BMP elif mime == "image/gif": fmt = tagpy.mp4.CoverArtFormats.GIF return tagpy.mp4.CoverArt(fmt, cover.picture()) def test_cover_and_tags(): with TemporaryDirectory() as tempdir: current_folder = Path(__file__).parent tempfile = Path(tempdir).joinpath("Caldhu.mp4") shutil.copy(current_folder.joinpath("Caldhu.mp4"), tempfile) f1 = tagpy.FileRef( current_folder.joinpath("Caldhu-with-cover-art.mp3").as_posix() ) f2 = tagpy.FileRef(tempfile.as_posix()) t1 = f1.tag() t2 = f2.tag() t2.title = t1.title t2.artist = t1.artist t2.album = t1.album t2.comment = t1.comment t2.genre = t1.genre t2.year = t1.year t2.track = t1.track c = tagpy.mp4.CoverArtList() c.append(get_cover(f1)) t2.covers = c f2.save() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736718969.0 tagpy-2025.1/test/test_ogg.py0000664000175000017500000000322714741035171016032 0ustar00palfreypalfreyfrom pathlib import Path import shutil from tempfile import TemporaryDirectory import tagpy import tagpy.id3v2 from tagpy.ogg import flac from packaging.version import Version def get_cover(f) -> tagpy.ogg.flac.Picture: tag = None if isinstance(f, tagpy.FileRef): tag = f.tag() f = f.file() covers = [] if tag is not None and hasattr(tag, "covers"): covers = tag.covers elif hasattr(f, "ID3v2Tag"): covers = [ a for a in f.ID3v2Tag().frameList() if isinstance(a, tagpy.id3v2.AttachedPictureFrame) ] if covers == []: raise Exception("No covers found") cover = covers[0] if isinstance(cover, flac.Picture): return cover else: mime = cover.mimeType().lower().strip() picture = flac.Picture(cover.picture()) picture.setMimeType(mime) return picture def test_cover_and_tags(): with TemporaryDirectory() as tempdir: current_folder = Path(__file__).parent tempfile = Path(tempdir).joinpath("la.ogg") shutil.copy(current_folder.joinpath("la.ogg"), tempfile) f1 = tagpy.FileRef( current_folder.joinpath("Caldhu-with-cover-art.mp3").as_posix() ) f2 = tagpy.FileRef(tempfile.as_posix()) t1 = f1.tag() t2 = f2.tag() t2.title = t1.title t2.artist = t1.artist t2.album = t1.album t2.comment = t1.comment t2.genre = t1.genre t2.year = t1.year t2.track = t1.track if Version(tagpy.version) >= Version("1.11"): cover = get_cover(f1) t2.addPicture(cover) f2.save() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736019321.0 tagpy-2025.1/test/test_tagprinter.py0000664000175000017500000000052414736306571017443 0ustar00palfreypalfreyimport pathlib import tagpy def test_tagprinter(): f = tagpy.FileRef(str(pathlib.Path(__file__).parent.joinpath("la.ogg"))) t = f.tag() print(t.artist) print(t.title) print(t.album) print(t.year) t.artist = "Andreas" t.title = "Laaa-ahh" t.album = "Shake what'cha got" t.year = 2006 f.save() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736019321.0 tagpy-2025.1/test/test_tagpy.py0000664000175000017500000000165514736306571016416 0ustar00palfreypalfreyimport pathlib import pytest import tagpy def test_non_existing_fileref(): with pytest.raises(IOError) as e: tagpy.FileRef("does_not_exist.ogg") assert e.value.args == ("File does not exist",) def test_no_such_type(): with pytest.raises(ValueError) as e: tagpy.FileRef(str(pathlib.Path(__file__).parent.joinpath("foo.bar"))) assert e.value.args == ("unable to find file type",) def test_resolver(): class DemoFile: pass class DemoResolver(tagpy.FileTypeResolver): def createFile(self, *args, **kwargs): return DemoFile() try: tagpy.FileRef.addFileTypeResolver(DemoResolver) f = tagpy.FileRef(str(pathlib.Path(__file__).parent.joinpath("la.ogg"))) assert isinstance(f._file, DemoFile) finally: tagpy.FileRef.fileTypeResolvers = [] def test_wav(): tagpy.FileRef(str(pathlib.Path(__file__).parent.joinpath("Caldhu.wav")))