pax_global_header00006660000000000000000000000064143675525000014521gustar00rootroot0000000000000052 comment=93fe97f7d79054867b5b6331c53ee41bd340cfad mediascanner2-0.115/000077500000000000000000000000001436755250000142425ustar00rootroot00000000000000mediascanner2-0.115/.gitignore000066400000000000000000000003611436755250000162320ustar00rootroot00000000000000build .project .cproject .pydevproject .settings buildopt buildcoverage buildclang /obj-* debian/files debian/libmediascanner-2.0-0 debian/libmediascanner-dev debian/mediascanner debian/tmp debian/*.debhelper debian/*.log debian/*.substvars mediascanner2-0.115/AUTHORS000066400000000000000000000006611436755250000153150ustar00rootroot00000000000000Alexander Alex Tu Alfonso Sanchez-Beato Alfred Neumayer Brian Douglass Dalton Durst Dan Chapman Dimitri John Ledkov Florian Leeber Guido Berhoerster James Henstridge Jamie Strandboge Jussi Pakkanen Ɓukasz 'sil2100' Zemczak Marius Gripsgard Matthias Klose Michael Terry Michal Hruby Mike Gabriel Pete Woods Ratchanan Srirattanamet Ricardo Salveti De Araujo Robert Bruce Park Rodney Dawes Sergey Chupligin Timo Jyrinki You-Sheng Yang mediascanner2-0.115/CMakeLists.txt000066400000000000000000000052201436755250000170010ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.6.0) project(mediascanner2 VERSION 0.115 LANGUAGES CXX C) set(MEDIASCANNER_VERSION "${PROJECT_VERSION}") set(MEDIASCANNER_SOVERSION "4") set(MEDIASCANNER_LIBVERSION "${MEDIASCANNER_SOVERSION}.${MEDIASCANNER_VERSION}") if(PROJECT_BINARY_DIR STREQUAL PROJECT_SOURCE_DIR) message(FATAL_ERROR "In-tree build attempt detected, aborting. Set your build dir outside your source dir, delete CMakeCache.txt from source root and try again.") endif() option(FULL_WARNINGS "All possible compiler warnings." OFF) option(ENABLE_TESTS "Enable tests" ON) include(FindPkgConfig) find_package(PkgConfig REQUIRED) pkg_check_modules(MEDIASCANNER_DEPS REQUIRED gio-2.0 gio-unix-2.0 sqlite3>=3.8.5 ) pkg_check_modules(GST gstreamer-1.0 gstreamer-pbutils-1.0 REQUIRED) pkg_check_modules(GLIB glib-2.0 REQUIRED) pkg_check_modules(PIXBUF gdk-pixbuf-2.0 REQUIRED) pkg_check_modules(EXIF libexif REQUIRED) pkg_check_modules(TAGLIB taglib REQUIRED) pkg_check_modules(DBUSCPP dbus-cpp REQUIRED IMPORTED_TARGET) pkg_check_modules(APPARMOR libapparmor REQUIRED) pkg_check_modules(UDISKS udisks2 REQUIRED) find_package(Threads REQUIRED) find_package(Qt5 "5.12.0" COMPONENTS Core Test QuickTest Qml Concurrent DBus REQUIRED) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -pedantic -Wextra -std=c99") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -pedantic -Wextra") set(CMAKE_CXX_STANDARD 17) if(${FULL_WARNINGS}) # C does not have any more warning flags. set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Weffc++") endif() find_package(CoverageReport) include(GNUInstallDirs) set(LIBDIR $CMAKE_INSTALL_LIBDIR) add_subdirectory(src) if(ENABLE_TESTS) message("Tests enabled") enable_testing() add_subdirectory(test) else() message(WARNING "Tests disabled") endif() # Install pkg-config file configure_file(mediascanner-2.0.pc.in mediascanner-2.0.pc) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/mediascanner-2.0.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) # Install SystemD user unit configure_file(mediascanner-2.0.service.in mediascanner-2.0.service) install( FILES ${CMAKE_CURRENT_BINARY_DIR}/mediascanner-2.0.service DESTINATION lib/systemd/user ) enable_coverage_report( TARGETS mediascanner extractor-client extractor-backend mediascanner-extractor scannerstuff scannerdaemon ms-dbus mediascanner-dbus mediascanner-qml query mountwatcher FILTER ${CMAKE_SOURCE_DIR}/tests/* ${CMAKE_BINARY_DIR}/* TESTS basic test_mediastore test_metadataextractor test_extractorbackend test_sqliteutils test_mfbuilder test_subtreewatcher test_volumemanager test_dbus test_qml test_util ) mediascanner2-0.115/COPYING.GPL000066400000000000000000001045131436755250000157220ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . mediascanner2-0.115/COPYING.LGPL000066400000000000000000000167431436755250000160450ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. mediascanner2-0.115/ChangeLog000066400000000000000000002644271436755250000160330ustar00rootroot000000000000002023-02-04 Mike Gabriel * Release 0.115 (HEAD -> main, tag: 0.115) * Merge branch 'fix_6' into 'main' (f12ed42) 2023-02-01 Sergey Chupligin * qml: Fixup QML import. Fixes #6 (11b7800) 2023-02-01 Guido Berhoerster * Merge branch 'personal/sunweaver/use-libexecdir' into 'main' (6abed00) 2023-02-01 Mike Gabriel * debian/mediascanner2.0.install: Adjust paths for files now installed into LIBEXECDIR. (952525f) * debian/: Adopt to LIBEXECDIR path change in previous commit. (759a772) * Place (helper) executables into LIBEXECDIR. (51da3cc) 2023-01-29 Mike Gabriel * Merge branch 'fix_5' into 'main' (0ea75b2) 2023-01-27 Sergey Chupligin * Fixup libexif headers path. Fix #5 (a255c3a) 2023-01-07 Florian Leeber * Merge branch 'main' into 'main' (cbc000d) 2023-01-07 Alexander * Fix bug causing looping on hidden files (f81495e) 2022-12-12 Marius Gripsgard * Merge branch 'personal/fredldotme/mediascanner+aa' into 'main' (9d17916) 2022-12-11 Alfred Neumayer * debian: Fix Halium >= 9.0 AppArmor rules (210aa01) 2022-12-02 Mike Gabriel * Merge branch 'personal/gberh/fix-startup' into 'main' (2d5cfd4) 2022-12-02 Guido Berhoerster * Start mediascanner as part of the graphical session (83dd45f) * Add changelog entry (00d2053) * Update packaging metadata (5015361) * Update to dh version 12 (2d2fa45) 2022-08-17 Ratchanan Srirattanamet * Merge branch 'systemd-migration' into 'main' (af0dd49) 2022-08-16 Guido Berhoerster * Remove obsolete upstart job and override file (7bbdf92) 2022-08-01 Marius Gripsgard * Merge branch 'ubports/focal_-_build' into 'main' (b4c6f8b) 2022-07-27 Ratchanan Srirattanamet * .gitignore: update schemantics compared to .bzrignore (a1bd288) 2022-07-26 Ratchanan Srirattanamet * daemon/Scanner.cc: remove additional logging (d4fcc66) * daemon/Scanner.cc: fix accidental `=` -> `==` (67daa7b) 2021-06-10 Rodney Dawes * Fix sizeof call to avoid crashing in tests sometimes. (fbc9eb8) 2021-06-08 Rodney Dawes * Migrate directory reading code to std::filesystetm to fix warnings. (94bf5d4) * Remove the Ubuntu namespacing of the QML module. (fbb6a5c) * Fix strftime format warning. (294da58) * Build with C++17 for std::filesystem::remove_all to fix build. (babbf3c) * Remove superfluous std::move to fix build. (98dc174) * Migrate bzrignore to gitignore (ffb7982) 2021-09-08 Florian Leeber * Maintainer: Move Jenkinsfile (ab93758) 2021-02-28 Alfred Neumayer * Update Jenkinsfile (70335d9) * apparmor: Allow media_codecs to be loaded from the vendor partition (8d0ff7a) 2020-10-08 Marius Gripsgard * General build and code cleanups (#4) (e877877) 2019-10-31 Marius Gripsgard * Merge pull request #3 from ubports/xenial_-_arm64 (70d315a) 2019-10-29 Marius Gripsgard * Fix property access with never libhybris patch (f0438b2) 2019-09-13 Brian Douglass * Fixes grouping albums by album artist rather than just album in the Music app (#1) (4107aa1) 2019-08-22 Florian Leeber * Merge pull request #2 from ubports/xenial_-_mediascanner-build (ebe41bf) 2019-08-21 Dalton Durst * Remove soversion 3 files (0afbdef) * Fix mediascanner tests (ceaa3dc) * Remove Bileto pre-release hook (acf6259) 2018-03-11 Marius Gripsgard * Also allow start if XDG_SESSION_DESKTOP is ubuntu-touch (e398ecc) 2018-01-12 Dan Chapman * Imported to UBports (6a24dce) 2017-03-22 Bileto Bot * Releasing 0.112+17.04.20170322-0ubuntu1 (867c4ab) 2017-03-22 Michael Terry * Only run mediascanner in unity8. (e940cae) 2017-03-21 Michael Terry * Only start mediascanner in unity8 (432ef7e) 2017-03-02 Bileto Bot * Releasing 0.112+17.04.20170302-0ubuntu1 (6bd7737) 2017-03-02 Pete Woods * Add SystemD session files (3018187) * Compatibility with unversioned cmake-extras modules (LP: #1563573) (c4511c6) * SystemD doesn't like the $SNAP part, also tell it about our dbus info (5f9552e) 2017-02-22 Pete Woods * Add SystemD session files (9548556) 2016-12-07 Pete Woods * Compatibility with unversioned cmake-extras modules (a71417b) 2016-12-01 Bileto Bot * Releasing 0.112+17.04.20161201-0ubuntu1 (498a33a) 2016-12-01 James Henstridge * Correctly detect embedded art in Vorbis files with TagLib 1.11. (b26c328) 2016-12-01 Michael Terry * Handle running inside of a snap by respecting the $SNAP variable. (LP: #1629009) (111c225) 2016-12-01 James Henstridge * Taglib 1.11 removes COVERART/METADATA_BLOCK_PICTURE from the fields list of XiphComment, so also check for a non-empty pictureList() when deciding whether a file contains a embedded art. (225ab61) * Add a test for detection of embedded art in vorbis files. (a360122) 2016-11-10 Michael Terry * Support $SNAP (6c1cc07) 2016-09-09 Bileto Bot * Releasing 0.112+16.10.20160909-0ubuntu1 (110904b) 2016-09-09 James Henstridge * Replace deprecated use of GetConnectionAppArmorSecurityContext method with GetConnectionCredentials. (LP: #1489489) (7441d62) * Disable optimisation when compiling dbus-codec.cc to avoid gcc 6 compilation bug. (LP: #1621002) (197ce52) 2016-09-09 You-Sheng Yang * Update mediascanner-extractor apparmor profile to cover Android library locations on 64-bit systems. (f279d0a) 2016-09-09 James Henstridge * Add apparmor-easyprof hardware directories to package so AppArmor profile can compile when apparmor-easyprof-ubuntu isn't installed. (LP: #1443693) (05461e5) 2016-09-09 Bileto Bot * When multiple volumes are mounted in quick succession, scan them serially to avoid reentrancy problems in the initial scan. (LP: #1489656) (4a68ba3) 2016-09-09 James Henstridge * Switch over to GetConnectionCredentials(). (57e184a) * Ignore test results on powerpc. (bbbc550) 2016-09-08 James Henstridge * Disable optimisation when compiling dbus-codec.cc to avoid gcc 6 bug. (9a04fb1) 2016-08-31 James Henstridge * Ensure the usr/share/apparmor/hardware/*.d directories that apparmor-easyprof-ubuntu usually exists are created when we're installed. (c2a31c1) * Ensure we send out an invalidate signal after processing all queued volumes. (ca4372b) * Increment package version number. (b43951b) * Add tests for VolumeManager. (dac0ad1) 2016-08-30 James Henstridge * More cleanup. (f6600b6) * Move the event out of the queue. (04880e9) * Actually manage a volume mount events as a queue. (d6bc2bf) * Switch public API to methods for queuing add/remove of volumes. Currently just adds/removes volumes synchronously. (c32b714) * Split volume management out from ScannerDaemon class into its own class. (e776880) * Move InvalidationSender to mediascanner namespace. (ef4a86e) 2016-08-23 You-Sheng Yang * add Android lib64 path for 64-bit architectures. (989fb9e) 2016-05-26 CI Train Bot * Releasing 0.111+16.10.20160526-0ubuntu1 (128c50e) 2016-05-26 Robert Bruce Park * Use new bileto_pre_release_hook. Approved by: Robert Bruce Park, James Henstridge (55d326b) 2016-05-26 Alex Tu * apparmor: extend read permissions to cover media_codecs*.xml to support Turbo. Approved by: Jim Hodapp, Alfonso Sanchez-Beato, James Henstridge (c722ef2) 2016-05-03 Alex Tu * extend the read permission to media_codecs*.xml (29ca208) 2016-03-21 Robert Bruce Park * Use new bileto_pre_release_hook. (cf56fc7) 2016-03-17 CI Train Bot * Releasing 0.111+16.04.20160317-0ubuntu1 (e3b4f8b) 2016-03-17 James Henstridge * If a file is unscannable, insert a fallback MediaFile into the index immediately, rather than relying on it being added by the broken_files logic on next startup. Approved by: Michi Henning (064ce2e) * Also catch runtime errors from extractor during initial scan. (e0256db) 2016-03-16 James Henstridge * Add a test to ensure SubtreeWatcher inserts fallback results promptly. (adac192) * Rather than using fixed sleeps, sleep until an invalidation signal is received. (9d235bd) * Update InvalidationSender so we can run it in a zero delay mode for testing. (e139707) * Insert fallback entries in index for unscannable files immediately. (d22eb38) 2016-02-25 CI Train Bot * Releasing 0.111+16.04.20160225-0ubuntu1 (60afc22) 2016-02-25 James Henstridge * Don't emit the InvalidateResults signal if a file was opened for writing and then closed, but not actually modified Fixes: #1542175 Approved by: Michi Henning (d3b4d65) * Favour the EXIF DateTimeOriginal tag over DateTime when extracting metadata from photos. Fixes: #1468585 Approved by: Michi Henning (6defd71) * Use taglib to extract metadata from Vorbis, Opus, Flac, MP3 and MP4 audio files. Other formats will fall back to the existing GStreamer code path. Fixes: #1536832 Approved by: Michi Henning (ecf7fd6) * Add an API for batching multiple MediaStore updates into a single transaction. Use this new API during the initial directory scan during start up. Approved by: Michi Henning (665c333) 2016-02-24 James Henstridge * For FLAC files, set hasThumbnail to true if pictureList() is not empty. (c1eb258) * Protect against GValues holding NULL strings. (43b953c) * Add isValid() checks to taglib extractor code paths. (563c2c2) 2016-02-23 James Henstridge * Add a test that shows we pick the DateTimeOriginal date from krillin.jpg, where DateTime has been offset by an hour. (60ee97a) * Add a test image taken from a krillin, demonstrating date issues (image scaled down but EXIF metadata kept intact). (b71a332) * Update shlibs file for gcc5 builds. (8f7656e) * Prefer the DateTimeOriginal EXIF tag when processing dates of photos. (3574dca) * Update symbols file. (19d32c6) 2016-02-22 James Henstridge * Bump version number. (f84f0f7) * Expand test to show multiple transactions using a single MediaStoreTransaction object. (54f9925) * Batch updates in ScannerDaemon::readFiles() to the same rate we send out the invalidation notifications. (23a4837) * Add a helper class to manage data store transactions. (98c33c0) 2016-02-17 James Henstridge * Add a hacking file that notes the need to install the qt5-default package. (37d3872) 2016-02-16 James Henstridge * Add missing break statement. (648839e) * Add some file format specific tests to test_extractorbackend.cc (9b45f28) * Fix up date parsing for MP3 and MP4, to ensure we get the month and day. (ea3e229) 2016-02-12 James Henstridge * build-depend on libtag1-dev. (95de0a7) * Add a test to compre GStreamer and Taglib based extraction of a vorbis file. (c93dc59) 2016-02-11 James Henstridge * Add Taglib based extraction for Ogg, Flac, MP3, and MP4 files. (2ceb8fc) 2016-02-10 James Henstridge * Fix up test set up. (560bbab) 2016-02-09 James Henstridge * Add a test to show that the invalidation count signal is not emitted when a file is opened for writing but not actually changed. (c3c8e18) * Don't notify about changes when the file etag does not change. (786b4a7) 2016-02-05 James Henstridge * Move GStreamer extraction code out to a separate helper class. (b34b137) * Split EXIF image extraction code into helper class. (a6a3e42) 2015-12-16 CI Train Bot * Releasing 0.110+16.04.20151216-0ubuntu1 (af3c69d) 2015-12-16 James Henstridge * If the mediascanner index can not be opened, catch the exception in the QML plugin and act as if the database is empty. A warning is printed via Qt's logging framework. This prevents QML apps using mediascanner from terminating if there is a problem opening the media index. Fixes: #1514517 Approved by: Michi Henning, PS Jenkins bot (881b6d2) * Fix up handling of directory renames, so old contents is correctly removed from the index and inotify watches for subdirectories are cleaned up. Fixes: #1460411 Approved by: PS Jenkins bot (15db8b9) * Bump version number and update symbols file. (31155b9) * Also remove watches for subdirectories in removeDir(). (9f0cb8d) * Use a private bus connection in basic.cc test, and remove data on unmount. (ba1d5c6) * Add a method to remove all files in a particular subtree. (5b24f9c) 2015-12-15 James Henstridge * Add a test for the issue described in the bug report. (98d466b) * Disable some warnings for dbus-generated.c. (f9f6e4d) 2015-12-11 James Henstridge * Add tests for the missing database case. (390bc1b) 2015-12-10 James Henstridge * If mediascanner database is missing, allow MediaStoreWrapper construction to complete, but act as if the database is empty. (0baac7f) 2015-11-24 CI Train Bot * Releasing 0.109+16.04.20151124.1-0ubuntu1 (30aae2c) 2015-11-24 James Henstridge * Disable MountWatcher's callback during destruction so we don't call back into a partially destroyed class. Fixes: #1492393 Approved by: PS Jenkins bot, Michi Henning (ff4907e) * If a folder contains an image file named {cover,album,albumart,.folder,folder}.{jpeg,jpg,png} use it as album art for songs in preference to online art if the songs do not have embedded art. Fixes: #1372000 Approved by: PS Jenkins bot, Michi Henning (dae96dd) 2015-11-23 James Henstridge * Clear the MountWatcher callback in the destructor so we don't send out notifications during shutdown. (2da7f64) * Bump version number to account for API additions. (ee5f8d9) * Add folder artwork support to Album class. (0f11e83) * Add Album::getHasThumbnail() for use by dbus code, and make MediaStore pass in has_thumbnail to Album. (5882868) * Add new constructor for album, explicitly passing the has_thumbnail boolean. (1c7cbaa) 2015-11-20 James Henstridge * Remove unneeded #define. (d2bba85) * Add a test that will overflow the folder art cache. (e09f2c8) * Add back support for case insensitive matching. (87d180e) * Add tests for art precedence, and case insensitive cover art. (1582083) 2015-11-19 James Henstridge * Plug folder art cache into MediaFile::getArtUri(). (7013b15) * Add folder art cache class. (960dd47) 2015-11-10 James Henstridge * Merge Jussi's standalone-art-uri branch. (f9ebe60) 2015-11-09 CI Train Bot * Releasing 0.108+16.04.20151109-0ubuntu1 (df32473) 2015-11-09 James Henstridge * Move the metadata extractor to a separate process to isolate bugs in media codecs. Fixes: #1508142 Approved by: PS Jenkins bot (e5ab1f4) 2015-11-08 James Henstridge * Add code to crash the metadata extractor after a certain number of extractions so we can verify that it is correctly restarted. (c2a1fb5) 2015-11-07 James Henstridge * Delay starting the test bus daemon, and use private connections to the session bus to ensure we aren't reusing connections between test cases. (05d94d7) 2015-11-04 James Henstridge * Recreate the GDBusProxy when we get the no reply error: the old proxy will likely be bound to the old instance's unique name. (16d3664) 2015-11-03 James Henstridge * Add attach_disconnected flag to extractor's apparmor profile so it can access /dev/socket/property_service. (2266292) 2015-11-02 James Henstridge * Fix compile error. (87f87b9) * Merge from trunk (1b49993) 2015-11-02 CI Train Bot * Releasing 0.108+16.04.20151102-0ubuntu1 (4a85c35) 2015-11-02 James Henstridge * Fix the metadata extractor so that it correctly extracts the date and presence of cover art from MPEG 4 audio files. Fixes: #1492407 Approved by: PS Jenkins bot (4d586bf) * Remove the id column from media_attic so that we don't get conflicts when copying data between media and media_attic. Fixes: #1501990 Approved by: Michi Henning (9e7f44a) * Update packaging to rename qtdeclarative5-ubuntu-mediascanner0.1 package to qml-module-*, and fix a few other problems pointed out by Lintian. Fixes: #1342031 Approved by: Michi Henning, PS Jenkins bot (5939ac9) * Make the new qml-module-ubuntu-mediascanner0.1 package break/replace old versions of the equivalent qtdeclarative5-... module. (2869975) 2015-10-23 James Henstridge * Update change log. (f711e1c) 2015-10-22 James Henstridge * Move daemon back to /usr/bin so the AppArmor profile is correctly applied again. We can't easily change the AppArmor profile name because it is encoded into the easyprof profiles of other apps :( (b59df7c) * Fix up QML tests to pass again. (e1e2ccc) * Move playlist blacklisting code to the right location. (a10948f) * Update D-Bus API to handle extra fields on DetectedFile and MediaFile. (cbeae46) * Merge from trunk (7d662a1) 2015-10-09 James Henstridge * Remove debug printfs. (430d41d) 2015-10-08 James Henstridge * Regenerate control file. (2991213) * Move the mediascanner-service-2.0 executable out of /usr/bin, since it is not intended to be executed directly. (cad6948) * Don't depend on qt5-default to avoid build-depends-on-metapackage Lintian warning: instead set QT_SELECT environment variable. (754caab) * Update standards version. (92a2929) * Add short license name (public-domain) for Mozilla FTS code. (7657250) * Rename QML plugin package to qml-module-* and add transitional package for upgrades. (dfdb8fb) * Tell cmake not to add a soname to the QML plugin. (1705d78) 2015-10-07 James Henstridge * Add test for mpeg4/m4a metadata extraction, and fix extraction logic to handle its cover art (exposed as GST_TAG_PREVIEW_IMAGE rather than GST_TAG_IMAGE) and the date (exposed as GST_TAG_DATE rather than GST_TAG_DATE_TIME). (12f3873) * Add a test MPEG4 audio file with cover art. (4650db6) * Remove the id column from media_attic so that we don't get conflicts when copying data between media and media_attic. (f658078) 2015-09-22 CI Train Bot * Releasing 0.107+15.10.20150922.1-0ubuntu1 (851a5c1) 2015-09-22 James Henstridge * Use a single source tree to build the wily and vivid packages, picking an appropriate soversion for each. (802f921) * Fix typo. (17ad7c3) * Add lsb-release to build-depends. (219d7c6) 2015-09-21 James Henstridge * Actually edit the right version of control.in (105ba89) * Revert libdbus-cpp-dev dependency to the version available in vivid: we don't rely on 5.0 at an API level. (d54e5ff) * Copy symbols file .so.3 symbols file from vivid branch. (fcf27a1) * Bump package version numbrer to 0.107. (98485b6) * Adapt the vivid/wily single-tree build infrastructure from unity-scopes-api. (a9a9f86) 2015-09-17 CI Train Bot * Releasing 0.106+15.10.20150917-0ubuntu1 (36e374d) 2015-09-17 James Henstridge * Add a hasMedia() method to MediaStore, which returns true if there is any media of the given type. Approved by: Pawel Stolowski (37d4cf4) * Record the modification time of files in the media index, and allow sorting results of query() and queryAlbums() by modification time. Approved by: Pawel Stolowski (d2e21ae) * Merge from track-mtime (b828afc) * Fix up symbols file for abi tags. (cf30e48) * Merge from track-mtime (c7fa85c) * Merge from trunk (28b5ef0) 2015-09-17 Matthias Klose * Update symbols file for GCC 5.; Tighten build dependency on libdbus-cpp-dev. (3a36951) 2015-08-07 James Henstridge * Merge from track-mtime (2b4f009) * Merge from lp:mediascanner2 (dcf5ed6) 2015-07-21 CI Train Bot * Releasing 0.105+15.10.20150721-0ubuntu1 (428e08b) 2015-07-21 James Henstridge * Remove the hard dependency on g++-4.9. Fixes: #1452332 Approved by: Pawel Stolowski (de3d62d) 2015-07-17 James Henstridge * Remove hard dependency on g++-4.9 (67d7a0c) 2015-07-14 James Henstridge * One of the pkg_check_modules() invocations was stomping on the MEDIASCANNER_VERSION variable. Change the variable prefix to avoid the conflict. (7d1970d) * Rename haveMedia to hasMedia. (87c0a09) 2015-07-13 James Henstridge * Update symbols. (777f952) * Add haveMedia() method to MediaStore. (e78fa65) * Update version in pkg-config file. (fc0153f) 2015-07-07 James Henstridge * Update Debian changelog. (23cae4b) * Update test to use same type as rest of code. (bcbd707) * Add new symbols and sort. (58f62ac) * Expose modification time in QML wrapper. (a7b7c73) * Allow sorting queryAlbums() results by modification date (e620e4e) * Allow sorting query() results by modification date. (1fdadd8) * Update D-Bus code to pass through mtime. (2d82499) * Make extractor record modification time. (d22d81d) 2015-06-30 James Henstridge * Update media store schema: (f003741) * Add modification time to MediaFile class. (14ab3df) * Use the cmake-extras version of the coverage report helpers. (6161537) 2015-06-04 CI Train Bot * Releasing 0.105+15.10.20150604-0ubuntu1 (fee6bd5) 2015-06-04 James Henstridge * Treat invalid dates in MP3s as missing metadata. Fixes: #1436110 Approved by: Michi Henning (9fb76f2) * More cleanups. (a8af275) 2015-06-03 James Henstridge * Update test to actually check what it claims to, and work now that we've got more test audio files. (b109904) * Move QML plugin directory so that it isn't shadowed by the installed version. (9f7f109) * Ignore date tag if it is NULL (as happens for MP3 files with invalid dates). (a634460) * Add tests for bad dates in Ogg and MP3 files. (9b2f316) 2015-01-28 CI Train Bot * Releasing 0.105+15.04.20150128-0ubuntu1 (5b5f3bf) 2015-01-28 Jussi Pakkanen * Revert use of WAL log and instead try to rerun queries that fail with SQLITE_BUSY. Fixes: #1415318 Approved by: Pete Woods, PS Jenkins bot (1685430) * Retry busy queries only a maximum amount of times. (410d427) * Revert use of WAL log and retry failing queries instead. (5233cce) 2015-01-27 CI Train Bot * Releasing 0.105+15.04.20150127-0ubuntu1 (6a878f9) 2015-01-27 Jussi Pakkanen * Send invalidations signals during scans that take a long time so the dash gets updated. Fixes: #1414566 Approved by: PS Jenkins bot, Marcus Tomlinson (9c33c87) * Use wal mode for the journal as it seems to work when there are multiple connections to the db. (bb21e62) 2015-01-26 Jussi Pakkanen * Use only seconds as requested by review. (146537b) * Putting comparison operators the right way is surprisingly hard. (b0e329b) * Tweak invalidation timeout. (36750dd) * Drain events while scanning so timeouts are processed. (6ba4b4f) * Send invalidation signals during long lasting scans. (94b9f17) 2015-01-22 CI Train Bot * Releasing 0.105+15.04.20150122-0ubuntu1 (8edc34f) 2015-01-22 Jussi Pakkanen * Skip scanning of special directories if they point to user home dir. Fixes: #285998 Approved by: James Henstridge, PS Jenkins bot (db6307c) * Add blacklist functionality and use it to block music playlists. Fixes: #1384295 Approved by: James Henstridge, PS Jenkins bot (15398f7) * Merged trunk. (aa09dd5) 2015-01-21 CI Train Bot * Releasing 0.105+15.04.20150121-0ubuntu1 (b36b398) 2015-01-21 Jussi Pakkanen * Make Valgrind uninitialised jump warning go away. Approved by: James Henstridge, PS Jenkins bot (bb8acc7) * Extract metadata from images that don't have exif entries. Fixes: #1378880 Approved by: Charles Kerr, James Henstridge, PS Jenkins bot (20963b4) 2015-01-20 Jussi Pakkanen * Whitespace fix to restart Jenkins. (c97c5bf) 2015-01-06 James Henstridge * Still start and stop the daemon manually, since it doesn't close cleanly. (f4ee7e3) 2015-01-05 James Henstridge * Switch test_qml over to GIO's dbus test fixture, since it correctly shuts down the private bus more reliably than dbus-cpp. (bc4c281) 2014-12-12 Jussi Pakkanen * Added test for standalone art. (017136a) 2014-12-12 James Henstridge * Fix ExtractorBackendTest::init test to actually call the constructor, rather than define a function prototype. (52536ad) 2014-12-12 Jussi Pakkanen * Detect standalone album art when building art uris. (f5d1b5d) 2014-12-12 James Henstridge * Update copyright statements on a few more files. (612aeb3) * Add copyright headers. (1ea923f) * Cleanups as per Jussi's review. (dfa5da2) * If we get no reply from the extraction daemon, retry the job once. The daemon most likely crashed due to a codec error, but the current file might not be the one to blame. (95bee5e) * The @{multiarch} part in the policy wasn't correctly attaching to extractor, so just use a simple glob. There is enough context in the rest of the path to be a reliable match. (3c595c9) * Add AppArmor profile for mediascanner-extractor, and remove some of the GStreamer related rules for mediascanner-service. (c8987cb) 2014-12-11 James Henstridge * Include mediascanner-extractor in the mediascanner2.0 package. (3cda86b) * No need to include "-2.0" in executable name, since it is installed to a version dependent directory. (d760efb) * Don't link mediascanner-service-2.0 to GStreamer of libexif. Fix a crasher bug in the error case of MetadataExtractor::extract(). (17db744) * Remove dead code from ExtractorBackend. (d2269d2) * Add tests that run directly against the extractor backend rather than through D-Bus to make debugging easier. (535a1dc) * Add a service file for the extractor service, and update the basic and test_metadata tests to spin up a private session bus to talk to the extractor. (5bbb03f) * Split MetadataExtractor backend code into a ExtractorBackend class, and make MetadataExtractor proxy the D-Bus service. (dd46c20) * Move extractor source to src/extractor. (9dcd72a) * Skeleton of extractor daemon. (443d555) 2014-10-30 CI bot * Releasing 0.105+15.04.20141030.1-0ubuntu1 (1fa4551) 2014-10-30 James Henstridge * When a new directory is added and a new inotify watch is set up, call fileAdded() on any regular files already in the directory so they are not missed. Fixes: 1379817, 1387103 Approved by: Jussi Pakkanen, PS Jenkins bot (272e654) * Add a fix to check the sqlite version at runtime to check for versions containing the tokenizer regression. (527f846) 2014-10-24 James Henstridge * Add a test for the new file+directory case. (cf067ba) 2014-10-23 James Henstridge * When a directory is added, call fileAdded() on regular files when checking its initial contents, so no files get missed in the time between the directory being created and the inotify watch being added. (b1e6b72) 2014-10-23 Jussi Pakkanen * Skip scanning of special dirs if they point to user home. (575995f) 2014-10-22 Jussi Pakkanen * Create blacklist functionality and use it to block audio playlists. (5beb056) * Use localtime instead of gmtime. (07ed064) 2014-10-10 Jussi Pakkanen * Use get_file_info which is a lot more efficient. (f0fda63) 2014-10-09 Jussi Pakkanen * Made test work regardless of what the timestamp on the test file is. (afc781d) * Removed vestigial gfile thingy. (2a6cce5) * Extract metadata from images that do not have exif entries (such as pngs). (353e966) 2014-10-01 CI bot * Releasing 0.105+14.10.20141001-0ubuntu1 (a5a702e) 2014-10-01 James Henstridge * Use udisksd to track mounts and unmounts of removable media. Fixes: 1358750 Approved by: Jussi Pakkanen, PS Jenkins bot (172eb85) * Further narrow the dbus rules to the specific methods. (f708f19) * Narrow down the D-Bus apparmor profile rules. (864e58b) 2014-09-30 James Henstridge * Merge from trunk, fixing conflicts. (b27cbcd) 2014-09-27 James Henstridge * Remove some unneeded #includes. (e50b0fe) 2014-09-26 Jussi Pakkanen * Valgrind silencing. (6211c84) 2014-09-26 James Henstridge * Ignore mount events outside of /media: on the phone, the root file system is erroneously marked as removable. (a8e8605) * Remove using directive. (88d9ade) * Update AppArmor profile to let us talk to udisksd. (e45733b) * Rip out the mountfd code in the scanner daemon and use MountWatcher instead. (f838690) * Add code to monitor mounts of removable media via udisks2. Not currently integrated into the daemon. (922d8b4) 2014-09-25 CI bot * Releasing 0.105+14.10.20140925.1-0ubuntu1 (7e936df) 2014-09-25 Jussi Pakkanen * Use fallback data for crasher files so they at least show up in scope queries. Approved by: James Henstridge, PS Jenkins bot (4b06f9c) * Print banner when starting the daemon so upstart logs are easier to parse. Approved by: James Henstridge, PS Jenkins bot (4c33406) * No need to set title on fallback case, it is done automatically. (1ccfe23) 2014-09-24 Jussi Pakkanen * In case of crasher files, write fallback data so at least something shows up. (e76b200) 2014-09-16 Jussi Pakkanen * Print banner when starting daemon. (fd67797) 2014-09-09 CI bot * Releasing 0.105+14.10.20140909-0ubuntu1 (7e388d4) 2014-09-09 Jussi Pakkanen * Print a log message when skipping a broken file. Approved by: James Henstridge, PS Jenkins bot (5edeab0) 2014-09-09 Jamie Strandboge * debian/usr.bin.mediascanner-service-2.0: - add video abstraction - silence access to /run/udev/data/** like we do elsewhere - allow read on /dev/video* - allow read on /sys/devices/**/video4linux/**/uevent Approved by: Jussi Pakkanen, PS Jenkins bot (47a2cf6) 2014-09-05 Jussi Pakkanen * More logging. (f2f7627) 2014-09-04 Jamie Strandboge * updates for various denials: - add video abstraction - silence access to /run/udev/data/** like we do elsewhere - allow read on /dev/video* - allow read on /sys/devices/**/video4linux/**/uevent (6eb9b31) 2014-09-03 CI bot * Releasing 0.105+14.10.20140903-0ubuntu1 (065de6d) * Replaced 0replaceme with real versions. (45c32f0) 2014-09-03 James Henstridge * Expose embedded album art for Album objects too. Bump soname to account for Album class changing size. Approved by: PS Jenkins bot (129c2d2) 2014-09-03 Jussi Pakkanen * Fix usage of blocking cache. Approved by: James Henstridge, PS Jenkins bot (59742bd) 2014-09-03 James Henstridge * Record whether files include embedded artwork in the index, and use this to generate artwork URIs that will invoke the appropriate image provider (thumbnailer if there is embedded art, albumart otherwise). Approved by: Jussi Pakkanen, PS Jenkins bot (93eaa51) * Fix up D-bus marshalling of Album objects, which was breaking the qml_dbus tests. (43f7ad7) 2014-09-02 James Henstridge * Ensure that other album attributes are exposed in albums model. (04152d6) * Update symbols. (1b0787c) * Move the logic for building Album's art URI out of MediaStore and into Album. Also expose the album's genre. (ba55152) 2014-09-01 James Henstridge * Update symbols. (6bc4a03) * Bump library soname, since we've changed the size of Album. (997b894) * Expose date through QML album model. (de6a24c) * Add date and art_uri to Album. (41de067) * Add move constructor/assignment to Filter. (d7adc3d) * Add a first() aggregate that returns the first result for a group. Use this to pick an arbitrary date, filename and has_thumbnail value for albums (not actually stored yet). (1366628) 2014-08-31 James Henstridge * And make sure we don't leak the private struct on assignment. (e3a886b) * Fix up copy constructors for Album, and add move constructors/assignment operators for both Album and MediaFile. (362bcfc) * Move Album fields to a private struct. (d27c98a) 2014-08-29 James Henstridge * Bump package version number. (0995053) * Update symbols. (5f8bdae) * Use art URIs from the core library in the QML plugin. (4d6bb85) * Add getArtUri() method to MediaFile and Album. (0aeae17) 2014-08-28 James Henstridge * Merge lp:~jpakkane/mediascanner2/thumbnailinfo fixing conflicts (868a4f5) 2014-08-25 Jussi Pakkanen * A (1db64e6) * Fix usage of dirblock cache. (ee23b0a) 2014-08-25 CI bot * Releasing 0.104+14.10.20140825-0ubuntu1 (d1f14ab) * Replaced 0replaceme with real versions. (ba46178) 2014-08-25 Jamie Strandboge * allow read access to /etc/udev/udev.conf for gstreamer (LP: #1356883) Fixes: 1356883 Approved by: James Henstridge, PS Jenkins bot (8010982) 2014-08-25 James Henstridge * Update AppArmor policy to allow read access to /sys/devices/**/video4linux/video** Fixes: 1353139 Approved by: Jussi Pakkanen, PS Jenkins bot (2725b77) 2014-08-25 Jussi Pakkanen * Add database integrity constraints. Approved by: James Henstridge, PS Jenkins bot (4c18b7d) * Don't call closedir with a null argument. Approved by: James Henstridge, PS Jenkins bot (920675e) * Store information on files that break GStreamer and skip them when encountered. Approved by: James Henstridge, PS Jenkins bot (bb34021) * Fix inotify usage. Approved by: James Henstridge, PS Jenkins bot (1cf897b) 2014-08-25 James Henstridge * Add support for custom sort orders to query(), queryArtists() and queryAlbums(). This involves an API break, so the soname has been changed. Approved by: Jussi Pakkanen, PS Jenkins bot (30e3b78) 2014-08-25 Jussi Pakkanen * Detect whether a media file has an embedded thumbnail. (3c3f8e1) * Add field that tells whether the file has an embedded thumbnail image. (5535d46) * Test constraints. (2f2e584) * Return if encountering a bad file. (634bd4c) 2014-08-22 Jamie Strandboge * Author: Jamie Strandboge Description: allow read access to /etc/udev/udev.conf for gstreamer Bug: https://launchpad.net/bugs/1356883 (2effede) 2014-08-22 Jussi Pakkanen * Add data integrity constraints to database. (8d81817) 2014-08-22 James Henstridge * Set the library version from the toplevel CMakeLists file. (c870825) 2014-08-22 Jussi Pakkanen * Symbol fix. (5824495) * Store etag info in broken files so they are no longer blocked once they get updated. (1f19856) 2014-08-22 James Henstridge * Update changelog. (aa1cb5a) * Allow read access to video4linux sysfs files to fix AppArmor problems on Manta. (0a54187) * Add sort order tests for queryAlbums and queryArtists. (e6ecbbd) * Add sort order tests for query(). (4a72def) * Update symbols. (59d0719) * Bump soname. (4dbb8e2) * Add sorting code to queryAlbums() and queryArtists(). (7fcbf90) * Handle ordering in query(). (8f73fb2) 2014-08-21 James Henstridge * Fix up methods that were bypassing the database mutex. (d2a59c4) * Update signature of query(), queryAlbums() and queryArtists() methods. (1e8a703) 2014-08-19 CI bot * Releasing 0.103+14.10.20140819-0ubuntu1 (8c95929) 2014-08-19 Jussi Pakkanen * Apparmor fix from jdstrand. Fixes: 1357348 Approved by: Pete Woods, PS Jenkins bot (201b426) * Check for null. (d406d41) * Fix symbols file. (8b86fa5) 2014-08-18 Jussi Pakkanen * Store and skip broken files when scanning. (3885790) * Added functionality to maintain a list of files that break GStreamer metadata scanner. (96a8249) * Fix inotify usage. (519880c) 2014-08-15 Jussi Pakkanen * Apparmor fix from jdstrand. (df4b7af) 2014-08-15 CI bot * Releasing 0.103+14.10.20140815.1-0ubuntu1 (5e24cd1) 2014-08-15 Jussi Pakkanen * Merge lp:~xnox/mediascanner2/missing-dev-depends, lp:~jamesh/mediascanner2/model-auto-update, lp:~jpakkane/mediascanner2/libc++ and lp:~jpakkane/mediascanner2/premountwatcher Approved by: Jussi Pakkanen, PS Jenkins bot (b6074ea) 2014-08-15 James Henstridge * Add back the filled signal since music-app started using it. We can remove it when they update. (490e0e0) * Update change log. (5679f8d) 2014-08-15 Jussi Pakkanen * Handle the case when /media/username does not exist when Mediascanner first starts up. (399fc72) * Minor fixes for libc++. (2eb64eb) 2014-08-15 James Henstridge * Automatically update models when the daemon sends the InvalidateResults D-Bus signal. Add a status property to models to let loading progress be tracked. Rename rowCount property to count, keeping the old name around for compatibility. (f49b5f9) 2014-08-15 Dimitri John Ledkov * Add missing dependencies to libmediascanner-2.0-dev package. (58218f9) 2014-08-14 Jussi Pakkanen * Make GSource's unique_ptr always destroy it fully. (bbd9671) * Fixed issues raised by James. (648ca48) 2014-08-13 Jussi Pakkanen * Print log message when /media/username does not exist on startup. (6c2e5f6) 2014-08-12 Jussi Pakkanen * The Columbo fix. (b449e53) * Handle the case where /media/username does not exist on startup. (a9d2be6) 2014-08-12 James Henstridge * Rename method and member based on Jussi's review. (d0eea79) 2014-08-11 Jussi Pakkanen * A few fixes for libc++. (db32a92) 2014-08-07 James Henstridge * s/rowCount/count/ in the QML tests. (3c3ff5c) * Make sure AdditionEvent::error is initialised. (aa4a72e) * Update qmltypes. (1cc7135) * No need to hold onto exception pointer in event. (8b73ef7) * Expose error state through the model "status" property. (7b3d5e2) * Replace the StreamingModel::filled signal with a status property with a corresponding statusChanged notification signal, similar to Image. This should allow hooking up progress UI. (a9aedb5) * Merge from trunk (4b82feb) * Hook the MediaStoreWrapper::updated signal to the StreamingModel::invalidate slot when setting the associated store so that updates are propagated to the models. (e8526f8) * Simplify the D-Bus signal connection code, and have the bus filter the signals we receive by argument. (e8b6de1) * Add an updated() signal to MediaStoreWrapper, emitted when mediascanner-service sends out the ResultsInvalidated signal. (a37d96d) 2014-08-06 James Henstridge * Have mediascanner-service register a name on the session bus (different to what mediascanner-dbus uses), and send the D-Bus signals directly. (9b238b0) 2014-08-05 CI bot * Releasing 0.102+14.10.20140805-0ubuntu1 (9e1de26) 2014-08-05 James Henstridge * Fix off by one error when appending new rows to QML models. Fixes: 1350529 Approved by: Pete Woods, PS Jenkins bot (d0126b3) * Fix off-by-one error when signaling which rows have been inserted in the model. The third argument to beginInsertRows() is the index of the last row being inserted, rather than the index after. (ed21a3a) 2014-08-01 Dimitri John Ledkov * Add missing dependencies to libmediascanner-2.0-dev package. (3162184) 2014-07-30 CI bot * Releasing 0.102+14.10.20140730.1-0ubuntu1 (1b6cefa) * Replaced 0replaceme with real versions. (876d24e) 2014-07-30 Jussi Pakkanen * Merge all approved pending branches. Approved by: PS Jenkins bot (8b46a24) 2014-07-30 James Henstridge * Bump soname. (14a8440) 2014-07-25 James Henstridge * Update changelog. (c1f60d3) 2014-07-25 Jussi Pakkanen * Archive items on unmount. (cfee73b) * Archive items on external drives when they are unmounted. (9003f2c) 2014-07-25 James Henstridge * Update version number in CMakeLists.txt (f794828) * Update changelog. (2989cf8) * Add MediaStore::queryArtists() method, needed for the new scope design. (9ccf45f) 2014-07-25 Jussi Pakkanen * Install Filter.hh. (0072534) * Fixes for various issues found by flint++. (163752a) 2014-07-25 James Henstridge * Stream data to the QML models in chunks through a background thread using QtConcurrent. (8be1733) 2014-07-25 Jamie Strandboge * Update to in archive version (0.101+14.10.20140627-0ubuntu2). (6247894) 2014-07-25 Jussi Pakkanen * Added an autopkgtest. (fe45b37) 2014-07-25 James Henstridge * Fix up symbols for queryArtists() method. (ce96b58) * Install Filter.hh. (b440801) * Add queryArtists to D-Bus interface. (6578690) * Add queryArtists() method. (0528e01) 2014-07-24 James Henstridge * Merge lp:~jamesh/mediascanner2/streaming-model (1bcea83) 2014-07-24 CI bot * Releasing 0.101+14.10.20140724.1-0ubuntu1 (96a3e78) * Adjust apparmor configuration for MTK device (2dbd32e) 2014-07-24 Alfonso Sanchez-Beato * Adjust apparmor configuration for MTK device (3c9ef0f) 2014-07-23 Jussi Pakkanen * Install Filter.hh. (8e7a674) 2014-07-21 CI bot * Releasing 0.101+14.10.20140721-0ubuntu1 (5b25b1c) 2014-07-21 Jussi Pakkanen * Specify GCC 4.9 explicitly to prevent ABI breakage. Approved by: James Henstridge (2aaee52) * Bump dbus-cpp dependency. (0175a43) 2014-07-17 James Henstridge * More review fixes from Jussi. (d50885d) * Update based on review comments. (bcf619f) 2014-07-16 Timo Jyrinki * Releasing 0.101+14.10.20140715 (8c85ef2) 2014-07-16 Jussi Pakkanen * Various fixes for flint++ issues. (f6e03d0) * Always include class header first in the corresponding implementation file. (1ca584c) * Use nullptr consistently. (d4126d3) 2014-07-15 CI bot * Releasing 0.101+14.10.20140715-0ubuntu1 (8bed854) * Rebuild project du to changes in the dbus-cpp abi. Approved by: Jussi Pakkanen (92e8058) 2014-07-10 James Henstridge * Update symbols. (dd7c198) * Fix songsearchmodel test. (24bab92) * Short circuit the concurrent thread when there is no store. (4d59147) * Update qmltypes. (5126ec0) * Port SongsModel and SongsSearchModel to StreamingModel. (0532763) * Port AlbumsModel to Streaming code. (58fdd7e) * Port Artists model over to streaming model. (c19b3a6) * Port GenresModel over to StreamingModel base class. (3454a6a) 2014-07-07 James Henstridge * Use the limit/offset options in the filter object rather than using a separate argument. (5e836ce) 2014-07-04 James Henstridge * Add limit and offset to mediascanner::Filter, and update D-Bus codec to handle it. (1cc7c41) 2014-07-02 CI bot * Releasing 0.101+14.10.20140702-0ubuntu1 (14b9b63) 2014-07-02 James Henstridge * Update the FTS tokenizer to work with SQLite 3.8.5, and reenable the MediaStoreTest.query_short test. Bump libsqlite3-dev build dependency to 3.8.5 too. (4795814) 2014-07-01 James Henstridge * Bump libsqlite version requirements. (c5d7016) * Fix FTS tokenizer to work with new SQLite. (117633a) 2014-07-01 Ricardo Salveti de Araujo * releasing package mediascanner2 version 0.101+14.10.20140627-0ubuntu3 (e752065) * apparmor: add missing proc file used by the mediatek soc (f6f2012) * releasing package mediascanner2 version 0.101+14.10.20140627-0ubuntu2 (4a5d43b) 2014-06-27 Jamie Strandboge * debian/usr.bin.mediascanner-service-2.0: couple more minor updates for (mostly) noisy denials - use consoles abstraction instead of just the pts interface since we need /dev/tty when crashing - finetune orcexec rules by allowing 'm' in /tmp but explicitly deny the others - allow non-owner read of @{PROC}/cmdline - use attach_disconnected for /dev/socket/property_service (c3e16b8) 2014-06-27 CI bot * Releasing 0.101+14.10.20140627-0ubuntu1 (feed45c) * Disable short query test to work around a behavioral change in SQLite.; debian/usr.bin.mediascanner-service-2.0: updates for libhybris based on work by Ricardo Salveti. (LP: #1334940) (31c8b85) 2014-06-27 Jamie Strandboge * merge lp:~jpakkane/mediascanner2/sqliteworkaround then update changelog to use UNRELEASED (00bc22f) 2014-06-27 Jussi Pakkanen * Changelog update. (40f263c) * Apparmor fix from jdstrand. (36c73f7) * Disable short query test temporarily due to sqlite issue. (6a3020a) * Back to no :native. (445de47) * Update packaging to keep supporting cross-compilation. (e1231fc) 2014-06-26 Jussi Pakkanen * Explicitly select GCC 4.9 to avoid ABI transition issues. (0bb5c15) 2014-06-25 CI bot * Releasing 0.101+14.10.20140625-0ubuntu1 (13b1c36) * Update symbols (dad9ed7) * * add AppArmor profile (LP: #1319065) - add debian/usr.bin.mediascanner-service-2.0 - debian/control: Build-Depends on dh-apparmor - debian/rules: update override_dh_installdeb to use dh_apparmor - debian/mediascanner2.0.dirs: add etc/apparmor.d - debian/mediascanner2.0.install: install profile in to place Fixes: 1319065 (b2f0f6a) 2014-06-25 Jamie Strandboge * * add AppArmor profile (LP: #1319065) - add debian/usr.bin.mediascanner-service-2.0 - debian/control: Build-Depends on dh-apparmor - debian/rules: update override_dh_installdeb to use dh_apparmor - debian/mediascanner2.0.dirs: add etc/apparmor.d - debian/mediascanner2.0.install: install profile in to place Fixes: 1319065 (f6569ad) 2014-06-25 James Henstridge * Enable the QML plugin to pick between two MediaStore backends: the direct disk backend and the D-Bus interface. The choice is made via the MEDIASCANNER_USE_DBUS environment variable, and defaults to direct access. (c3e163c) 2014-06-24 Jamie Strandboge * * add AppArmor profile (LP: #1319065) - add debian/usr.bin.mediascanner-service-2.0 - debian/control: Build-Depends on dh-apparmor - debian/rules: update override_dh_installdeb to use dh_apparmor - debian/mediascanner2.0.dirs: add etc/apparmor.d - debian/mediascanner2.0.install: install profile in to place (abec60c) 2014-06-24 James Henstridge * Address Jussi's review comments. (4125637) * Change environment variable name. (be5587f) * Update symbols file. (a831d7f) * Pick which MediaStore implementation to use based on an environment variable. Also run the QML tests twice: once for each backend. (f284ce3) * Add an abstract base class shared by mediascanner::MediaStore and mediascanner::dbus::ServiceStub. (c6e4a4f) 2014-06-13 Jussi Pakkanen * Documentation fix. (42a29e8) * Build a media file, too. (33997fc) * Added an autopkgtest. (c306ec8) 2014-06-13 CI bot * Releasing 0.101+14.10.20140613-0ubuntu1 (f0a71fd) * Update symbols (1517850) 2014-06-13 James Henstridge * Add support for scanning photos using libexif. Adds libexif-dev as new build dependency. Fixes: 1256334 (799acfa) * Catch exceptions in the QML plugin, and print a warning instead. QML applications terminate when C++ exceptions bubble up, which can not be recovered from. Fixes: 1326753 (e6a801d) * Increase the timeout on D-Bus method calls. Fixes: 1326753 (4374051) * Fix typo in the symbols file. (aafc784) 2014-06-11 James Henstridge * Add new symbols, and sort/uniq the list. (eef7f05) * Merge lp:~jpakkane/mediascanner2/symbols (c21657d) * Fix up misuse of logging API in AlbumsModel. (2ec024f) * Longitude and latitude should be doubles. (630cb37) * Fix up EXIF date extraction routine based on review from Jussi. (d7907d4) * Make sure the stream info list gets freed. (75852ec) 2014-06-10 James Henstridge * Increase the timeout for dbus-cpp method calls. (ba899da) * Protect MediaStore calls with try/catch blocks, since QML code will terminate if the exception bubbles up. (2707a01) 2014-06-07 James Henstridge * Expose new metadata via QML API too. (0e652f4) * Expose ImageMedia constant to QML. (60f03f8) * Adjust the indexes so they are used for the list*() methods. (6ffc1a9) * Also scan the pictures directory. (6ce28d6) * Include new metadata fields in D-Bus API. (630334f) 2014-06-06 James Henstridge * Update MediaStore tests to include round trip testing of image metadata. (fe997c1) * Add support for additional metadata fields to media store. (05d0f5c) * Build-depend on libexif-dev. (99a038f) * Remove imagetest, since the code has been incorporated into MetadataExtractor. (0e9638f) * Include tests for extracting metadata from photos. (d3394cd) * Add some test images. They have been shrunk to keep size down, but EXIF data has been left untouched. (5c9c256) * Add EXIF parsing code to MetadataExtractor, and also extract height and width from videos. (3515f56) * Add height, width, latitude and longitude fields to MediaFile. (20d0a98) 2014-06-05 James Henstridge * Fix up handling of GPS coordinates and width/height. (58be0fd) 2014-06-05 Jussi Pakkanen * Added symbol files and removed one unused old test binary that no longer linked. (ae32fd8) 2014-06-05 James Henstridge * Use libjpeg to read the file size. (9076b12) * Merge from lp:~jpakkane/mediascanner2/imagetest (c407ef4) 2014-05-30 CI bot * Releasing 0.101+14.10.20140530-0ubuntu1 (5a7d212) 2014-05-30 James Henstridge * Merge lp:~jpakkane/mediascanner2/nomedia, lp:~jamesh/mediascanner2/mediafile-constructor, lp:~jpakkane/mediascanner2/clangfixes, lp:~jpakkane/mediascanner2/nullguard, lp:~jpakkane/mediascanner2/projectname, lp:~jpakkane/mediascanner2/pruneblocked, lp:~jamesh/mediascanner2/dbus-transport, lp:~jamesh/mediascanner2/dbus-apparmor, lp:~jamesh/mediascanner2/media-filter (0937a45) * Fix typo in service name. (96fb3ef) 2014-05-29 James Henstridge * Use full libdir. (e56aedc) * Fix D-Bus name in service file. (f0bfb7a) * Update changelog for lp:~jamesh/mediascanner2/media-filter (96d29e1) * Introduce a mediascanner::Filter type to hold search parameters for the various MediaStore::list*() methods, and expand them to handle genres. The new API also distinguishes between an unset filter and a filter set to "". Make similar changes to the QML API, and add notify signals to the rowCount properties. (71fe545) * Update Debian change log for merged branches. (be0e2d8) * Limit access to the MediaScanner D-Bus interface from confined processes. (b9634d8) 2014-05-29 Jussi Pakkanen * Prune those files that have a scan block file on their path when restoring. (b889be5) * Apply paper bag to head. (28a988c) 2014-05-29 James Henstridge * Add a D-Bus service exposing the MediaStore, and move the QML binding over to using it. (eb9d1a3) 2014-05-29 Jussi Pakkanen * Fix CMake Project name. (2768b18) * Guard against empty MediaFileBuilders. (cea946e) * Fix a few warnings emitted by clang. (fed1b76) 2014-05-29 James Henstridge * Get rid of the direct constructor for MediaFile, forcing creation to go through MediaFileBuilder (which won't break when we add more metadata fields). (8d38753) 2014-05-29 Jussi Pakkanen * Add support for .nomedia files to prevent chosen subtrees from being scanned. (4f1e3e4) 2014-05-29 James Henstridge * Merge ~jamesh/mediascanner2/dbus-apparmor (8095bca) * If apparmor is not enabled, treat d-bus peers as unconfined. (4c69f5f) * Add notify signal for rowCount properties. (19e9fbf) * Add genres model. (34f2f0a) * Test genre property on albums and songs models. (18ed420) * Actually reference plugin.qmltypes in qmldir (3fdff26) * Add genre property to other models and update QML types. (335d387) 2014-05-28 James Henstridge * Add genre property to ArtistsModel. (6520fb2) * Distinguish between unset and empty string filters. (06343b9) * Use mediascanner::Filter in the D-Bus API and QML plugin. (c9601c8) * Include a timeout on the QML test. (dd04236) * Convert listing methods over to using mediascanner::Filter. (57589a4) * Add support for serialising/deserialising mediascanner::Filter with dbus-cpp. (9d98b26) 2014-05-27 James Henstridge * Add MediaFilter class. (e76b0a1) * Limit access to the D-Bus interface from confined applications. (9281ca4) 2014-05-24 James Henstridge * Remove some excess debug logging. (fa269f0) 2014-05-23 James Henstridge * Add a comment explaining why the helper D-Bus library is linked with -fPIC. (1639db4) * Build-Depend on libproperties-cpp-dev. (0e2e85d) * Build-Depend on libdbus-1-dev. (c7113eb) * Depend on libdbus-cpp-dev (for dbus service) and dbus (for tests). (e51cc6e) * Install dbus service along with activation config file. (af89063) * Make QML tests run against D-Bus service. (2197059) 2014-05-22 James Henstridge * Make the QML binding access the media store over D-Bus. (66a8667) * Compile dbus code into a static library so it can be used by the QML binding. (bfaa4f5) * Add stub class for D-Bus interface. (8c73715) 2014-05-21 James Henstridge * Fill out rest of MediaStore API. (bff4a23) 2014-05-20 James Henstridge * Start of MediaScanner d-bus interface. (9eb73a0) 2014-05-16 Jussi Pakkanen * Fixed CMake project name. (049c9e2) 2014-05-15 Jussi Pakkanen * Grab basic data. (7b6318a) * Simple image metadata extractiont tool. (3ba0d85) * Guard against empty MediaFileBuilders. (230f3c9) 2014-05-15 James Henstridge * Remove comment about multiple calls to MediaFileBuilder::set*() methods raising an exception. (374665c) 2014-05-14 Jussi Pakkanen * Fix clang warnings in third party file. (79fe343) * Fixed forward declaration of DetectedFile. (03b589c) * Merged trunk. (0bc3ee7) 2014-05-14 James Henstridge * Get rid of unneeded build() call. (54a128a) * Move fallback title and album_artist code to MediaFile construction time: this is safe because the properties can't be changed after construction. (5f17ef8) * Add a simple move constructor for creating a MediaFile from a MediaFileBuilder. (aed4ea6) * Add equality operation to MediaFilePrivate. (6f41529) * Add missing destructor to MediaFileBuilder. (293b52f) 2014-05-12 James Henstridge * Get rid of long constructor on MediaFile. (19c97fa) * Switch qml test suite over to MediaFileBuilder. (91bca8b) * Switch MediaStore over to using MediaFileBuilder. (1538284) * Allow chaining of MediaFileBuilder methods, and convert test_mediastore tests over. (604bc00) * Make MediaFileBuilder using MediaFilePrivate too, simplifying construction of MediaFile instances. (39fe19b) * Move MediaFile fields to a private struct. (22a645a) 2014-05-09 Jussi Pakkanen * Cache scanblock info. (3be9a0e) * Check scan blocks on restore. Also unify the two util files. (b0a44d4) 2014-05-08 CI bot * Releasing 0.100+14.10.20140508-0ubuntu1 (8717791) 2014-05-08 James Henstridge * Add genre and discNumber metadata fields to the media index, and expose it in the QML binding. (8563f05) 2014-05-08 Jussi Pakkanen * Use generator for file traversal instead of a vector. (ec38265) * Don't scan optical discs. (62794fe) 2014-05-08 James Henstridge * Update qmltypes. (53eda2d) 2014-05-06 James Henstridge * Test that the get() method works. (f5cf2aa) * Add get() methods to models. (824d75f) 2014-05-06 Jussi Pakkanen * Added support for a .nomedia file which tells mediascanner not to scan specified directories. (2d6a492) 2014-05-06 James Henstridge * Expose row counts of models to QML. (d55f5b1) * Expose genre and discNumber through QML interface. (b3cf5a1) * Extract genre and disc number from media. (ca0b487) * Store genre and disc_number in index. (2a6b390) * Add genre and disc_number properties to MediaFile. (74f3265) 2014-05-05 Jussi Pakkanen * Moved generator's functionality inside scanner. (490a8b8) 2014-05-02 Jussi Pakkanen * Removed useless if. (6bf1d68) * Allocate directory entry only once. (fb619d9) * Added test for is_optical. (6555644) 2014-04-28 Jussi Pakkanen * Store some more state to make it really work this time. (0d061b0) * Use unique ptr. (713c10a) * Use the generator for traversal. (96565ef) * Create a generator for file traversal. (07202ea) 2014-04-09 Jussi Pakkanen * Skip top level mounts that look like optical discs. (1ee715a) 2014-04-03 CI bot * Releasing 0.100+14.04.20140403-0ubuntu1 (be2ebbf) 2014-04-03 James Henstridge * Before using GStreamer to extract metadata from a file, print a log message. This is intended to help track problems with metadata extraction. (c458a08) 2014-04-03 Jussi Pakkanen * The output value of stat is undefined when the stat call fails. So don't use it. (500940b) 2014-04-03 James Henstridge * Remove the Unity scope found in src/utils: it looks like it has suffered from bit rot and has never been used (we have unity-scope-mediascanner instead). (c11920f) 2014-04-03 Jussi Pakkanen * Avoid assert crash by not trying to unregister subvolumes that were skipped due to looking like root subdirs. Fixes: 1294193 (03ef0eb) 2014-04-03 James Henstridge * Add a full stop to the end of the log message. (140511a) 2014-04-02 James Henstridge * Before using GStreamer to extract metadata from a file, print a log message. This is intended to help track problems with metadata extraction. (d628c55) 2014-04-01 Jussi Pakkanen * And a newline. (fafe06a) * When stat fails, the result struct is not populated so do not use it. (e811b8a) 2014-03-28 James Henstridge * Remove unused (and seemingly bitrotten) test program. (03e866f) * Remove the (unused) demo scope: it uses the old scopes API, and is incomplete compared to the scope provided by unity-scope-mediascanner. (6e99ebd) 2014-03-24 CI bot * Releasing 0.100+14.04.20140324-0ubuntu1 (a0332e3) 2014-03-24 James Henstridge * Expose an "art" role/property on album and song models, returning a URI that can be used with a QML Image component. The actual image provider comes from ubuntu-ui-toolkit. (505591f) * Update plugin.qmltypes. (93b72aa) * Add album art URI roles to MediaFile and Album models. (25bd5c0) 2014-03-21 Jussi Pakkanen * Avoid assert crash and improve logging. (d15419f) 2014-03-14 CI bot * Releasing 0.100+14.04.20140314-0ubuntu1 (ea1d9d4) 2014-03-14 Jussi Pakkanen * Delay signal handling initialisation. (58f1a4e) * Skip subdirectories that look like root directories. (f102357) * Keep going if inotify watch creation fails. (6547bd9) * Initialise signal handling later. (6d410d1) 2014-03-12 Jussi Pakkanen * Let us not forget Windows drives. (554ea6c) * Added test for rootlike skipping. (53b7fc9) 2014-03-11 Jussi Pakkanen * Skip subdirectories that look like top level root directories. (9af8f57) 2014-03-10 Jussi Pakkanen * Keep going if watch creation fails. (0ebc6ba) 2014-03-10 CI bot * Releasing 0.100+14.04.20140310-0ubuntu1 (3df9df7) * No change rebuild against Qt 5.2.1. (fa34b53) 2014-03-07 CI bot * Releasing 0.100+14.04.20140307.1-0ubuntu1 (51a39a2) 2014-03-07 Michal Hruby * Fix incorrect depends (54311c3) 2014-03-07 James Henstridge * Merge lp:~jamesh/mediascanner2/qml-plugin, lp:~jamesh/mediascanner2/short-searches, lp:~jpakkane/mediascanner2/errmsg, lp:~jpakkane/mediascanner2/cleanshutdown, lp:~jpakkane/mediascanner2/threadness and lp:~jamesh/mediascanner2/glib-main-loop (42313c4) 2014-03-07 Michal Hruby * Fix incorrect depends (7e67d1a) 2014-03-07 James Henstridge * Update change log to cover merged branches. (7448a22) * Convert the scanner daemon over to using the glib main loop rather than a hand rolled one, and limit invalidation signals to be sent at most once a second. (ddac597) 2014-03-07 Jussi Pakkanen * Made SQLite db safe to access from multiple threads. (0d5d884) * Shutdown cleanly on signals. (70e4e49) * Add sqlite error string to exception. (8014f3a) 2014-03-07 James Henstridge * Relax the rules for tokenising the input when it ends with a short token followed '*' wildcard. This allows us to produce results for short strings when doing incremental searches. (9860b5d) * First stages of a QML plugin for the Media Scanner. At present it only exposes the query() and lookup() interfaces. (fd72714) 2014-03-06 James Henstridge * Update comment to match new logic. (ae448e0) * Don't free the GError if it is NULL. (7d9ca52) * Make InvalidationSender send dbus signals from a timeout callback, to batch multiple invalidations that occur within a short time frame. (ce82808) 2014-03-05 James Henstridge * Merge lp:~jpakkane/mediascanner2/cleanshutdown and convert shutdown changes over to using g_unix_signal_add(). (5f6c0b2) * Remove SubtreeWatcher::pumpEvents() (1ab6af0) * Switch ScannerDaemon::run() over to running the glib mainloop, and switch mountfd over to a main loop source too. (1564e2e) * Fix cast calls. (6d2ac93) * Make SubtreeWatcher register a glib mainloop source on construction, and give it direct access to the InvalidationSender object. (6e02b72) * Rather than each subtree watcher running select() again, make ScannerDaemon::run() check the FD set before asking each watcher to process events. (0da8383) 2014-03-03 Jussi Pakkanen * A few cleanups. (7a81bdd) * A few leak fixes uncovered by Valgrind. (a74d1b9) 2014-02-28 James Henstridge * Add artists model. (830932b) * Add support for listing artists. (08913c7) 2014-02-27 James Henstridge * Add a test for the MediaStore QML methods. (3deff24) * Update plugin.qmltypes (fff5463) * Add album list model. (94a2b49) * Add songs listing model. (970cc57) * Add listSongs() API to MediaStore. (fdbfba9) 2014-02-26 James Henstridge * Merge from trunk (40c8408) * Build depend on qtdeclarative5-test-plugin. (ac517be) * Run tests under minimal platform module. (756e182) 2014-02-25 James Henstridge * Add base class for album models. (14b21f3) * Add test harness for QML tests. (4a21808) 2014-02-24 Jussi Pakkanen * Do not throw in destructor. (cb8b0b5) * Made sqlite database queries thread safe. (4fbde47) * Shutdown cleanly when signals happen. (556ab46) * Add sqlite error string to exception description. (4432d2c) 2014-02-24 James Henstridge * Only output short tokens of at least one character long. (037b6e4) * Add a test for short queries. (389a3f7) * The only case where Mozilla was making use of wildcard searches was to match single character CJK queries, where they were expanded to "x*" to match bigrams starting with that character. Hence the check that the token was at the start of the string and contained a single character encoded in UTF-8 as three or more bytes. (c365380) 2014-02-20 CI bot * Releasing 0.99+14.04.20140220-0ubuntu1 (4037e61) 2014-02-20 Jussi Pakkanen * Send invalidation signal once the db is fully populated. (21bbeee) * Nil merge to get on the release train. (2ecee7a) 2014-02-20 James Henstridge * Add an abstract list model for searching for songs. (753d9b6) 2014-02-19 Jussi Pakkanen * Set invalidation once data is read from disk. (8a5e715) * Removed empty grilo subdirectory. (5b12188) * Fix path where media cache is stored. (37001ec) * Use lstat to avoid loops. (8106658) 2014-02-19 James Henstridge * Move plugin classes to a namespace. (35ce37b) 2014-02-18 Jussi Pakkanen * Use lstat so we do not get into eternal loops. (9929a91) * Renamed storage path. (8870aaa) 2014-02-17 Jussi Pakkanen * Added const qualifiers to methods and made album immutable just like mediafile. (ace7e0c) * Merged trunk. (21972ee) 2014-02-17 James Henstridge * Add a limit argument to MediaStore::queryAlbums(), and ensure that it returns some results when given an empty query string. (e3159b6) * Merge from trunk (cf6bc42) 2014-02-12 James Henstridge * Ensure that queryAlbums() returns some results for empty queries. (8c0fcd0) * Ensure that blank queries for albums returns some results. (457759e) * Add a limit argument to the queryAlbums() method. (a855470) 2014-02-05 CI bot * Releasing 0.99+14.04.20140205-0ubuntu1 (1b8983d) * Null merge to get changes through CITrain (cb5219c) 2014-01-31 Jussi Pakkanen * Merged trunk. (f068aaa) * Simpler header reorg. (3fa7cdf) 2014-01-31 James Henstridge * Update to match new namespacing. (022b3c4) * Merge from trunk (264e610) 2014-01-31 Jussi Pakkanen * Add a mediascanner namespace. (b4125f4) 2014-01-30 James Henstridge * Add a limit argument to MediaStore::query(), and ensure that it produces some results in response to an empty query. (845708a) 2014-01-30 Jussi Pakkanen * Constify and immutabilize. (5e4e0de) 2014-01-30 James Henstridge * Produce some results in response to an empty query, and add tests for this case and the limit clause. (2f9fa42) * Add a limit argument to the query() method. (e306a7d) 2014-01-29 Jussi Pakkanen * Moved non-installed headers to internal subdirectory. (80dbde6) * Install MediaFileBuilder's header file. (e1290e1) 2014-01-22 Jussi Pakkanen * Simple class for sending result set invalidation messages. (37c5d16) 2014-01-20 Jussi Pakkanen * Grabbed invalidation code from old branch. (9d4d790) 2014-01-17 Automatic PS uploader * Releasing 0.99+14.04.20140117-0ubuntu1 (revision 197 from lp:mediascanner2). (5ddf5a1) * Releasing 0.99+14.04.20140117-0ubuntu1, based on r197 (2463413) 2014-01-16 James Henstridge * Add LGPL headers to QML plugin code. (c683253) * Merge from trunk (057b716) * Fix the licensing on the public library so that it is correctly labelled as LGPL, like the old mediascanner code base. (5efef37) 2014-01-16 Jussi Pakkanen * Moved class documentation to its proper place. (6597d4a) * Added namespace. (e48ad53) 2014-01-15 James Henstridge * Fix a few issues that prevent the scanner daemon from running when stdin is redirected to /dev/null. (5845dc0) * Remove the "Press enter to quit" feature, since it fails if stdin is hooked up to /dev/null. (0e020b0) * Don't use getlogin() to determine current user, since it fails when stdin is redirected. (b747421) * The contents of src/mediascanner (the public interface) really should be LGPL, like the library it is replacing. (6ce863d) 2014-01-10 Automatic PS uploader * Releasing 0.99+14.04.20140110-0ubuntu1 (revision 194 from lp:mediascanner2). (27726f5) * Releasing 0.99+14.04.20140110-0ubuntu1, based on r194 (271222d) 2014-01-10 Ɓukasz 'sil2100' Zemczak * We no longer ship any LGPL sources. Also, fix packaging naming in some places - along with Vcs-* (ce4d2a1) * Binary name of mediascanner2.0 is ok, but a source package shouldn't include a major and minor version - this changes far too rapidly. So, maybe rename it to mediascanner2 instead?. (cc45c59) * Binary name of mediascanner2.0 is ok, but a source package shouldn't include a major and minor version - this changes far too rapidly. So, maybe rename it to mediascanner2 instead? (8e24e0e) 2014-01-02 James Henstridge * Include the generated plugin.qmltypes file under source control, and add a "make update-qmltypes" command to update it. (0e4d944) * Build-depend on QtQuick, since qmlplugindump seems to need it. (1a3d1e9) * Rearrange source tree to match QML import paths so we can generate the plugins.qmltypes file. (a4012d0) * Merge from trunk. (0d2596d) 2013-12-29 Ɓukasz 'sil2100' Zemczak * Add bzr-builddeb and the bootstrapping commit for daily-build. (4d215e7) 2013-12-20 Ɓukasz 'sil2100' Zemczak * Add bzr-builddeb and the bootstrapping commit for daily-build (942488c) 2013-12-18 James Henstridge * Adjust packaging to allow parallel installation, and add Upstart session job. (aae6154) * Add Upstart session job, parallel installable with old media scanner. (fb30c57) * Name the actual scanner service similar to the original one, with a version suffix appended. (199bbf7) * Rename source and binary packages so they don't conflict with old media scanner. (9cc7474) * Add remaining MediaFile properties to QML wrapper. (07ec732) * Explicitly set ownership of MediaFileWrapper objects. (451e5b5) 2013-12-17 James Henstridge * Version the QML plugin package and installation dir. (0502985) * Correctly namespace the QML file. (a6a307b) * Depend on qtdeclarative5-dev too. (397e1e3) * Update packaging to cover QML plugin, and set plugin version to 0.1. (4a2b4d3) 2013-12-16 James Henstridge * Expose a bit more through the MediaFile wrapper, and expose media type through MediaStore.query(). (46d7a52) 2013-12-15 James Henstridge * Add qmldir file, and update wrapper constructors. (7b2c29b) 2013-12-14 James Henstridge * Use automoc for mediascanner-qml target. (4ae1d9b) 2013-12-13 James Henstridge * Split out into separate files. (8896575) * Skeleton QML wrapper for MediaStore class. (0898b18) 2013-12-03 James Henstridge * In SubtreeWatcher, ignore inotify events for watches we are no longer interested in. (e69a696) * Run the test suite in series, and always include the test output in the build log. (e36368e) * Ignore inotify events directed at unknown watches. This can happen when there are queued events for watches we've since removed. (23661b2) 2013-12-02 Jussi Pakkanen * Made MediaFile immutable. (6a0df57) * Fixed initialisation order. (d8d58c4) * Use initialisers in a constructor. (ce1f2be) * Make MediaFileBuilder take the filename in a constructor because that is mandatory. (1ced6b7) * Return of getUri. (b160c65) 2013-12-01 Jussi Pakkanen * Use EXPECT_TRUE/FALSE instead of EXPECT_EQ(true/false, ...). (8b13f3b) 2013-11-29 Jussi Pakkanen * Use EXPECT_TRUE/FALSE. (c4c42fe) * Removed unnecessary includes. (f4ab443) 2013-11-28 Jussi Pakkanen * Capitalisation fix. (443551c) * Update tests. (3ab9e33) * Merged trunk. (d1eb1cb) 2013-11-28 James Henstridge * Store the etag of files in the MediaStore so the scanner can detect whether it is necessary to redo the metadata extraction on files on start up. (5a291dc) 2013-11-28 Jussi Pakkanen * Made MediaFiles immutable. (5003765) * Finish MFBuilder. (5f651a9) * Some setter functions. More to come. (e64a8ac) * Started creating an mfbuilder class. (b2aaa1e) 2013-11-28 James Henstridge * Add a test for the skipping of changed metadata. This really needs access to some scannerdaemon.cc methods to be more robust. (1f85ad3) * Add a MediaStore.lookup() method to look up metadata by a file name. (8a61978) * Have MetadataExtractor.detect() return a DetectedFile struct, and have MetadataExtractor.extract() convert that to a MediaFile. (2fdbbea) 2013-11-27 James Henstridge * Rather than opening files for read access in MediaScanner.pruneDeleted(), just check if the file exists with access(). (f8dde9e) * Only perform metadata extraction if the file has changed. (8d89f26) 2013-11-26 James Henstridge * Add a MediaStore.getETag() routine to get the stored etag for a file name, if it exists. (fa66f08) * Make MetadataExtractor.extract() annotate a MediaFile rather than create a new one. (b60ca09) * Delete the media_fts table in the upgrade logic. (3854001) * Convert Scanner.scanFiles() to use MetadataExtractor.detect() instead of FileTypeDetector. (6ce0d8c) 2013-11-25 James Henstridge * Remove some includes of FileTypeDetector.hh in tests. (b146269) * Rename MetadataExtractor.stat() to detect(), and make it use the fast content type. (65062f1) * Move GFile logic into MetadataExtractor so we can record the content type and etag. Expose this as a separate entry point so that we can extract this data without continuing on to the expensive metadata extraction. (34eee75) * Store content type and etag in MediaStore. (a568be9) * Add content_type and etag members to MediaFile (21eba0d) 2013-11-23 James Henstridge * Add a constructor for MediaStore that opens the default database. This removes the need for users of the client locate the library themselves. (9c906dc) 2013-11-22 James Henstridge * Update the query test program to open the default database. The MEDIASCANNER_CACHE environment variable can be used to point it at different databases. (1a0bd82) * Make the scanner daemon open the default database, and look up the user's music and videos directories using the XDG user dirs spec for better i18n support. (989a7fa) * Add a second constructor that opens the default database. (40d2dc1) 2013-11-21 James Henstridge * Add a ranking function based on the one in the SQLite documentation (http://sqlite.org/fts3.html#appendix_a), and use it to rank the results of MediaStore.query(). (5f7fc24) * Add a test to show that the ranking is being applied and weights matches in the various fields correctly. (0031b19) 2013-11-20 James Henstridge * Add tests to show that searches by artist or album name work. (cff6b64) * Introduce a ranking function and use it for query(). (b04679b) 2013-11-18 Jussi Pakkanen * Pass test defines through a header file rather than command line arguments. (d8d8102) 2013-11-18 James Henstridge * Add a method to get a URI for a MediaFile instance. (6143040) 2013-11-15 James Henstridge * Ignore files created by package build. (623877c) * Update copyright file. (6f71c0e) * pc file is in binary dir. (bc34faf) 2013-11-14 James Henstridge * Add pkg-config file for mediascanner. (f7ba5e0) * Add test video files from old media scanner code base. (db1afc0) * Move metadata extractor tests into a separate file. (4589de4) * Move test data to test/media/ (884d260) 2013-11-13 James Henstridge * Separate out MediaStore related tests into a separate test program that doesn't link with the scanner daemon code. (489838a) * Use preprocessor defines to pass source/build dir to test rather than environment variables. This lets us easily run the tests directly. (53f5245) * Convert tokenizer registration over to using Statement helper class. (3633feb) * Add license text to branch. (bb3dab8) * Remove unnecessary includes. (2034ad4) * Push LDFLAGS to the various libraries rather than repeating them for each executable. (a979229) * Fix up packaging to include dev package. (c4998ad) 2013-11-12 James Henstridge * Add README for sqlite tokenizer from Mozilla tree. (6d3e492) * install daemon to CMAKE_INSTALL_BINDIR (c393b23) * Move utils. (2dacba7) * Move files related to actual scanning to src/daemon, so src/mediascanner only deals with metadata storage and retrieval: scanning and extraction is the responsibility of the daemon. (7bdd70b) * Move daemon to src/daemon/ (0b5db2d) * Move the media scanner library code to a separate directory to try and separate out what represents the public API. (ee00a56) 2013-11-11 James Henstridge * Add tests for album query interface. (8feb5a6) 2013-11-08 James Henstridge * Add MediaStore methods to search albums. (0fe1a46) 2013-11-07 James Henstridge * CMake fixes suggested by Jussi. (f4a16e6) * Round trip additional music metadata. (49b2d73) * More updates. (2f94efb) * Port insert() over to bound parameters. (5e359b6) * Convert query() and pruneDeleted() over to using bound parameters. (78eca83) * Convert some queries over to using helper class. (a03dc95) * Add small helper class to help run sqlite statements with bound parameters. (e07a0bd) 2013-11-06 Jussi Pakkanen * Some .bzrignore. (b15cbc3) * Check system exit value. (7e536bc) * Something2. (3eaccf9) * Something. (6d12dec) * A few polishing fixes. (5f76d88) * Test extractor in isolation. (440b0b9) * Test detector. (30e49e7) * Cleanupping. (3d39247) * For great destruction. (84e530b) * Subtreewatcher pimpl. (f51a571) * Test that subdirectory deletion is properly detected. (606bc45) * Quote test. (8552cdc) * Add util test. And also an off-by-one fix. (510d473) * Merged changes. (c1f85ca) * Use gtest. (5100fa4) 2013-11-06 James Henstridge * Extract date and track number via GStreamer. Not yet stored in database. (72ec25b) 2013-11-06 Jussi Pakkanen * A dep fix. (0fb212b) 2013-11-05 Jussi Pakkanen * Packaging now works. (e5b89df) * Some basic packaging. (b73b737) * Use own assert that works even when NDEBUG is defined. (fb2977b) 2013-11-04 Jussi Pakkanen * Removed unused mediaroot table remains. (d2b23fd) * Store schema version id in the database to prevent apps from using old data by accident. (b8ec8e7) * Build uris correctly. Again. (fd7df1f) * Gave MetadataExctractor its own private struct. (3b93448) 2013-11-04 James Henstridge * Merge in MetadataExtractor changes from last week that didn't get pushed correctly. (3df6fc8) 2013-11-01 Jussi Pakkanen * Use std::string searcher rather than strncmp. (d7940c0) 2013-10-31 Jussi Pakkanen * A few more finals. (8fbd118) * Vestigial stuff removal. (ad3269d) * Test metadataextractor. (2390c9b) * More hidden file ignoring. (db3bd00) * Made classes final. (60966b0) * Build gstreamer URIs correctly. (07612f8) * Made metadata extractor a class. (c3a5936) * Only traverse subtrees once. (6bcbbf5) 2013-10-31 James Henstridge * Share the GstDiscoverer between metadata extractions. (8e801a6) 2013-10-31 Jussi Pakkanen * Some cleanups. (d8e6c02) 2013-10-31 James Henstridge * Use GIO to query the content type to determine file type rather than working on a small set of file extensions. (1be0cba) * Don't bail if the /media/$user directory doesn't exist. This means mount monitoring won't work, but it allows the home directory to be scanned. (404a339) 2013-10-30 Jussi Pakkanen * Use enum instead of boolean. (8d33f2f) * Can open db as read only. (8659e2f) * Added mounting test. (1b9a9c2) * Remnant removing. (591daed) * More work on single table. (01d975e) * Experiment to use media attic for files that get unmounted. (d42525a) * Store more info in exception message. (f7957cb) * Throw actual exceptions. (1d2aa06) 2013-10-29 Jussi Pakkanen * Tests pass again. (c768548) * More work on single media table. (537b251) * Started work on moving to a single table. (e790a40) 2013-10-28 Jussi Pakkanen * Cleanup. (c191332) * Test that query round trips work. (4d31f56) * Comparison and tests. (14ef3fd) * External db fix. (08e4913) * Moved search inside MediaStore. (2bf9c3d) * Added coverage support. (083e4b2) 2013-10-25 Jussi Pakkanen * Check that files removed and added during downtime are processed. (44d7a5f) * Test that subdir tracking works. (46601a1) * Initialize and check that added files are really added. (a6be80c) * Cleanup. (381773c) * A basic indexing test. (8ecaeb1) * Delete from videos too. (392266f) * Trigger fix. (7c81e10) * Created test audio file. (c69069a) * Test subtreewatcher initialisation too. (028e320) * Added a test. (582c252) * Moved files into src subdirectory. (f999f97) 2013-10-25 James Henstridge * Import the tokenizer used by Mozilla's Gloda search index. (d95c747) 2013-10-24 Jussi Pakkanen * Can query stuff. (0ec2f87) * Write generated data to db. (705bc1c) * Create scalability tester app. (dd8d981) * Use reference instead of pointer. (b50537e) 2013-10-23 Jussi Pakkanen * Some unique_ptr lovin. (ab825ee) * Do video queries too. (c99ade3) * Through the wonders of SQL magic you can now do queries again. (7e39b01) * Put duration to backing store. (0089cc5) * Reworked backing store. (8ff61af) 2013-10-23 James Henstridge * Convert getMetadata() over to using GstDiscoverer. (41ab823) 2013-10-23 Jussi Pakkanen * Let's noexcept love. (ad2c2f5) * Do not store duration yet. Need to figure out table details. (3d6c439) * Determine duration of files. (a10214d) 2013-10-22 Jussi Pakkanen * Watch for changes in backend files. (5354e5c) * Store db files in .cache. (7eb3aa0) * Mount dir fixing. (ef54191) 2013-10-21 Jussi Pakkanen * Palm of face. (4bba8bd) * Use unique_ptrs. (6e2da35) * Close inotify fd. (9169028) 2013-10-18 Jussi Pakkanen * Query mounted volumes too, if they exist. (3faacee) * Query multiple directories. (337e5d6) * Use readdir_r. (d3f0452) * Unique_ptring. (032a3a2) * Use readdir_r. (958e7a4) * Do not add same subdir several times. (6c5a987) * Add those volumes that are already mounted. (d8afc06) * Watch mounts and unmounts. (06c600f) * Made function method. (254ea23) * Can watch arbitrary number of subdirectories. (3cd4bb1) * Made daemon into a dynamic class. (06a18cb) * Query video results, too. (74bb301) * Build title from file name if it is missing. (5352765) * Basic support for video files. (df10bb3) * Do not archive hidden files. (3c6a0ba) 2013-10-17 Jussi Pakkanen * Fffafaffafddddh. (25e2afd) * Micro-optimisation. (9f64107) * Detect files that have been deleted while scanner was down. (55a7078) * Added SQL quote function. (653737c) * Renamed exe. (1a0fe68) * Made scannerdemo work daemon-like. (3982603) * Make subtreewatcher runnable from the outside. (2450f9d) * No longer delete mediastore on start. (a0f444a) * Better error diagnostics. (96dcc34) 2013-09-04 Jussi Pakkanen * Some initialization. (c361d16) 2013-09-03 Jussi Pakkanen * Better error text. (68ae9d3) * Store artist name if it is known. (f6303ce) * Can haz query results. (7f03b3b) * Return dummy data. (eee21f8) * Basic scope skeleton works. (605d3c9) * Scope experiment starts now. (f615023) 2013-08-30 Jussi Pakkanen * Can delete and query files. (1209664) * Consting. (8847edb) * Measure size and print it. (dabf7e1) * Do not run file system watcher for the time being. (e0b3d18) * Use only one query. (ae6cebf) * Open in read only mode. (a5dcea7) * Added query src file to bzr too. (7f57780) * Can haz queries. (c1362d7) * Put data into backing store. (cc5d14f) * Create a backing db. (af90bb1) 2013-08-05 Jussi Pakkanen * Typo fix from jamesh. (7ddba8b) 2013-07-12 Jussi Pakkanen * Comment. (692a2fb) * Close dirs after traversal. (a45cad9) * Betterer meta data. (9a201dc) * Use exceptions for errors. (dc572b0) * Putting pieces together. (f8193a1) * Renamed exe. (e19b5d4) * Work work work. (96fe2d4) * Moved subtreewatcher to a class file. (f6d3250) 2013-07-10 Jussi Pakkanen * Even more cleanups. (c25100b) * Even more inotify flag mangling. (3758f93) * Inotify flag cleanup. (0557a46) * Update watch subscriptions as subdirs come and go. (70eb86f) * Refactoring. (cef76ee) * Print what is happening. (1e752ea) * Watch for it. (fdae716) * Initialise and shutdown inotify. (1b84d50) * Started on subtree change watcher. (1fc0e98) * Extract meta data properly. (090e18a) * Started work on GStreamer based metadata extractor. (d8d8c98) 2013-07-09 Jussi Pakkanen * Can store and query for media files. (38919ab) * Created MediaFile class. (18f062c) * Do not crash. Which is always nice. (21c5333) * Created a test app to watch for mounted files. (f57587c) * Something. (2dead3a) * Created a simple file type autodetector. (97f72ff) * Can traverse subdirs. (7b75e38) * Started work on scanning experiment. (bcd1aa3) mediascanner2-0.115/HACKING000066400000000000000000000011511436755250000152270ustar00rootroot00000000000000Building the code ----------------- The list of packages required to build mediascanner can be found in the Build-Depends stanza of the debian/control.in file. In addition to those packages, you should install "qt5-default" to ensure that the Qt 5.x versions of build tools are used in preference to the Qt 4.x versions. The software can then be built with the following commands: $ mkdir build $ cd build $ cmake .. $ make The tests can then be run using: $ make test Note that "make test" will not trigger a rebuild, so it is necessary to rerun "make" first if any code has been changed. mediascanner2-0.115/debian/000077500000000000000000000000001436755250000154645ustar00rootroot00000000000000mediascanner2-0.115/debian/Jenkinsfile000066400000000000000000000006511436755250000176520ustar00rootroot00000000000000@Library('ubports-build-tools') _ buildAndProvideDebianPackage() // Or if the package consists entirely of arch-independent packages: // (optional optimization, will confuse BlueOcean's live view at build stage) // buildAndProvideDebianPackage(/* isArchIndependent */ true) // Optionally, to skip building on some architectures (amd64 is always built): // buildAndProvideDebianPackage(false, /* ignoredArchs */ ['arm64']) mediascanner2-0.115/debian/changelog000066400000000000000000000577311436755250000173530ustar00rootroot00000000000000mediascanner2 (0.115) unstable; urgency=medium * Upstream-provided Debian package for mediascanner2. See upstream ChangeLog for recent changes. -- UBports developers Sat, 04 Feb 2023 22:57:09 +0100 mediascanner2 (0.114+0ubports0) xenial; urgency=medium * Remove Bileto pre-release hook -- Dalton Durst Wed, 21 Aug 2019 17:19:22 -0500 mediascanner2 (0.113+ubports) xenial; urgency=medium * Imported to UBports -- UBports auto importer Fri, 12 Jan 2018 12:57:53 +0000 mediascanner2 (0.112+17.04.20170322-0ubuntu1) zesty; urgency=medium * Only run mediascanner in unity8. -- Michael Terry Wed, 22 Mar 2017 15:23:21 +0000 mediascanner2 (0.112+17.04.20170302-0ubuntu1) zesty; urgency=medium * Compatibility with unversioned cmake-extras modules (LP: #1563573) * Add SystemD session files -- Pete Woods Thu, 02 Mar 2017 10:42:36 +0000 mediascanner2 (0.112+17.04.20161201-0ubuntu1) zesty; urgency=medium [ James Henstridge ] * Correctly detect embedded art in Vorbis files with TagLib 1.11. [ Michael Terry ] * Handle running inside of a snap by respecting the $SNAP variable. (LP: #1629009) -- James Henstridge Thu, 01 Dec 2016 09:07:15 +0000 mediascanner2 (0.112+16.10.20160909-0ubuntu1) yakkety; urgency=medium [ James Henstridge ] * When multiple volumes are mounted in quick succession, scan them serially to avoid reentrancy problems in the initial scan. (LP: #1489656) * Add apparmor-easyprof hardware directories to package so AppArmor profile can compile when apparmor-easyprof-ubuntu isn't installed. (LP: #1443693) * Disable optimisation when compiling dbus-codec.cc to avoid gcc 6 compilation bug. (LP: #1621002) * Replace deprecated use of GetConnectionAppArmorSecurityContext method with GetConnectionCredentials. (LP: #1489489) [ You-Sheng Yang ] * Update mediascanner-extractor apparmor profile to cover Android library locations on 64-bit systems. -- James Henstridge Fri, 09 Sep 2016 13:46:43 +0000 mediascanner2 (0.111+16.10.20160526-0ubuntu1) yakkety; urgency=medium [ Alex Tu ] * apparmor: extend read permissions to cover media_codecs*.xml to support Turbo. [ Robert Bruce Park ] * Use new bileto_pre_release_hook. -- James Henstridge Thu, 26 May 2016 06:02:56 +0000 mediascanner2 (0.111+16.04.20160317-0ubuntu1) xenial; urgency=medium * If a file is unscannable, insert a fallback MediaFile into the index immediately, rather than relying on it being added by the broken_files logic on next startup. -- James Henstridge Thu, 17 Mar 2016 03:24:55 +0000 mediascanner2 (0.111+16.04.20160225-0ubuntu1) xenial; urgency=medium [ James Henstridge ] * Batch index updates during the initial scan in 10 second intervals (the same rate as invalidation notifications go out). This greatly reduces the IO overhead on initial startup, and is fairly safe now we have out of process metadata extraction. * Don't emit the InvalidateResults signal if a file was opened for writing and then closed, but not actually modified (LP: #1542175) * Favour the EXIF DateTimeOriginal tag over DateTime when extracting metadata from photos. (LP: #1468585) * Use taglib to extract metadata from Vorbis, Opus, Flac, MP3 and MP4 audio files. Other formats will fall back to the existing GStreamer code path. (LP: #1536832) [ CI Train Bot ] * debian/libmediascanner-2.0-3.symbols: update to released version. -- James Henstridge Thu, 25 Feb 2016 01:53:33 +0000 mediascanner2 (0.110+16.04.20151216-0ubuntu1) xenial; urgency=medium [ James Henstridge ] * Fix up handling of directory renames, so old contents is correctly removed from the index and inotify watches for subdirectories are cleaned up. (LP: #1460411) * If the mediascanner index can not be opened, catch the exception in the QML plugin and act as if the database is empty. A warning is printed via Qt's logging framework. This prevents QML apps using mediascanner from terminating if there is a problem opening the media index. (LP: #1514517) [ CI Train Bot ] * debian/libmediascanner-2.0-3.symbols: update to released version. -- James Henstridge Wed, 16 Dec 2015 08:04:43 +0000 mediascanner2 (0.109+16.04.20151124.1-0ubuntu1) xenial; urgency=medium [ James Henstridge ] * If a folder contains an image file named {cover,album,albumart,.folder,folder}.{jpeg,jpg,png} use it as album art for songs in preference to online art if the songs do not have embedded art. (LP: #1372000) * Disable MountWatcher's callback during destruction so we don't call back into a partially destroyed class. (LP: #1492393) [ CI Train Bot ] * debian/libmediascanner-2.0-3.symbols: update to released version. * New rebuild forced. -- James Henstridge Tue, 24 Nov 2015 08:56:52 +0000 mediascanner2 (0.108+16.04.20151109-0ubuntu1) xenial; urgency=medium * Move the metadata extractor to a separate process to isolate bugs in media codecs. (LP: #1508142) -- James Henstridge Mon, 09 Nov 2015 01:56:39 +0000 mediascanner2 (0.108+16.04.20151102-0ubuntu1) xenial; urgency=medium * Rename QML plugin package to qml-module-* and add transitional package for upgrades. (LP: #1342031) * Fix the metadata extractor so that it correctly extracts the date and presence of cover art from MPEG 4 audio files. (LP: #1492407) * Remove the id column from media_attic so that we don't get conflicts when copying data between media and media_attic. (LP: #1501990) -- James Henstridge Mon, 02 Nov 2015 02:52:43 +0000 mediascanner2 (0.107+15.10.20150922.1-0ubuntu1) wily; urgency=medium [ James Henstridge ] * Use a single source tree for the gcc 4.x/5.x builds. [ CI Train Bot ] * No-change rebuild. -- James Henstridge Tue, 22 Sep 2015 12:11:11 +0000 mediascanner2 (0.106+15.10.20150917-0ubuntu1) wily; urgency=medium [ James Henstridge ] * Store the file modification time in the media database so we can find recently added media. * Add a hasMedia() method to MediaStore, which returns true if there is any media of the given type. [ CI Train Bot ] * debian/libmediascanner-2.0-3.symbols: update to released version. * New rebuild forced. -- Pawel Stolowski Thu, 17 Sep 2015 12:03:39 +0000 mediascanner2 (0.105+15.10.20150721-0ubuntu2~ppa1) wily; urgency=medium * Update symbols file for GCC 5. * Tighten build dependency on libdbus-cpp-dev. -- Matthias Klose Tue, 21 Jul 2015 16:07:06 +0200 mediascanner2 (0.105+15.10.20150721-0ubuntu1) wily; urgency=medium [ James Henstridge ] * Remove the hard dependency on g++-4.9. (LP: #1452332) -- CI Train Bot Tue, 21 Jul 2015 05:13:08 +0000 mediascanner2 (0.105+15.10.20150604-0ubuntu1) wily; urgency=medium [ James Henstridge ] * Treat invalid dates in MP3s as missing metadata. (LP: #1436110) -- CI Train Bot Thu, 04 Jun 2015 06:51:43 +0000 mediascanner2 (0.105+15.04.20150128-0ubuntu1) vivid; urgency=low [ Jussi Pakkanen ] * Revert use of WAL log and instead try to rerun queries that fail with SQLITE_BUSY. (LP: #1415318) -- Ubuntu daily release Wed, 28 Jan 2015 12:59:06 +0000 mediascanner2 (0.105+15.04.20150127-0ubuntu1) vivid; urgency=low [ Jussi Pakkanen ] * Send invalidations signals during scans that take a long time so the dash gets updated. (LP: #1414566) -- Ubuntu daily release Tue, 27 Jan 2015 14:28:29 +0000 mediascanner2 (0.105+15.04.20150122-0ubuntu1) vivid; urgency=low [ Jussi Pakkanen ] * Add blacklist functionality and use it to block music playlists. (LP: #1384295) * Skip scanning of special directories if they point to user home dir. (LP: #285998) -- Ubuntu daily release Thu, 22 Jan 2015 10:18:10 +0000 mediascanner2 (0.105+15.04.20150121-0ubuntu1) vivid; urgency=low [ Jussi Pakkanen ] * Extract metadata from images that don't have exif entries. (LP: #1378880) * Make Valgrind uninitialised jump warning go away. -- Ubuntu daily release Wed, 21 Jan 2015 15:21:18 +0000 mediascanner2 (0.105+15.04.20141030.1-0ubuntu1) vivid; urgency=low [ James Henstridge ] * When a new directory is added and a new inotify watch is set up, call fileAdded() on any regular files already in the directory so they are not missed. (LP: #1379817) -- Ubuntu daily release Thu, 30 Oct 2014 11:23:12 +0000 mediascanner2 (0.105+14.10.20141001-0ubuntu1) utopic; urgency=low [ James Henstridge ] * Use udisksd to track mounts and unmounts of removable media. (LP: #1358750) -- Ubuntu daily release Wed, 01 Oct 2014 06:59:45 +0000 mediascanner2 (0.105+14.10.20140925.1-0ubuntu1) utopic; urgency=low [ Jussi Pakkanen ] * Print banner when starting the daemon so upstart logs are easier to parse. * Use fallback data for crasher files so they at least show up in scope queries. -- Ubuntu daily release Thu, 25 Sep 2014 11:58:02 +0000 mediascanner2 (0.105+14.10.20140909-0ubuntu1) utopic; urgency=low [ Jussi Pakkanen ] * Print a log message when skipping a broken file. [ Jamie Strandboge ] * debian/usr.bin.mediascanner-service-2.0: add video abstraction. silence access to /run/udev/data/** like we do elsewhere. allow read on /dev/video*. allow read on /sys/devices/**/video4linux/**/uevent . -- Ubuntu daily release Tue, 09 Sep 2014 08:41:03 +0000 mediascanner2 (0.105+14.10.20140903-0ubuntu1) utopic; urgency=medium [ James Henstridge ] * Record whether files contain embedded artwork in the media index. * Use this info to generate art URIs that either use embedded artwork, or the network based albumart provider. * Expose embedded album art for Album objects too. Bump soname to account for Album class changing size. [ Ubuntu daily release ] * debian/libmediascanner-2.0-3.symbols: auto-update to released version * New rebuild forced [ Jussi Pakkanen ] * Fix usage of blocking cache. -- Ubuntu daily release Wed, 03 Sep 2014 11:46:24 +0000 mediascanner2 (0.104+14.10.20140825-0ubuntu1) utopic; urgency=medium [ James Henstridge ] * Add support for custom sort orders in query(), queryAlbums() and queryArtists() methods. This breaks the ABI, so bump soname. * Update AppArmor policy to allow read access to /sys/devices/**/video4linux/video** (LP: #1353139) [ Ubuntu daily release ] * debian/libmediascanner-2.0-2.symbols: auto-update to released version [ Jussi Pakkanen ] * Fix inotify usage. * Store information on files that break GStreamer and skip them when encountered. * Don't call closedir with a null argument. * Add database integrity constraints. [ Jamie Strandboge ] * allow read access to /etc/udev/udev.conf for gstreamer (LP: #1356883) (LP: #1356883) -- Ubuntu daily release Mon, 25 Aug 2014 12:52:17 +0000 mediascanner2 (0.103+14.10.20140819-0ubuntu1) utopic; urgency=low [ Jussi Pakkanen ] * Apparmor fix from jdstrand. (LP: #1357348) -- Ubuntu daily release Tue, 19 Aug 2014 12:52:52 +0000 mediascanner2 (0.103+14.10.20140815.1-0ubuntu1) utopic; urgency=medium [ Dimitri John Ledkov ] * Add missing dependencies to libmediascanner-2.0-dev package. (LP: #1351358) [ James Henstridge ] * Automatically update models when the daemon sends the InvalidateResults D-Bus signal. Add a status property to models to let loading progress be tracked. Rename rowCount property to count, keeping the old name around for compatibility. (LP: #1347444) [ Jussi Pakkanen ] * Minor fixes for libc++. * Handle the case when /media/username does not exist when Mediascanner first starts up. (LP: #1354543) -- Ubuntu daily release Fri, 15 Aug 2014 11:18:39 +0000 mediascanner2 (0.102+14.10.20140805-0ubuntu1) utopic; urgency=low [ James Henstridge ] * Fix off by one error when appending new rows to QML models. (LP: #1350529) -- Ubuntu daily release Tue, 05 Aug 2014 11:44:48 +0000 mediascanner2 (0.102+14.10.20140730.1-0ubuntu1) utopic; urgency=medium [ Jussi Pakkanen ] * Added an autopkgtest. * Fixes for various issues found by flint++. * Install Filter.hh. * Archive items on unmount. [ James Henstridge ] * Stream data to the QML models in chunks through a background thread using QtConcurrent. The limit option on the various models has been removed, since we the individual queries are smaller. * Add MediaStore::queryArtists() method, needed for the new scope design. [ Ubuntu daily release ] * debian/libmediascanner-2.0-1.symbols: auto-update to released version -- Ubuntu daily release Wed, 30 Jul 2014 10:49:39 +0000 mediascanner2 (0.101+14.10.20140724.1-0ubuntu1) utopic; urgency=medium [ Alfonso Sanchez-Beato (email Canonical) ] * Adjust apparmor configuration for MTK device -- Ubuntu daily release Thu, 24 Jul 2014 11:15:33 +0000 mediascanner2 (0.101+14.10.20140721-0ubuntu1) utopic; urgency=low [ Jussi Pakkanen ] * Specify GCC 4.9 explicitly to prevent ABI breakage. -- Ubuntu daily release Mon, 21 Jul 2014 14:16:51 +0000 mediascanner2 (0.101+14.10.20140715-0ubuntu1) utopic; urgency=low * New rebuild forced -- Ubuntu daily release Tue, 15 Jul 2014 14:03:12 +0000 mediascanner2 (0.101+14.10.20140702-0ubuntu1) utopic; urgency=low [ James Henstridge ] * Update the FTS tokenizer to work with SQLite 3.8.5, and reenable the MediaStoreTest.query_short test. Bump libsqlite3-dev build dependency to 3.8.5 too. -- Ubuntu daily release Wed, 02 Jul 2014 08:07:49 +0000 mediascanner2 (0.101+14.10.20140627-0ubuntu3) utopic; urgency=medium * apparmor: add missing proc file used by the mediatek soc -- Ricardo Salveti de Araujo Tue, 01 Jul 2014 11:29:31 +0800 mediascanner2 (0.101+14.10.20140627-0ubuntu2) utopic; urgency=medium * debian/usr.bin.mediascanner-service-2.0: couple more minor updates for (mostly) noisy denials - use consoles abstraction instead of just the pts interface since we need /dev/tty when crashing - finetune orcexec rules by allowing 'm' in /tmp but explicitly deny the others - allow non-owner read of @{PROC}/cmdline - use attach_disconnected for /dev/socket/property_service -- Jamie Strandboge Fri, 27 Jun 2014 14:59:34 -0500 mediascanner2 (0.101+14.10.20140627-0ubuntu1) utopic; urgency=medium [ Jussi Pakkanen ] * Disable short query test to work around a behavioral change in SQLite. -- Ubuntu daily release Fri, 27 Jun 2014 17:30:35 +0000 mediascanner2 (0.101+14.10.20140625-0ubuntu2) utopic; urgency=medium * debian/usr.bin.mediascanner-service-2.0: updates for libhybris based on work by Ricardo Salveti. (LP: #1334940) -- Jamie Strandboge Fri, 27 Jun 2014 08:00:22 -0500 mediascanner2 (0.101+14.10.20140625-0ubuntu1) utopic; urgency=low [ Ubuntu daily release ] * debian/*symbols: auto-update new symbols to released version [ James Henstridge ] * Enable the QML plugin to pick between two MediaStore backends: the direct disk backend and the D-Bus interface. The choice is made via the MEDIASCANNER_USE_DBUS environment variable, and defaults to direct access. [ Jamie Strandboge ] * * add AppArmor profile (LP: #1319065) - add debian/usr.bin.mediascanner-service-2.0 - debian/control: Build- Depends on dh-apparmor - debian/rules: update override_dh_installdeb to use dh_apparmor - debian/mediascanner2.0.dirs: add etc/apparmor.d - debian/mediascanner2.0.install: install profile in to place (LP: #1319065) [ CI bot ] * * add AppArmor profile (LP: #1319065) - add debian/usr.bin.mediascanner-service-2.0 - debian/control: Build- Depends on dh-apparmor - debian/rules: update override_dh_installdeb to use dh_apparmor - debian/mediascanner2.0.dirs: add etc/apparmor.d - debian/mediascanner2.0.install: install profile in to place (LP: #1319065) -- Ubuntu daily release Wed, 25 Jun 2014 16:28:53 +0000 mediascanner2 (0.101+14.10.20140613-0ubuntu1) utopic; urgency=low [ Ubuntu daily release ] * debian/*symbols: auto-update new symbols to released version [ James Henstridge ] * Increase the timeout on D-Bus method calls. (LP: #1326753) * Catch exceptions in the QML plugin, and print a warning instead. QML applications terminate when C++ exceptions bubble up, which can not be recovered from. (LP: #1326753) * Add support for scanning photos using libexif. Adds libexif-dev as new build dependency. (LP: #1256334) -- Ubuntu daily release Fri, 13 Jun 2014 06:22:32 +0000 mediascanner2 (0.101+14.10.20140530-0ubuntu1) utopic; urgency=low [ James Henstridge ] * Get rid of the direct constructor for MediaFile, forcing creation to go through MediaFileBuilder (which won't break when we add more metadata fields). Also move MediaFile fields to a private struct to avoid breaking ABI in future when adding new metadata fields. * Add a D-Bus service exposing the MediaStore, and move the QML binding over to using it. * Limit access to the MediaScanner D-Bus interface from confined processes. The trust-store API does not exist yet, so it just hard codes support for the "com.ubuntu.music" application. * Introduce a mediascanner::Filter type to hold search parameters for the various MediaStore::list*() methods, and expand them to handle genres. The new API also distinguishes between an unset filter and a filter set to "". Make similar changes to the QML API, and add notify signals to the rowCount properties. (LP: #1319168, LP: #1319174). [ Jussi Pakkanen ] * Add support for .nomedia files to prevent chosen subtrees from being scanned. (LP: #1294303). * Fix a few warnings emitted by clang. * Guard against empty MediaFileBuilders. * Fix CMake Project name. * Prune those files that have a scan block file on their path when restoring. -- Ubuntu daily release Fri, 30 May 2014 07:29:07 +0000 mediascanner2 (0.100+14.10.20140508-0ubuntu1) utopic; urgency=low [ James Henstridge ] * Add genre and discNumber metadata fields to the media index, and expose it in the QML binding. Add rowCount property and get(row, role) method to each of the model classes in the QML binding. (LP: #1246172) [ Jussi Pakkanen ] * Don't scan optical discs. * Use generator for file traversal instead of a vector. -- Ubuntu daily release Thu, 08 May 2014 10:18:14 +0000 mediascanner2 (0.100+14.04.20140403-0ubuntu1) trusty; urgency=low [ James Henstridge ] * Remove the Unity scope found in src/utils: it looks like it has suffered from bit rot and has never been used (we have unity-scope- mediascanner instead). This also removes an unnecessary dependency on the old libunity. * Before using GStreamer to extract metadata from a file, print a log message. This is intended to help track problems with metadata extraction. [ Jussi Pakkanen ] * Avoid assert crash by not trying to unregister subvolumes that were skipped due to looking like root subdirs. (LP: #1294193) * The output value of stat is undefined when the stat call fails. So don't use it. -- Ubuntu daily release Thu, 03 Apr 2014 10:00:38 +0000 mediascanner2 (0.100+14.04.20140324-0ubuntu1) trusty; urgency=low [ James Henstridge ] * Expose an "art" role/property on album and song models, returning a URI that can be used with a QML Image component. The actual image provider comes from ubuntu-ui-toolkit. -- Ubuntu daily release Mon, 24 Mar 2014 18:34:58 +0000 mediascanner2 (0.100+14.04.20140314-0ubuntu1) trusty; urgency=low [ Jussi Pakkanen ] * Keep going if inotify watch creation fails. * Skip subdirectories that look like root directories. * Delay signal handling initialisation. -- Ubuntu daily release Fri, 14 Mar 2014 15:17:16 +0000 mediascanner2 (0.100+14.04.20140310-0ubuntu1) trusty; urgency=low * New rebuild forced -- Ubuntu daily release Mon, 10 Mar 2014 08:20:17 +0000 mediascanner2 (0.100+14.04.20140307.1-0ubuntu1) trusty; urgency=low [ James Henstridge ] * First stages of a QML plugin for the Media Scanner. At present it only exposes the query() and lookup() interfaces. * Relax the rules for tokenising the input when it ends with a short token followed '*' wildcard. This allows us to produce results for short strings when doing incremental searches. * Convert the scanner daemon over to using the glib main loop rather than a hand rolled one, and limit invalidation signals to be sent at most once a second. [ Jussi Pakkanen ] * Add sqlite error string to exception. * Shutdown cleanly on signals. * Made SQLite db safe to access from multiple threads. [ Michal Hruby ] * Fix incorrect depends -- Ubuntu daily release Fri, 07 Mar 2014 17:40:21 +0000 mediascanner2 (0.99+14.04.20140220-0ubuntu1) trusty; urgency=low [ James Henstridge ] * Add a limit argument to MediaStore::queryAlbums(), and ensure that it returns some results when given an empty query string. [ Jussi Pakkanen ] * Added const qualifiers to methods and made album immutable just like mediafile. * Use lstat to avoid loops. * Fix path where media cache is stored. * Nil merge to get on the release train. * Send invalidation signal once the db is fully populated. -- Ubuntu daily release Thu, 20 Feb 2014 10:03:04 +0000 mediascanner2 (0.99+14.04.20140205-0ubuntu1) trusty; urgency=low [ James Henstridge ] * Add a limit argument to MediaStore::query(), and ensure that it produces some results in response to an empty query. [ Jussi Pakkanen ] * Simple class for sending result set invalidation messages. * Add a mediascanner namespace. * Simpler header reorg. [ CI bot ] * Null merge to get changes through CITrain -- Ubuntu daily release Wed, 05 Feb 2014 18:08:14 +0000 mediascanner2 (0.99+14.04.20140117-0ubuntu1) trusty; urgency=low [ James Henstridge ] * Fix a few issues that prevent the scanner daemon from running when stdin is redirected to /dev/null. * Fix the licensing on the public library so that it is correctly labelled as LGPL, like the old mediascanner code base. [ Ubuntu daily release ] * Automatic snapshot from revision 197 -- Ubuntu daily release Fri, 17 Jan 2014 07:50:53 +0000 mediascanner2 (0.99+14.04.20140110-0ubuntu1) trusty; urgency=low [ Jussi Pakkanen ] * Hot new stuff [ Ɓukasz 'sil2100' Zemczak ] * Automatic snapshot from revision 191 (bootstrap) * We no longer ship any LGPL sources. Also, fix packaging naming in some places - along with Vcs-* [ Ubuntu daily release ] * Automatic snapshot from revision 194 -- Ubuntu daily release Fri, 10 Jan 2014 14:18:01 +0000 mediascanner2-0.115/debian/control000066400000000000000000000053411436755250000170720ustar00rootroot00000000000000Source: mediascanner2 Section: libs Priority: optional Maintainer: UBports Developers Standards-Version: 3.9.6 Build-Depends: cmake, cmake-extras (>= 0.10), dbus, debhelper-compat (= 12), google-mock, libapparmor-dev, libdbus-1-dev, libdbus-cpp-dev (>= 4.0.0), libexif-dev, libglib2.0-dev, libgstreamer-plugins-base1.0-dev, libgstreamer1.0-dev, libgdk-pixbuf2.0-dev, libgtest-dev, libproperties-cpp-dev, libsqlite3-dev (>= 3.8.5), libtag1-dev, libudisks2-dev, lsb-release, qml-module-qtquick2, qml-module-qttest, qtbase5-dev, qtbase5-dev-tools, qtdeclarative5-dev, qtdeclarative5-dev-tools, dh-apparmor, # For running unit tests gstreamer1.0-plugins-base, gstreamer1.0-plugins-good, shared-mime-info, Homepage: https://gitlab.com/ubports/development/core/mediascanner2 Vcs-Git: https://gitlab.com/ubports/development/core/mediascanner2.git Vcs-Browser: https://gitlab.com/ubports/development/core/mediascanner2 Package: libmediascanner-2.0-4 Architecture: any Multi-Arch: same Pre-Depends: ${misc:Pre-Depends}, Depends: ${misc:Depends}, ${shlibs:Depends}, Recommends: mediascanner2.0 Description: Access library for the media scanner's index This library provides convenient and safe access to the media scanner's index files. Package: mediascanner2.0 Architecture: any Multi-Arch: foreign Pre-Depends: ${misc:Pre-Depends}, Depends: gstreamer1.0-plugins-base, gstreamer1.0-plugins-good, dbus, ${misc:Depends}, ${shlibs:Depends}, Description: Media scanner package This package provides the media scanner service. Package: libmediascanner-2.0-dev Section: libdevel Architecture: any Multi-Arch: same Pre-Depends: ${misc:Pre-Depends}, Depends: libmediascanner-2.0-4 (= ${binary:Version}), libsqlite3-dev, libglib2.0-dev, ${misc:Depends}, Description: Development files for libmediascanner This package provides the infrastructure for using the media scanner's access library in C++ based projects. Package: qml-module-mediascanner0.1 Architecture: any Multi-Arch: same Pre-Depends: ${misc:Pre-Depends}, Depends: mediascanner2.0 (= ${binary:Version}), ${misc:Depends}, ${shlibs:Depends}, Description: QML plugin for the Media Scanner This package provides components that allow access to the media scanner index from Qt Quick 2 / QML applications. mediascanner2-0.115/debian/copyright000066400000000000000000000037141436755250000174240ustar00rootroot00000000000000Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: mediascanner2 Source: https://launchpad.net/mediascanner2 Files: * Copyright: 2013 Canonical Ltd. License: GPL-3 This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 3 as published by the Free Software Foundation. . This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. . On Debian/Ubuntu systems, the full text of the GPL v3 can be found in `/usr/share/common-licenses/GPL-3' Files: src/mediascanner/* src/qml/* Copyright: 2013 Canonical Ltd. License: LGPL-3 This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License Version 3.0 as published by the Free Software Foundation. . This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. . On Debian/Ubuntu systems, the full text of the LGPL v3 can be found in `/usr/share/common-licenses/LGPL-3' Files: src/mediascanner/mozilla/fts* Copyright: Public Domain License: public-domain The author disclaims copyright to this source code. In place of a legal notice, here is a blessing: . May you do good and not evil. May you find forgiveness for yourself and forgive others. May you share freely, never taking more than you give. Files: src/mediascanner/mozilla/Normalize.c Copyright: Not stated License: MPL-2.0 This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. mediascanner2-0.115/debian/libmediascanner-2.0-4.install000066400000000000000000000000241436755250000226260ustar00rootroot00000000000000usr/lib/*/lib*.so.* mediascanner2-0.115/debian/libmediascanner-2.0-4.shlibs000066400000000000000000000000671436755250000224530ustar00rootroot00000000000000libmediascanner-2.0 4 libmediascanner-2.0-4 (>= 0.111) mediascanner2-0.115/debian/libmediascanner-2.0-dev.install000066400000000000000000000000661436755250000232470ustar00rootroot00000000000000usr/include/* usr/lib/*/lib*.so usr/lib/*/pkgconfig/* mediascanner2-0.115/debian/mediascanner2.0.dirs000066400000000000000000000004741436755250000212250ustar00rootroot00000000000000etc/apparmor.d # The apparmor profile depends on these directories that are usually # populated by apparmor-easyprof-ubuntu. We don't depend on that # package though, so make sure the directories exist. usr/share/apparmor/hardware/audio.d usr/share/apparmor/hardware/graphics.d usr/share/apparmor/hardware/video.d mediascanner2-0.115/debian/mediascanner2.0.install000066400000000000000000000004611436755250000217260ustar00rootroot00000000000000usr/bin/mediascanner-service-2.0 usr/libexec/mediascanner-2.0/mediascanner-dbus-2.0 usr/libexec/mediascanner-2.0/mediascanner-extractor usr/lib/systemd usr/share/dbus-1/services/* debian/usr.bin.mediascanner-service-2.0 etc/apparmor.d debian/usr.lib.mediascanner-2.0.mediascanner-extractor etc/apparmor.d mediascanner2-0.115/debian/qml-module-mediascanner0.1.install000066400000000000000000000000451436755250000237750ustar00rootroot00000000000000usr/lib/*/qt5/qml/MediaScanner.0.1/* mediascanner2-0.115/debian/rules000077500000000000000000000013461436755250000165500ustar00rootroot00000000000000#!/usr/bin/make -f # -*- makefile -*- include /usr/share/dpkg/default.mk # Uncomment this to turn on verbose mode. #export DH_VERBOSE=1 export DPKG_GENSYMBOLS_CHECK_LEVEL=4 # http://ccache.samba.org/manual.html#_precompiled_headers export CCACHE_SLOPPINESS=time_macros export QT_SELECT=qt5 %: dh $@ ifneq (,$(filter powerpc,$(DEB_HOST_ARCH))) test_arg = || : else test_arg = endif override_dh_auto_test: dh_auto_test --max-parallel=1 -- ARGS="--verbose" $(test_arg) override_dh_installdeb: dh_apparmor --profile-name=usr.bin.mediascanner-service-2.0 -pmediascanner2.0 dh_apparmor --profile-name=usr.lib.mediascanner-2.0.mediascanner-extractor -pmediascanner2.0 dh_installdeb override_dh_missing: dh_missing --fail-missing mediascanner2-0.115/debian/tests/000077500000000000000000000000001436755250000166265ustar00rootroot00000000000000mediascanner2-0.115/debian/tests/control000066400000000000000000000001011436755250000202210ustar00rootroot00000000000000Tests: msbuild Depends: g++, pkg-config, libmediascanner-2.0-dev mediascanner2-0.115/debian/tests/msbuild000077500000000000000000000011731436755250000202150ustar00rootroot00000000000000#!/bin/sh # Build a test program using libmediascanner. # (C) 2014 Canonical Ltd. # Authors: Jussi Pakkanen set -e WORKDIR=$(mktemp -d) trap "rm -rf $WORKDIR" 0 INT QUIT ABRT PIPE TERM cd $WORKDIR cat < mstest.cpp #include #include int main(int argc, char **argv) { mediascanner::MediaFileBuilder mfb("dummy"); mediascanner::MediaFile mf(mfb); return 0; } EOF g++ -std=c++11 -o msbin mstest.cpp `pkg-config --cflags --libs mediascanner-2.0` echo "Mediascanner build: OK" [ -x msbin ] ./msbin echo "Mediascanner run: OK" mediascanner2-0.115/debian/usr.bin.mediascanner-service-2.0000066400000000000000000000050041436755250000233510ustar00rootroot00000000000000#include /usr/bin/mediascanner-service-2.0 (attach_disconnected) { #include #include #include #include #include #include #include #include #include deny /dev/cpuctl/apps/tasks w, deny /dev/cpuctl/apps/bg_non_interactive/tasks w, @{PROC}/interrupts r, @{PROC}/cmdline r, owner @{PROC}/[0-9]*/auxv r, owner @{PROC}/[0-9]*/fd/ r, owner @{PROC}/[0-9]*/status r, owner @{PROC}/[0-9]*/task/ r, owner @{PROC}/[0-9]*/task/[0-9]*/ r, owner @{PROC}/[0-9]*/cmdline r, /etc/udev/udev.conf r, deny /run/udev/data/** r, ptrace (read) peer=@{profile_name}, # attach_disconnected path /{,dev/}socket/property_service rw, # Android logging triggered by platform. Can safely deny deny /dev/log_main w, deny /dev/log_radio w, deny /dev/log_events w, deny /dev/log_system w, /usr/bin/mediascanner-service-2.0 r, # Allow read on all directories /**/ r, # Allow read on click install directories, removable media and files in # /usr/local/share. /usr/share/** r, /usr/local/share/** r, /{media,mnt,opt,srv}/** r, # Allow reading any files in non-hidden directories owner @{HOME}/[^.]* rk, owner @{HOME}/[^.]*/ rk, owner @{HOME}/[^.]*/** rk, # Allow reading files in XDG directories (ie, where apps are allowed to # write) owner @{HOME}/.config/user-dirs.dirs r, owner @{HOME}/.cache/** rk, owner @{HOME}/.local/share/** rk, owner /{,var/}run/user/[0-9]*/** rk, # Write out the database files owner @{HOME}/.cache/mediascanner-2.0/ rw, owner @{HOME}/.cache/mediascanner-2.0/** rwkl, # Allow communication with udisksd dbus (send) bus=system path="/org/freedesktop/UDisks2*" interface="org.freedesktop.DBus.ObjectManager" member=GetManagedObjects, dbus (send) bus=system path="/org/freedesktop/UDisks2*" interface="org.freedesktop.DBus.Properties" member={Get,GetAll}, dbus (receive) bus=system interface="org.freedesktop.DBus.ObjectManager", dbus (receive) bus=system interface="org.freedesktop.DBus.Properties", dbus (receive) bus=system interface="org.freedesktop.UDisks2*", # Site-specific additions and overrides. See local/README for details. #include } mediascanner2-0.115/debian/usr.lib.mediascanner-2.0.mediascanner-extractor000066400000000000000000000075351436755250000263650ustar00rootroot00000000000000#include /usr/libexec/mediascanner-2.0/mediascanner-extractor (attach_disconnected) { #include #include #include #include #include #include #include #include #include #include #include "/usr/share/apparmor/hardware/audio.d" #include "/usr/share/apparmor/hardware/graphics.d" #include "/usr/share/apparmor/hardware/video.d" deny /dev/cpuctl/apps/tasks w, deny /dev/cpuctl/apps/bg_non_interactive/tasks w, @{PROC}/interrupts r, @{PROC}/cmdline r, owner @{PROC}/[0-9]*/auxv r, owner @{PROC}/[0-9]*/fd/ r, owner @{PROC}/[0-9]*/status r, owner @{PROC}/[0-9]*/task/ r, owner @{PROC}/[0-9]*/task/[0-9]*/ r, owner @{PROC}/[0-9]*/cmdline r, /etc/udev/udev.conf r, deny /run/udev/data/** r, # specific to the mediatek soc @{PROC}/xlog/setfil r, @{PROC}/M4U_device r, /dev/Vcodec rw, /sys/bus/platform/drivers/** rw, /{,android/}system/etc/mtk_omx_core.cfg r, /dev/devmap r, @{PROC}/mtk_mdp_cmdq r, /dev/video* r, /sys/devices/**/video4linux/video** r, /sys/devices/**/video4linux/**/uevent r, /sys/kernel/debug/tracing/trace_marker w, /dev/ashmem rw, ptrace (read) peer=@{profile_name}, # libhybris /{,var/}run/shm/hybris_shm_data rw, /usr/lib/@{multiarch}/libhybris/*.so mr, /{,android/}system/build.prop r, /dev/__properties__/{,*} r, # These libraries can be in any of: # /vendor/lib{,64} # /system/lib{,64} # /system/vendor/lib{,64} # /android/vendor/lib{,64} # /android/system/lib{,64} # /android/system/vendor/lib{,64} /{,android/}vendor/lib{,64}/** r, /{,android/}vendor/lib{,64}/**.so m, /{,android/}system/lib{,64}/** r, /{,android/}system/lib{,64}/**.so m, /{,android/}system/vendor/lib{,64}/** r, /{,android/}system/vendor/lib{,64}/**.so m, # attach_disconnected path /{,dev/}socket/property_service rw, # Android logging triggered by platform. Can safely deny deny /dev/log_main w, deny /dev/log_radio w, deny /dev/log_events w, deny /dev/log_system w, /usr/libexec/mediascanner-2.0/mediascanner-extractor r, # GStreamer binary registry - hybris pulls this in for everything now, not # just audio owner @{HOME}/.gstreamer*/registry.*.bin* rw, owner @{HOME}/.gstreamer*/ rw, owner @{HOME}/.cache/gstreamer*/ rw, owner @{HOME}/.cache/gstreamer*/registry.*.bin* rw, /usr/lib/@{multiarch}/gstreamer*/gstreamer*/gst-plugin-scanner ix, owner /tmp/orcexec* m, # gstreamer writes JIT compiled code in the form of orcexec.* files. Various # locations are tried so silence the ones we won't permit anyway deny /{,var/}run/user/*/orcexec* w, deny @{HOME}/orcexec* w, /{,android/}system/etc/media_codecs*.xml r, /{,android/}vendor/etc/media_codecs*.xml r, /etc/wildmidi/wildmidi.cfg r, # Unisoc video decoding /dev/dri/** rw, # Allow read on all directories /**/ r, # Allow read on click install directories, removable media and files in # /usr/local/share. /usr/share/** r, /usr/local/share/** r, /{media,mnt,opt,srv}/** r, # Allow reading any files in non-hidden directories owner @{HOME}/[^.]* rk, owner @{HOME}/[^.]*/ rk, owner @{HOME}/[^.]*/** rk, # Allow reading files in XDG directories (ie, where apps are allowed to # write) owner @{HOME}/.config/user-dirs.dirs r, owner @{HOME}/.cache/** rk, owner @{HOME}/.local/share/** rk, owner /{,var/}run/user/[0-9]*/** rk, # Site-specific additions and overrides. See local/README for details. #include } mediascanner2-0.115/mediascanner-2.0.pc.in000066400000000000000000000004051436755250000201200ustar00rootroot00000000000000Name: mediascanner-2.0 Description: Access library for the media scanner's index Version: @MEDIASCANNER_VERSION@ Requires.private: gio-2.0 sqlite3 Libs: -L@CMAKE_INSTALL_FULL_LIBDIR@ -lmediascanner-2.0 Cflags: -I@CMAKE_INSTALL_FULL_INCLUDEDIR@/mediascanner-2.0 mediascanner2-0.115/mediascanner-2.0.service.in000066400000000000000000000004401436755250000211550ustar00rootroot00000000000000[Unit] Description=Media Scanner PartOf=graphical-session.target Before=graphical-session.target [Service] Type=dbus BusName=com.canonical.MediaScanner2.Daemon ExecStart=@CMAKE_INSTALL_FULL_BINDIR@/mediascanner-service-2.0 Restart=on-failure [Install] WantedBy=graphical-session.target mediascanner2-0.115/src/000077500000000000000000000000001436755250000150315ustar00rootroot00000000000000mediascanner2-0.115/src/CMakeLists.txt000066400000000000000000000002551436755250000175730ustar00rootroot00000000000000add_subdirectory(mediascanner) add_subdirectory(extractor) add_subdirectory(daemon) add_subdirectory(ms-dbus) add_subdirectory(qml/MediaScanner.0.1) add_subdirectory(utils) mediascanner2-0.115/src/daemon/000077500000000000000000000000001436755250000162745ustar00rootroot00000000000000mediascanner2-0.115/src/daemon/CMakeLists.txt000066400000000000000000000011361436755250000210350ustar00rootroot00000000000000add_definitions(${MEDIASCANNER_DEPS_CFLAGS} ${UDISKS_CFLAGS}) include_directories(..) add_library(scannerstuff STATIC InvalidationSender.cc MountWatcher.cc VolumeManager.cc SubtreeWatcher.cc Scanner.cc ../mediascanner/utils.cc ) target_link_libraries(scannerstuff extractor-client ${UDISKS_LDFLAGS}) add_executable(scannerdaemon scannerdaemon.cc ) set_target_properties(scannerdaemon PROPERTIES OUTPUT_NAME "mediascanner-service-2.0") target_link_libraries(scannerdaemon mediascanner scannerstuff ) install( TARGETS scannerdaemon RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} ) mediascanner2-0.115/src/daemon/InvalidationSender.cc000066400000000000000000000053551436755250000223750ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include"InvalidationSender.hh" #include #include #include #include #include using namespace std; // timer delay in seconds static const char SCOPES_DBUS_IFACE[] = "com.canonical.unity.scopes"; static const char SCOPES_DBUS_PATH[] = "/com/canonical/unity/scopes"; static const char SCOPES_INVALIDATE_RESULTS[] = "InvalidateResults"; namespace mediascanner { InvalidationSender::InvalidationSender() : bus(nullptr, g_object_unref) { } InvalidationSender::~InvalidationSender() { if (timeout_id != 0) { g_source_remove(timeout_id); } } void InvalidationSender::setBus(GDBusConnection *bus) { this->bus.reset(static_cast(g_object_ref(bus))); } void InvalidationSender::setDelay(int delay) { this->delay = delay; } void InvalidationSender::invalidate() { if (!bus) { return; } if (timeout_id != 0) { return; } if (delay > 0) { timeout_id = g_timeout_add_seconds(delay, &InvalidationSender::callback, static_cast(this)); } else { InvalidationSender::callback(this); } } int InvalidationSender::callback(void *data) { auto invalidator = static_cast(data); GError *error = nullptr; if (!g_dbus_connection_emit_signal( invalidator->bus.get(), nullptr, SCOPES_DBUS_PATH, SCOPES_DBUS_IFACE, SCOPES_INVALIDATE_RESULTS, g_variant_new("(s)", "mediascanner-music"), &error)) { fprintf(stderr, "Could not invalidate music scope results: %s\n", error->message); g_error_free(error); error = nullptr; } if (!g_dbus_connection_emit_signal( invalidator->bus.get(), nullptr, SCOPES_DBUS_PATH, SCOPES_DBUS_IFACE, SCOPES_INVALIDATE_RESULTS, g_variant_new("(s)", "mediascanner-video"), &error)) { fprintf(stderr, "Could not invalidate video scope results: %s\n", error->message); g_error_free(error); error = nullptr; } invalidator->timeout_id = 0; return G_SOURCE_REMOVE; } } mediascanner2-0.115/src/daemon/InvalidationSender.hh000066400000000000000000000026451436755250000224060ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef INVALIDATIONSENDER_HH #define INVALIDATIONSENDER_HH #include typedef struct _GDBusConnection GDBusConnection; namespace mediascanner { /** * A class that sends a broadcast signal that the state of media * files has changed. */ class InvalidationSender final { public: InvalidationSender(); ~InvalidationSender(); InvalidationSender(const InvalidationSender &o) = delete; InvalidationSender& operator=(const InvalidationSender &o) = delete; void invalidate(); void setBus(GDBusConnection *bus); void setDelay(int delay); private: static int callback(void *data); std::unique_ptr bus; unsigned int timeout_id = 0; int delay = 0; }; } #endif mediascanner2-0.115/src/daemon/MountWatcher.cc000066400000000000000000000246261436755250000212350ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "MountWatcher.hh" #include #include #include #include #include #include #include #include #include namespace { const char BLOCK_DEVICE_PREFIX[] = "/org/freedesktop/UDisks2/block_devices/"; const char FILESYSTEM_IFACE[] = "org.freedesktop.UDisks2.Filesystem"; struct DeviceInfo { mediascanner::MountWatcherPrivate *p; std::unique_ptr device; std::unique_ptr filesystem; std::string mount_point; bool is_mounted = false; unsigned int mount_point_changed_id = 0; DeviceInfo(mediascanner::MountWatcherPrivate *p, UDisksObject *dev); ~DeviceInfo(); DeviceInfo(const DeviceInfo&) = delete; DeviceInfo& operator=(DeviceInfo& o) = delete; void filesystem_added(); void filesystem_removed(); void update_mount_state(); static void mount_point_changed(GObject *, GParamSpec *, void *user_data) noexcept; }; } namespace mediascanner { struct MountWatcherPrivate { std::function callback; std::unique_ptr client; GDBusObjectManager *manager = nullptr; std::map> devices; unsigned int object_added_id = 0; unsigned int object_removed_id = 0; unsigned int interface_added_id = 0; unsigned int interface_removed_id = 0; MountWatcherPrivate(std::function callback); ~MountWatcherPrivate(); static void object_added(GDBusObjectManager *manager, GDBusObject *object, void *user_data) noexcept; static void object_removed(GDBusObjectManager *manager, GDBusObject *object, void *user_data) noexcept; static void interface_added(GDBusObjectManager *manager, GDBusObject *object, GDBusInterface *iface, void *user_data) noexcept; static void interface_removed(GDBusObjectManager *manager, GDBusObject *object, GDBusInterface *iface, void *user_data) noexcept; }; MountWatcher::MountWatcher(std::function callback) : p(new MountWatcherPrivate(callback)) { GError *error = nullptr; p->client.reset(udisks_client_new_sync(nullptr, &error)); if (not p->client) { std::string errortxt(error->message); g_error_free(error); delete(p); throw std::runtime_error( std::string("Failed to create udisks2 client: ") + errortxt); } p->manager = udisks_client_get_object_manager(p->client.get()); p->object_added_id = g_signal_connect( p->manager, "object-added", G_CALLBACK(&MountWatcherPrivate::object_added), p); p->object_removed_id = g_signal_connect( p->manager, "object-removed", G_CALLBACK(&MountWatcherPrivate::object_removed), p); p->interface_added_id = g_signal_connect( p->manager, "interface-added", G_CALLBACK(&MountWatcherPrivate::interface_added), p); p->interface_removed_id = g_signal_connect( p->manager, "interface-removed", G_CALLBACK(&MountWatcherPrivate::interface_removed), p); GList *objects = g_dbus_object_manager_get_objects(p->manager); for (GList *l = objects; l != nullptr; l = l->next) { GDBusObject *object = reinterpret_cast(l->data); MountWatcherPrivate::object_added(p->manager, object, p); } } MountWatcher::~MountWatcher() { delete p; } MountWatcherPrivate::MountWatcherPrivate( std::function callback) : callback(callback), client(nullptr, g_object_unref) { } MountWatcherPrivate::~MountWatcherPrivate() { if (object_added_id != 0) { g_signal_handler_disconnect(manager, object_added_id); } if (object_removed_id != 0) { g_signal_handler_disconnect(manager, object_removed_id); } if (interface_added_id != 0) { g_signal_handler_disconnect(manager, interface_added_id); } if (interface_removed_id != 0) { g_signal_handler_disconnect(manager, interface_removed_id); } // Clear the callback so we don't send out any notifications // during destruction. callback = nullptr; } void MountWatcherPrivate::object_added(GDBusObjectManager *, GDBusObject *object, void *user_data) noexcept { MountWatcherPrivate *p = reinterpret_cast(user_data); // We're only interested in block devices const char *object_path = g_dbus_object_get_object_path(object); if (!g_str_has_prefix(object_path, BLOCK_DEVICE_PREFIX)) { return; } UDisksObject *device = UDISKS_OBJECT(object); UDisksBlock *block = udisks_object_peek_block(device); if (not block) { return; } // Check if we're already tracking this object (this should never // be true if events are received in order, but it doesn't hurt to // check). if (p->devices.find(object_path) != p->devices.end()) { return; } // Determine whether the device belongs to a removable drive. std::unique_ptr drive_object( udisks_client_get_object(p->client.get(), udisks_block_get_drive(block)), g_object_unref); if (not drive_object) { return; } UDisksDrive *drive = udisks_object_peek_drive(drive_object.get()); if (not drive || !udisks_drive_get_removable(drive)) return; // Start tracking this device std::unique_ptr info(new DeviceInfo(p, device)); p->devices.emplace(object_path, std::move(info)); } void MountWatcherPrivate::object_removed(GDBusObjectManager *, GDBusObject *object, void *user_data) noexcept { MountWatcherPrivate *p = reinterpret_cast(user_data); const char *object_path = g_dbus_object_get_object_path(object); p->devices.erase(object_path); } void MountWatcherPrivate::interface_added(GDBusObjectManager *, GDBusObject *object, GDBusInterface *iface, void *user_data) noexcept { // We're only interested in the filesystem interface GDBusInterfaceInfo *iface_info = g_dbus_interface_get_info(iface); if (strcmp(iface_info->name, FILESYSTEM_IFACE) != 0) { return; } MountWatcherPrivate *p = reinterpret_cast(user_data); const char *object_path = g_dbus_object_get_object_path(object); try { p->devices.at(object_path)->filesystem_added(); } catch (const std::out_of_range &e) { } } void MountWatcherPrivate::interface_removed(GDBusObjectManager *, GDBusObject *object, GDBusInterface *iface, void *user_data) noexcept { // We're only interested in the filesystem interface GDBusInterfaceInfo *iface_info = g_dbus_interface_get_info(iface); if (strcmp(iface_info->name, FILESYSTEM_IFACE) != 0) { return; } MountWatcherPrivate *p = reinterpret_cast(user_data); const char *object_path = g_dbus_object_get_object_path(object); try { p->devices.at(object_path)->filesystem_removed(); } catch (const std::out_of_range &e) { } } } namespace { DeviceInfo::DeviceInfo(mediascanner::MountWatcherPrivate *p, UDisksObject *dev) : p(p), device(reinterpret_cast(g_object_ref(dev)), g_object_unref), filesystem(nullptr, g_object_unref) { if (udisks_object_peek_filesystem(device.get())) { filesystem_added(); } } DeviceInfo::~DeviceInfo() { filesystem_removed(); } void DeviceInfo::filesystem_added() { if (mount_point_changed_id != 0) { g_signal_handler_disconnect(filesystem.get(), mount_point_changed_id); mount_point_changed_id = 0; } filesystem.reset(udisks_object_get_filesystem(device.get())); mount_point_changed_id = g_signal_connect( filesystem.get(), "notify::mount-points", G_CALLBACK(&DeviceInfo::mount_point_changed), this); update_mount_state(); } void DeviceInfo::filesystem_removed() { if (mount_point_changed_id != 0) { g_signal_handler_disconnect(filesystem.get(), mount_point_changed_id); mount_point_changed_id = 0; } filesystem.reset(); update_mount_state(); } void DeviceInfo::update_mount_state() { const char *new_mount_point = nullptr; if (filesystem) { auto mount_points = udisks_filesystem_get_mount_points(filesystem.get()); new_mount_point = mount_points ? mount_points[0] : nullptr; } // Has the mount state changed? if ((new_mount_point != nullptr) == is_mounted) { return; } // Construct an info structure for the callback mediascanner::MountWatcher::Info mount_info; mount_info.is_mounted = new_mount_point != nullptr; mount_info.mount_point = mount_info.is_mounted ? std::string(new_mount_point) : mount_point; UDisksBlock *block = udisks_object_peek_block(device.get()); const char *device = udisks_block_get_device(block); if (device != nullptr) { mount_info.device = device; } const char *uuid = udisks_block_get_id_uuid(block); if (uuid != nullptr) { mount_info.uuid = uuid; } // And then update our internal state is_mounted = new_mount_point != nullptr; if (is_mounted) { mount_point = new_mount_point; } else { mount_point = ""; } if (p->callback) { p->callback(mount_info); } } void DeviceInfo::mount_point_changed(GObject *, GParamSpec *, void *user_data) noexcept { DeviceInfo *info = reinterpret_cast(user_data); info->update_mount_state(); } } mediascanner2-0.115/src/daemon/MountWatcher.hh000066400000000000000000000022621436755250000212370ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include namespace mediascanner { struct MountWatcherPrivate; class MountWatcher final { public: struct Info { std::string device; std::string uuid; std::string mount_point; bool is_mounted; }; MountWatcher(std::function callback); ~MountWatcher(); MountWatcher(const MountWatcher&) = delete; MountWatcher& operator=(MountWatcher& o) = delete; private: MountWatcherPrivate *p; }; } mediascanner2-0.115/src/daemon/Scanner.cc000066400000000000000000000072141436755250000202000ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "Scanner.hh" #include "../extractor/DetectedFile.hh" #include "../extractor/MetadataExtractor.hh" #include "../mediascanner/internal/utils.hh" #include #include #include #include #include using namespace std; namespace mediascanner { struct Scanner::Private { Private(MetadataExtractor *extractor_, const std::string &root, const MediaType type_); string curdir; filesystem::directory_iterator dir; vector dirs; MediaType type; MetadataExtractor *extractor; }; Scanner::Private::Private(MetadataExtractor *extractor, const std::string &root, const MediaType type) : type(type), extractor(extractor) { dirs.push_back(root); } Scanner::Scanner(MetadataExtractor *extractor, const std::string &root, const MediaType type) : p(new Scanner::Private(extractor, root, type)) { } Scanner::~Scanner() { delete p; } DetectedFile Scanner::next() { begin: while(p->dir == filesystem::directory_iterator()) { if(p->dirs.empty()) { throw StopIteration(); } p->curdir = p->dirs.back(); p->dirs.pop_back(); p->dir = filesystem::directory_iterator(p->curdir); if(is_rootlike(p->curdir)) { fprintf(stderr, "Directory %s looks like a top level root directory, skipping it (%s).\n", p->curdir.c_str(), __PRETTY_FUNCTION__); p->dir = filesystem::directory_iterator(); continue; } if(has_scanblock(p->curdir)) { fprintf(stderr, "Directory %s has a scan block file, skipping it.\n", p->curdir.c_str()); p->dir = filesystem::directory_iterator(); continue; } printf("In subdir %s\n", p->curdir.c_str()); } while (p->dir != filesystem::end(p->dir)) { const auto& entry = *(p->dir); const auto fname = entry.path().filename().string(); if(fname[0] == '.') { // Ignore hidden files and dirs. ++p->dir; continue; } string fullpath = p->curdir + "/" + fname; if(entry.is_regular_file()) { try { DetectedFile d = p->extractor->detect(fullpath); if (p->type == AllMedia || d.type == p->type) { ++p->dir; return d; } } catch (const exception &e) { /* Ignore non-media files */ } } else if(entry.is_directory()) { p->dirs.push_back(fullpath); } ++p->dir; } // Nothing left in this directory so on to the next. p->dir = filesystem::directory_iterator(); p->curdir.clear(); // This should be just return next(s) but we can't guarantee // that GCC can optimize away the tail recursion so we do this // instead. Using goto instead of wrapping the whole function body in // a while(true) to avoid the extra indentation. goto begin; } } mediascanner2-0.115/src/daemon/Scanner.hh000066400000000000000000000023701436755250000202100ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef SCANNER_HH_ #define SCANNER_HH_ #include #include #include #include namespace mediascanner { struct DetectedFile; class MetadataExtractor; class StopIteration : public std::exception { }; class Scanner final { public: Scanner(MetadataExtractor *extractor, const std::string &root, const MediaType type); ~Scanner(); Scanner(const Scanner &o) = delete; Scanner& operator=(const Scanner &o) = delete; DetectedFile next(); private: struct Private; Private *p; }; } #endif mediascanner2-0.115/src/daemon/SubtreeWatcher.cc000066400000000000000000000227451436755250000215440ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "SubtreeWatcher.hh" #include "../mediascanner/MediaStore.hh" #include "../mediascanner/MediaFile.hh" #include "InvalidationSender.hh" #include "../extractor/DetectedFile.hh" #include "../extractor/MetadataExtractor.hh" #include "../mediascanner/internal/utils.hh" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; namespace mediascanner { struct SubtreeWatcherPrivate { MediaStore &store; // Hackhackhack, should be replaced with callback object or something. MetadataExtractor &extractor; InvalidationSender &invalidator; int inotifyid; // Ideally use boost::bimap or something instead of these two separate objects. std::map wd2str; std::map str2wd; bool keep_going; std::unique_ptr source; SubtreeWatcherPrivate(MediaStore &store, MetadataExtractor &extractor, InvalidationSender &invalidator) : store(store), extractor(extractor), invalidator(invalidator), inotifyid(inotify_init()), keep_going(true), source(g_unix_fd_source_new(inotifyid, G_IO_IN), g_source_unref) { } ~SubtreeWatcherPrivate() { for(auto &i : wd2str) { inotify_rm_watch(inotifyid, i.first); } close(inotifyid); } }; static gboolean source_callback(GIOChannel *, GIOCondition, gpointer data) { SubtreeWatcher *watcher = static_cast(data); watcher->processEvents(); return TRUE; } SubtreeWatcher::SubtreeWatcher(MediaStore &store, MetadataExtractor &extractor, InvalidationSender &invalidator) { p = new SubtreeWatcherPrivate(store, extractor, invalidator); if(p->inotifyid == -1) { string msg("Could not init inotify: "); msg += strerror(errno); delete p; throw runtime_error(msg); } g_source_set_callback(p->source.get(), reinterpret_cast(source_callback), static_cast(this), nullptr); g_source_attach(p->source.get(), nullptr); } SubtreeWatcher::~SubtreeWatcher() { g_source_destroy(p->source.get()); delete p; } void SubtreeWatcher::addDir(const string &root) { if(root[0] != '/') throw runtime_error("Path must be absolute."); if(is_rootlike(root)) { fprintf(stderr, "Directory %s looks like a top level root directory, skipping it (%s).\n", root.c_str(), __PRETTY_FUNCTION__); return; } if(has_scanblock(root)) { fprintf(stderr, "Directory %s has a scan block file, skipping it.\n", root.c_str()); return; } if(p->str2wd.find(root) != p->str2wd.end()) return; int wd = inotify_add_watch(p->inotifyid, root.c_str(), IN_CREATE | IN_DELETE_SELF | IN_DELETE | IN_CLOSE_WRITE | IN_MOVED_FROM | IN_MOVED_TO | IN_ONLYDIR); if(wd == -1) { fprintf(stderr, "Could not create inotify watch object: %s\n", strerror(errno)); return; // Probably ran out of watches, keep monitoring what we can. } p->wd2str[wd] = root; p->str2wd[root] = wd; printf("Watching subdirectory %s, %ld watches in total.\n", root.c_str(), (long)p->wd2str.size()); for (const auto& entry: filesystem::directory_iterator(root)) { string fname = entry.path().filename().string(); if(fname[0] == '.') // Ignore hidden entries and also "." and "..". continue; string fullpath = root + "/" + fname; if(entry.is_directory()) { addDir(fullpath); } else if (entry.is_regular_file()) { fileAdded(fullpath); } } } bool SubtreeWatcher::removeDir(const string &abspath) { auto it = p->str2wd.find(abspath); if (it == p->str2wd.end()) { return false; } while (it != p->str2wd.end()) { // Stop if we're no longer dealing with subdirectories of abspath if (it->first.compare(0, abspath.size(), abspath) != 0) { break; } if (it->first.size() != abspath.size() && it->first[abspath.size()] != '/') { break; } int wd = it->second; inotify_rm_watch(p->inotifyid, wd); p->wd2str.erase(wd); printf("Stopped watching %s, %ld directories remain.\n", it->first.c_str(), (long)p->wd2str.size()); it = p->str2wd.erase(it); } if(p->wd2str.empty()) p->keep_going = false; p->store.removeSubtree(abspath); return true; } bool SubtreeWatcher::fileAdded(const string &abspath) { bool changed = false; printf("New file was created: %s.\n", abspath.c_str()); try { DetectedFile d = p->extractor.detect(abspath); if(p->store.is_broken_file(abspath, d.etag)) { fprintf(stderr, "Using fallback data for unscannable file %s.\n", abspath.c_str()); p->store.insert(p->extractor.fallback_extract(d)); } else if (d.etag != p->store.getETag(d.filename)) { // Only extract and insert the file if the ETag has changed. p->store.insert_broken_file(abspath, d.etag); // If detection dies, insertion into broken files persists // and the next time this file is encountered, it is skipped. // Insert cleans broken status of the file. MediaFile media; try { media = p->extractor.extract(d); } catch (const std::runtime_error &e) { fprintf(stderr, "Error extracting from '%s': %s\n", d.filename.c_str(), e.what()); media = p->extractor.fallback_extract(d); } p->store.insert(std::move(media)); changed = true; } } catch(const exception &e) { fprintf(stderr, "Error when adding new file: %s\n", e.what()); } return changed; } void SubtreeWatcher::fileDeleted(const string &abspath) { printf("File was deleted: %s\n", abspath.c_str()); p->store.remove(abspath); } void SubtreeWatcher::dirAdded(const string &abspath) { printf("New directory was created: %s.\n", abspath.c_str()); addDir(abspath); } void SubtreeWatcher::dirRemoved(const string &abspath) { printf("Subdirectory was deleted: %s.\n", abspath.c_str()); removeDir(abspath); } void SubtreeWatcher::processEvents() { const int BUFSIZE=4096; char buf[BUFSIZE]; bool changed = false; ssize_t num_read; num_read = read(p->inotifyid, buf, BUFSIZE); if(num_read == 0) { printf("Inotify returned 0.\n"); return; } if(num_read == -1) { printf("Read error.\n"); return; } for(char *d = buf; d < buf + num_read;) { struct inotify_event *event = (struct inotify_event *) d; if (p->wd2str.find(event->wd) == p->wd2str.end()) { // Ignore events for unknown watches. We may receive // such events when a directory is removed. d += sizeof(struct inotify_event) + event->len; continue; } string directory = p->wd2str[event->wd]; string filename(event->name); string abspath = directory + '/' + filename; bool is_dir = false; bool is_file = false; struct stat statbuf; lstat(abspath.c_str(), &statbuf); // Remember: these are not valid in case of delete event. is_dir = S_ISDIR(statbuf.st_mode); is_file = S_ISREG(statbuf.st_mode); if(event->mask & IN_CREATE) { if(is_dir) { dirAdded(abspath); changed = true; } // Do not add files upon creation because we can't parse // their metadata until it is fully written. } else if((event->mask & IN_CLOSE_WRITE) || (event->mask & IN_MOVED_TO)) { if(is_dir) { dirAdded(abspath); changed = true; } if(is_file) { changed = fileAdded(abspath); } } else if((event->mask & IN_DELETE) || (event->mask & IN_MOVED_FROM)) { if(p->str2wd.find(abspath) != p->str2wd.end()) { dirRemoved(abspath); changed = true; } else { fileDeleted(abspath); changed = true; } } else if((event->mask & IN_IGNORED) || (event->mask & IN_UNMOUNT) || (event->mask & IN_DELETE_SELF)) { removeDir(abspath); changed = true; } d += sizeof(struct inotify_event) + event->len; } if (changed) { p->invalidator.invalidate(); } } int SubtreeWatcher::getFd() const { return p->inotifyid; } int SubtreeWatcher::directoryCount() const { return (int) p->wd2str.size(); } } mediascanner2-0.115/src/daemon/SubtreeWatcher.hh000066400000000000000000000031021436755250000215400ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef SUBTREEWATCHER_HH_ #define SUBTREEWATCHER_HH_ #include namespace mediascanner { class InvalidationSender; class MediaStore; class MetadataExtractor; struct SubtreeWatcherPrivate; class SubtreeWatcher final { private: SubtreeWatcherPrivate *p; bool fileAdded(const std::string &abspath); void fileDeleted(const std::string &abspath); void dirAdded(const std::string &abspath); void dirRemoved(const std::string &abspath); bool removeDir(const std::string &abspath); public: SubtreeWatcher(MediaStore &store, MetadataExtractor &extractor, InvalidationSender &invalidator); ~SubtreeWatcher(); SubtreeWatcher(const SubtreeWatcher &o) = delete; SubtreeWatcher& operator=(const SubtreeWatcher &o) = delete; void addDir(const std::string &path); void processEvents(); int getFd() const; int directoryCount() const; }; } #endif mediascanner2-0.115/src/daemon/VolumeManager.cc000066400000000000000000000162141436755250000213510ustar00rootroot00000000000000/* * Copyright (C) 2016 Canonical, Ltd. * * Authors: * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "VolumeManager.hh" #include #include #include #include #include "InvalidationSender.hh" #include "Scanner.hh" #include "SubtreeWatcher.hh" #include "../mediascanner/internal/utils.hh" #include #include #include #include #include #include using namespace std; namespace { enum class VolumeEventType { added, removed, }; struct VolumeEvent { VolumeEventType type; string path; VolumeEvent(VolumeEventType type, const string& path) : type(type), path(path) {} }; } namespace mediascanner { struct VolumeManagerPrivate { MediaStore& store; MetadataExtractor& extractor; InvalidationSender& invalidator; map> volumes; deque pending; unsigned int idle_id = 0; VolumeManagerPrivate(MediaStore& store, MetadataExtractor& extractor, InvalidationSender& invalidator); ~VolumeManagerPrivate(); void queueUpdate(VolumeEventType type, const string& path); static gboolean processEvent(void *user_data) noexcept; void addVolume(const string& path); void removeVolume(const string& path); void readFiles(const string& subdir, const MediaType type); }; VolumeManager::VolumeManager(MediaStore& store, MetadataExtractor& extractor, InvalidationSender& invalidator) : p(new VolumeManagerPrivate(store, extractor, invalidator)) { } VolumeManager::~VolumeManager() = default; void VolumeManager::queueAddVolume(const string& path) { p->queueUpdate(VolumeEventType::added, path); } void VolumeManager::queueRemoveVolume(const string& path) { p->queueUpdate(VolumeEventType::removed, path); } bool VolumeManager::idle() const { // idle_id will only be reset once the scanning job has completed. return p->idle_id == 0; } VolumeManagerPrivate::VolumeManagerPrivate(MediaStore& store, MetadataExtractor& extractor, InvalidationSender& invalidator) : store(store), extractor(extractor), invalidator(invalidator) { } VolumeManagerPrivate::~VolumeManagerPrivate() { if (idle_id != 0) { g_source_remove(idle_id); } } void VolumeManagerPrivate::queueUpdate(VolumeEventType type, const string& path) { for (auto it = pending.begin(); it != pending.end();) { if (it->path == path) { it = pending.erase(it); } else { ++it; } } pending.emplace_back(type, path); if (idle_id == 0) { idle_id = g_idle_add(&VolumeManagerPrivate::processEvent, this); } } gboolean VolumeManagerPrivate::processEvent(void *user_data) noexcept { auto *p = reinterpret_cast(user_data); while (!p->pending.empty()) { auto event = move(p->pending.front()); p->pending.pop_front(); switch (event.type) { case VolumeEventType::added: p->addVolume(event.path); break; case VolumeEventType::removed: p->removeVolume(event.path); break; } } p->invalidator.invalidate(); p->idle_id = 0; return G_SOURCE_REMOVE; } void VolumeManagerPrivate::addVolume(const string& path) { assert(path[0] == '/'); if(volumes.find(path) != volumes.end()) { return; } if(is_rootlike(path)) { fprintf(stderr, "Directory %s looks like a top level root directory, skipping it (%s).\n", path.c_str(), __PRETTY_FUNCTION__); return; } if(is_optical_disc(path)) { fprintf(stderr, "Directory %s looks like an optical disc, skipping it.\n", path.c_str()); return; } if(has_scanblock(path)) { fprintf(stderr, "Directory %s has a scan block file, skipping it.\n", path.c_str()); return; } unique_ptr sw(new SubtreeWatcher(store, extractor, invalidator)); store.restoreItems(path); store.pruneDeleted(); readFiles(path, AllMedia); sw->addDir(path); volumes[path] = move(sw); } void VolumeManagerPrivate::removeVolume(const string& path) { assert(path[0] == '/'); if(volumes.find(path) == volumes.end()) return; store.archiveItems(path); volumes.erase(path); } void VolumeManagerPrivate::readFiles(const string &subdir, const MediaType type) { Scanner s(&extractor, subdir, type); MediaStoreTransaction txn = store.beginTransaction(); const int update_interval = 10; // How often to send invalidations. struct timespec previous_update, current_time; clock_gettime(CLOCK_MONOTONIC, &previous_update); previous_update.tv_sec -= update_interval/2; // Send the first update sooner for better visual appeal. while(true) { try { auto d = s.next(); clock_gettime(CLOCK_MONOTONIC, ¤t_time); while(g_main_context_pending(g_main_context_default())) { g_main_context_iteration(g_main_context_default(), FALSE); } if(current_time.tv_sec - previous_update.tv_sec >= update_interval) { txn.commit(); invalidator.invalidate(); previous_update = current_time; } // If the file is broken or unchanged, use fallback. if (store.is_broken_file(d.filename, d.etag)) { fprintf(stderr, "Using fallback data for unscannable file %s.\n", d.filename.c_str()); store.insert(extractor.fallback_extract(d)); continue; } if(d.etag == store.getETag(d.filename)) continue; try { store.insert_broken_file(d.filename, d.etag); MediaFile media; try { media = extractor.extract(d); } catch (const runtime_error &e) { fprintf(stderr, "Error extracting from '%s': %s\n", d.filename.c_str(), e.what()); media = extractor.fallback_extract(d); } store.insert(std::move(media)); } catch(const exception &e) { fprintf(stderr, "Error when indexing: %s\n", e.what()); } } catch(const StopIteration &stop) { break; } } txn.commit(); } } mediascanner2-0.115/src/daemon/VolumeManager.hh000066400000000000000000000024731436755250000213650ustar00rootroot00000000000000/* * Copyright (C) 2016 Canonical, Ltd. * * Authors: * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #pragma once #include #include namespace mediascanner { class MediaStore; class MetadataExtractor; class InvalidationSender; struct VolumeManagerPrivate; class VolumeManager final { public: VolumeManager(MediaStore& store, MetadataExtractor& extractor, InvalidationSender& invalidator); ~VolumeManager(); VolumeManager(const VolumeManager&) = delete; VolumeManager& operator=(const VolumeManager&) = delete; void queueAddVolume(const std::string& path); void queueRemoveVolume(const std::string& path); bool idle() const; private: std::unique_ptr p; }; } mediascanner2-0.115/src/daemon/scannerdaemon.cc000066400000000000000000000202321436755250000214170ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include "../mediascanner/MediaFile.hh" #include "../mediascanner/MediaStore.hh" #include "../extractor/MetadataExtractor.hh" #include "MountWatcher.hh" #include "InvalidationSender.hh" #include "VolumeManager.hh" using namespace std; using namespace mediascanner; namespace { bool is_same_directory(const char *dir1, const char *dir2) { struct stat s1, s2; if(stat(dir1, &s1) != 0) { return false; } if(stat(dir2, &s2) != 0) { return false; } return s1.st_dev == s2.st_dev && s1.st_ino == s2.st_ino; } } static const char BUS_NAME[] = "com.canonical.MediaScanner2.Daemon"; static const unsigned int INVALIDATE_DELAY = 1; class ScannerDaemon final { public: ScannerDaemon(); ~ScannerDaemon(); int run(); private: void setupBus(); void setupSignals(); void setupMountWatcher(); static gboolean signalCallback(gpointer data); static void busNameLostCallback(GDBusConnection *connection, const char *name, gpointer data); void mountEvent(const MountWatcher::Info &info); unique_ptr mount_watcher; unsigned int sigint_id = 0, sigterm_id = 0; string cachedir; unique_ptr store; unique_ptr extractor; InvalidationSender invalidator; unique_ptr volumes; unique_ptr main_loop; unique_ptr session_bus; unsigned int bus_name_id = 0; }; ScannerDaemon::ScannerDaemon() : main_loop(g_main_loop_new(nullptr, FALSE), g_main_loop_unref), session_bus(nullptr, g_object_unref) { setupBus(); store.reset(new MediaStore(MS_READ_WRITE, "/media/")); extractor.reset(new MetadataExtractor(session_bus.get())); volumes.reset(new VolumeManager(*store, *extractor, invalidator)); setupMountWatcher(); const char *musicdir = g_get_user_special_dir(G_USER_DIRECTORY_MUSIC); const char *videodir = g_get_user_special_dir(G_USER_DIRECTORY_VIDEOS); const char *picturesdir = g_get_user_special_dir(G_USER_DIRECTORY_PICTURES); const char *homedir = g_get_home_dir(); // According to XDG specification, when one of the special dirs is missing // it falls back to home directory. This would mean scanning the entire home // directory. This is probably not what people want so skip if this is the case. if (musicdir && !is_same_directory(musicdir, homedir)) volumes->queueAddVolume(musicdir); if (videodir && !is_same_directory(videodir, homedir)) volumes->queueAddVolume(videodir); if (picturesdir && !is_same_directory(picturesdir, homedir)) volumes->queueAddVolume(picturesdir); // In case someone opened the db file before we could populate it. invalidator.invalidate(); // This is at the end because the initial scan may take a while // and is not interruptible but we want the process to die if it // gets a SIGINT or the like. setupSignals(); } ScannerDaemon::~ScannerDaemon() { if (sigint_id != 0) { g_source_remove(sigint_id); } if (sigterm_id != 0) { g_source_remove(sigterm_id); } if (bus_name_id != 0) { g_bus_unown_name(bus_name_id); } } void ScannerDaemon::busNameLostCallback(GDBusConnection *, const char *name, gpointer data) { ScannerDaemon *daemon = static_cast(data); fprintf(stderr, "Exiting due to loss of control of bus name %s\n", name); daemon->bus_name_id = 0; g_main_loop_quit(daemon->main_loop.get()); } void ScannerDaemon::setupBus() { GError *error = nullptr; session_bus.reset(g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error)); if (!session_bus) { string errortxt(error->message); g_error_free(error); string msg = "Failed to connect to session bus: "; msg += errortxt; throw runtime_error(msg); } invalidator.setBus(session_bus.get()); invalidator.setDelay(INVALIDATE_DELAY); bus_name_id = g_bus_own_name_on_connection( session_bus.get(), BUS_NAME, static_cast( G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | G_BUS_NAME_OWNER_FLAGS_REPLACE), nullptr, &ScannerDaemon::busNameLostCallback, this, nullptr); } gboolean ScannerDaemon::signalCallback(gpointer data) { ScannerDaemon *daemon = static_cast(data); g_main_loop_quit(daemon->main_loop.get()); return G_SOURCE_CONTINUE; } void ScannerDaemon::setupSignals() { sigint_id = g_unix_signal_add(SIGINT, &ScannerDaemon::signalCallback, this); sigterm_id = g_unix_signal_add(SIGTERM, &ScannerDaemon::signalCallback, this); } int ScannerDaemon::run() { g_main_loop_run(main_loop.get()); return 99; } void ScannerDaemon::setupMountWatcher() { try { using namespace std::placeholders; mount_watcher.reset( new MountWatcher(std::bind(&ScannerDaemon::mountEvent, this, _1))); } catch (const std::runtime_error &e) { fprintf(stderr, "Failed to connect to udisksd: %s\n", e.what()); fprintf(stderr, "Removable media support disabled\n"); return; } } void ScannerDaemon::mountEvent(const MountWatcher::Info& info) { if (info.is_mounted) { printf("Volume %s was mounted.\n", info.mount_point.c_str()); if (info.mount_point.substr(0, 6) == "/media") { volumes->queueAddVolume(info.mount_point); } } else { printf("Volume %s was unmounted.\n", info.mount_point.c_str()); volumes->queueRemoveVolume(info.mount_point); } } static void validate_desktop() { // Only set manually const gchar *ms_run_env = g_getenv("MEDIASCANNER_RUN"); if (g_strcmp0(ms_run_env, "1") == 0) return; // Only set properly in 17.04 and onward const gchar *desktop_env = g_getenv("XDG_CURRENT_DESKTOP"); if (g_regex_match_simple("(^|:)Unity8(:|$)", desktop_env, (GRegexCompileFlags)0, (GRegexMatchFlags)0)) return; // Shouldn't rely on this, but we only need to use it for 16.04 - 17.04 const gchar *session_env = g_getenv("XDG_SESSION_DESKTOP"); if (g_strcmp0("unity8", session_env) == 0) return; // We should also start on Ubuntu touch! if (g_strcmp0("ubuntu-touch", session_env) == 0) return; // We shouldn't run if we weren't asked for; we can confuse some desktops // (like unity7) with our scanning of mounted drives and the like. printf("Mediascanner service not starting due to unsupported desktop environment.\n"); printf("Set MEDIASCANNER_RUN=1 to override this.\n"); exit(0); } static void print_banner() { char timestr[200]; time_t t; struct tm *tmp; t = time(NULL); tmp = localtime(&t); if (tmp == NULL) { printf("\nMediascanner service starting.\n\n"); return; } if (strftime(timestr, sizeof(timestr), "%Y-%m-%d %H:%M:%S", tmp) == 0) { printf("\nMediascanner service starting.\n\n"); return; } printf("\nMediascanner service starting at %s.\n\n", timestr); } int main(int /*argc*/, char **/*argv*/) { validate_desktop(); print_banner(); try { ScannerDaemon d; return d.run(); } catch(string &s) { printf("Error: %s\n", s.c_str()); } return 100; } mediascanner2-0.115/src/extractor/000077500000000000000000000000001436755250000170445ustar00rootroot00000000000000mediascanner2-0.115/src/extractor/CMakeLists.txt000066400000000000000000000035131436755250000216060ustar00rootroot00000000000000 add_definitions(${MEDIASCANNER_DEPS_CFLAGS} ${GST_CFLAGS} ${EXIF_CFLAGS} ${PIXBUF_CFLAGS} ${TAGLIB_CFLAGS}) include_directories(.. ${CMAKE_CURRENT_BINARY_DIR}) # Build stubs/skeletons for D-Bus interface find_program(gdbus_codegen gdbus-codegen) if(NOT gdbus_codegen) msg(FATAL_ERROR "Could not locate gdbus-codegen") endif() add_custom_command( OUTPUT dbus-generated.c dbus-generated.h COMMAND ${gdbus_codegen} --interface-prefix=com.canonical.MediaScanner2 --generate-c-code dbus-generated --c-namespace MS ${CMAKE_CURRENT_SOURCE_DIR}/dbus-interface.xml MAIN_DEPENDENCY dbus-interface.xml ) # Code generated by gdbus-codegen doesn't like all the warning flags # we have turned on. set_property(SOURCE dbus-generated.c APPEND_STRING PROPERTY COMPILE_FLAGS " -Wno-unused-parameter -Wno-pedantic") # The client code for the extractor daemon add_library(extractor-client STATIC MetadataExtractor.cc dbus-generated.c dbus-marshal.cc ../mediascanner/utils.cc ) target_link_libraries(extractor-client mediascanner ) # The backend code for the extractor daemon, as a library for use by tests add_library(extractor-backend STATIC ExtractorBackend.cc GStreamerExtractor.cc ImageExtractor.cc TaglibExtractor.cc ) target_link_libraries(extractor-backend extractor-client ${GST_LDFLAGS} ${EXIF_LDFLAGS} ${PIXBUF_LDFLAGS} ${TAGLIB_LDFLAGS} ) add_executable(mediascanner-extractor main.cc ) target_link_libraries(mediascanner-extractor extractor-backend) install( TARGETS mediascanner-extractor RUNTIME DESTINATION ${CMAKE_INSTALL_LIBEXECDIR}/mediascanner-2.0 ) configure_file( com.canonical.MediaScanner2.Extractor.service.in com.canonical.MediaScanner2.Extractor.service) install( FILES ${CMAKE_CURRENT_BINARY_DIR}/com.canonical.MediaScanner2.Extractor.service DESTINATION ${CMAKE_INSTALL_DATADIR}/dbus-1/services ) mediascanner2-0.115/src/extractor/DetectedFile.hh000066400000000000000000000024721436755250000217130ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef EXTRACTOR_DETECTEDFILE_H #define EXTRACTOR_DETECTEDFILE_H #include #include #include "../mediascanner/scannercore.hh" namespace mediascanner { struct DetectedFile { DetectedFile(const std::string &filename, const std::string &etag, const std::string &content_type, uint64_t mtime, MediaType type) : filename(filename), etag(etag), content_type(content_type) , mtime(mtime), type(type) {} std::string filename; std::string etag; std::string content_type; uint64_t mtime; MediaType type; }; } #endif mediascanner2-0.115/src/extractor/ExtractorBackend.cc000066400000000000000000000037701436755250000226050ustar00rootroot00000000000000/* * Copyright (C) 2013-2014 Canonical, Ltd. * * Authors: * Jussi Pakkanen * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ExtractorBackend.hh" #include "DetectedFile.hh" #include "GStreamerExtractor.hh" #include "ImageExtractor.hh" #include "TaglibExtractor.hh" #include "../mediascanner/MediaFile.hh" #include "../mediascanner/MediaFileBuilder.hh" #include #include using namespace std; namespace mediascanner { struct ExtractorBackendPrivate { ExtractorBackendPrivate(int seconds) : gstreamer(seconds) {} GStreamerExtractor gstreamer; ImageExtractor image; TaglibExtractor taglib; }; ExtractorBackend::ExtractorBackend(int seconds) : p(new ExtractorBackendPrivate(seconds)) { } ExtractorBackend::~ExtractorBackend() { delete p; } MediaFile ExtractorBackend::extract(const DetectedFile &d) { printf("Extracting metadata from %s.\n", d.filename.c_str()); MediaFileBuilder mfb(d.filename); mfb.setETag(d.etag); mfb.setContentType(d.content_type); mfb.setModificationTime(d.mtime); mfb.setType(d.type); switch (d.type) { case ImageMedia: p->image.extract(d, mfb); break; case AudioMedia: if (!p->taglib.extract(d, mfb)) { p->gstreamer.extract(d, mfb); } break; default: p->gstreamer.extract(d, mfb); break; } return mfb; } } mediascanner2-0.115/src/extractor/ExtractorBackend.hh000066400000000000000000000024041436755250000226100ustar00rootroot00000000000000/* * Copyright (C) 2013-2014 Canonical, Ltd. * * Authors: * Jussi Pakkanen * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef EXTRACTOR_EXTRACTORBACKEND_H #define EXTRACTOR_EXTRACTORBACKEND_H #include namespace mediascanner { class MediaFile; struct DetectedFile; struct ExtractorBackendPrivate; class ExtractorBackend final { public: explicit ExtractorBackend(int seconds=25); ~ExtractorBackend(); ExtractorBackend(const ExtractorBackend&) = delete; ExtractorBackend& operator=(ExtractorBackend &o) = delete; MediaFile extract(const DetectedFile &d); private: ExtractorBackendPrivate *p; }; } #endif mediascanner2-0.115/src/extractor/GStreamerExtractor.cc000066400000000000000000000134351436755250000231460ustar00rootroot00000000000000/* * Copyright (C) 2013-2014 Canonical, Ltd. * * Authors: * Jussi Pakkanen * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "GStreamerExtractor.hh" #include "DetectedFile.hh" #include "../mediascanner/MediaFile.hh" #include "../mediascanner/MediaFileBuilder.hh" #include "../mediascanner/internal/utils.hh" #include #include #include #include #include #include #include using namespace std; using mediascanner::MediaFileBuilder; namespace { void extract_tag_info (const GstTagList *list, const gchar *tag, void *user_data) { MediaFileBuilder *mfb = (MediaFileBuilder *) user_data; int i, num; string tagname(tag); if(tagname == GST_TAG_IMAGE || tagname == GST_TAG_PREVIEW_IMAGE) { mfb->setHasThumbnail(true); return; } num = gst_tag_list_get_tag_size (list, tag); for (i = 0; i < num; ++i) { const GValue *val; val = gst_tag_list_get_value_index (list, tag, i); if (G_VALUE_HOLDS_STRING(val)) { const char *value = g_value_get_string(val); if (!value) { continue; } if (tagname == GST_TAG_ARTIST) mfb->setAuthor(value); else if (tagname == GST_TAG_TITLE) mfb->setTitle(value); else if (tagname == GST_TAG_ALBUM) mfb->setAlbum(value); else if (tagname == GST_TAG_ALBUM_ARTIST) mfb->setAlbumArtist(value); else if (tagname == GST_TAG_GENRE) mfb->setGenre(value); } else if (G_VALUE_HOLDS(val, GST_TYPE_DATE_TIME)) { if (tagname == GST_TAG_DATE_TIME) { GstDateTime *dt = static_cast(g_value_get_boxed(val)); if (!dt) { continue; } char *dt_string = gst_date_time_to_iso8601_string(dt); mfb->setDate(dt_string); g_free(dt_string); } } else if (G_VALUE_HOLDS(val, G_TYPE_DATE)) { if (tagname == GST_TAG_DATE) { GDate *dt = static_cast(g_value_get_boxed(val)); if (!dt) { continue; } char buf[100]; if (g_date_strftime(buf, sizeof(buf), "%Y-%m-%d", dt) != 0) { mfb->setDate(buf); } } } else if (G_VALUE_HOLDS_UINT(val)) { if (tagname == GST_TAG_TRACK_NUMBER) { mfb->setTrackNumber(g_value_get_uint(val)); } else if (tagname == GST_TAG_ALBUM_VOLUME_NUMBER) { mfb->setDiscNumber(g_value_get_uint(val)); } } } } } namespace mediascanner { GStreamerExtractor::GStreamerExtractor(int seconds) : discoverer(nullptr, g_object_unref) { GError *error = nullptr; discoverer.reset(gst_discoverer_new(GST_SECOND * seconds, &error)); if (not discoverer) { string errortxt(error->message); g_error_free(error); string msg = "Failed to create discoverer: "; msg += errortxt; throw runtime_error(msg); } if (error) { // Sometimes this is filled in even though no error happens. g_error_free(error); } } GStreamerExtractor::~GStreamerExtractor() = default; void GStreamerExtractor::extract(const DetectedFile &d, MediaFileBuilder &mfb) { string uri = getUri(d.filename); GError *error = nullptr; unique_ptr info( gst_discoverer_discover_uri(discoverer.get(), uri.c_str(), &error), g_object_unref); if (info.get() == nullptr) { string errortxt(error->message); g_error_free(error); string msg = "Discovery of file "; msg += d.filename; msg += " failed: "; msg += errortxt; throw runtime_error(msg); } if (error) { // Sometimes this gets filled in even if no error actually occurs. g_error_free(error); error = nullptr; } if (gst_discoverer_info_get_result(info.get()) != GST_DISCOVERER_OK) { throw runtime_error("Unable to discover file " + d.filename); } const GstTagList *tags = gst_discoverer_info_get_tags(info.get()); if (tags != nullptr) { gst_tag_list_foreach(tags, extract_tag_info, &mfb); } mfb.setDuration(static_cast( gst_discoverer_info_get_duration(info.get())/GST_SECOND)); /* Check for video specific information */ unique_ptr streams( gst_discoverer_info_get_stream_list(info.get()), gst_discoverer_stream_info_list_free); for (const GList *l = streams.get(); l != nullptr; l = l->next) { auto stream_info = static_cast(l->data); if (GST_IS_DISCOVERER_VIDEO_INFO(stream_info)) { mfb.setWidth(gst_discoverer_video_info_get_width( GST_DISCOVERER_VIDEO_INFO(stream_info))); mfb.setHeight(gst_discoverer_video_info_get_height( GST_DISCOVERER_VIDEO_INFO(stream_info))); break; } } } } mediascanner2-0.115/src/extractor/GStreamerExtractor.hh000066400000000000000000000025351436755250000231570ustar00rootroot00000000000000/* * Copyright (C) 2013-2014 Canonical, Ltd. * * Authors: * Jussi Pakkanen * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef EXTRACTOR_GSTREAMEREXTRACTOR_H #define EXTRACTOR_GSTREAMEREXTRACTOR_H #include typedef struct _GstDiscoverer GstDiscoverer; namespace mediascanner { class MediaFileBuilder; struct DetectedFile; class GStreamerExtractor final { public: explicit GStreamerExtractor(int seconds); ~GStreamerExtractor(); GStreamerExtractor(const GStreamerExtractor&) = delete; GStreamerExtractor& operator=(GStreamerExtractor &o) = delete; void extract(const DetectedFile &d, MediaFileBuilder &builder); private: std::unique_ptr discoverer; }; } #endif mediascanner2-0.115/src/extractor/ImageExtractor.cc000066400000000000000000000163261436755250000223010ustar00rootroot00000000000000/* * Copyright (C) 2013-2014 Canonical, Ltd. * * Authors: * Jussi Pakkanen * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ImageExtractor.hh" #include "DetectedFile.hh" #include "../mediascanner/MediaFile.hh" #include "../mediascanner/MediaFileBuilder.hh" #include #include #include #include #include #include #include #include using namespace std; using mediascanner::MediaFileBuilder; namespace { const char exif_date_template[] = "%Y:%m:%d %H:%M:%S"; const char iso8601_date_format[] = "%Y-%m-%dT%H:%M:%S"; const char iso8601_date_with_zone_format[] = "%Y-%m-%dT%H:%M:%S%z"; void parse_exif_date(ExifData *data, ExifByteOrder order, MediaFileBuilder &mfb) { static const std::vector date_tags{ EXIF_TAG_DATE_TIME_ORIGINAL, EXIF_TAG_DATE_TIME_DIGITIZED, EXIF_TAG_DATE_TIME, }; struct tm timeinfo; bool have_date = false; for (ExifTag tag : date_tags) { ExifEntry *ent = exif_data_get_entry(data, tag); if (ent == nullptr) { continue; } if (strptime((const char*)ent->data, exif_date_template, &timeinfo) != nullptr) { have_date = true; break; } } if (!have_date) { return; } char buf[100]; ExifEntry *ent = exif_data_get_entry(data, EXIF_TAG_TIME_ZONE_OFFSET); if (ent) { timeinfo.tm_gmtoff = (int)exif_get_sshort(ent->data, order) * 3600; if (strftime(buf, sizeof(buf), iso8601_date_with_zone_format, &timeinfo) != 0) { mfb.setDate(buf); } } else { /* No time zone info */ if (strftime(buf, sizeof(buf), iso8601_date_format, &timeinfo) != 0) { mfb.setDate(buf); } } } int get_exif_int(ExifEntry *ent, ExifByteOrder order) { switch (ent->format) { case EXIF_FORMAT_BYTE: return (unsigned char)ent->data[0]; case EXIF_FORMAT_SHORT: return exif_get_short(ent->data, order); case EXIF_FORMAT_LONG: return exif_get_long(ent->data, order); case EXIF_FORMAT_SBYTE: return (signed char)ent->data[0]; case EXIF_FORMAT_SSHORT: return exif_get_sshort(ent->data, order); case EXIF_FORMAT_SLONG: return exif_get_slong(ent->data, order); default: break; } return 0; } void parse_exif_dimensions(ExifData *data, ExifByteOrder order, MediaFileBuilder &mfb) { ExifEntry *w_ent = exif_data_get_entry(data, EXIF_TAG_PIXEL_X_DIMENSION); ExifEntry *h_ent = exif_data_get_entry(data, EXIF_TAG_PIXEL_Y_DIMENSION); ExifEntry *o_ent = exif_data_get_entry(data, EXIF_TAG_ORIENTATION); if (!w_ent || !h_ent) { return; } int width = get_exif_int(w_ent, order); int height = get_exif_int(h_ent, order); // Optionally swap height and width depending on orientation if (o_ent) { int tmp; // exif_data_fix() has ensured this is a short. switch (exif_get_short(o_ent->data, order)) { case 5: // Mirror horizontal and rotate 270 CW case 6: // Rotate 90 CW case 7: // Mirror horizontal and rotate 90 CW case 8: // Rotate 270 CW tmp = width; width = height; height = tmp; break; default: break; } } mfb.setWidth(width); mfb.setHeight(height); } bool rational_to_degrees(ExifEntry *ent, ExifByteOrder order, double *out) { if (ent->format != EXIF_FORMAT_RATIONAL) { return false; } ExifRational r = exif_get_rational(ent->data, order); *out = ((double) r.numerator) / r.denominator; // Minutes if (ent->components >= 2) { r = exif_get_rational(ent->data + exif_format_get_size(EXIF_FORMAT_RATIONAL), order); *out += ((double) r.numerator) / r.denominator / 60; } // Seconds if (ent->components >= 3) { r = exif_get_rational(ent->data + 2 * exif_format_get_size(EXIF_FORMAT_RATIONAL), order); *out += ((double) r.numerator) / r.denominator / 3600; } return true; } void parse_exif_location(ExifData *data, ExifByteOrder order, MediaFileBuilder &mfb) { ExifContent *ifd = data->ifd[EXIF_IFD_GPS]; ExifEntry *lat_ent = exif_content_get_entry(ifd, (ExifTag)EXIF_TAG_GPS_LATITUDE); ExifEntry *latref_ent = exif_content_get_entry(ifd, (ExifTag)EXIF_TAG_GPS_LATITUDE_REF); ExifEntry *long_ent = exif_content_get_entry(ifd, (ExifTag)EXIF_TAG_GPS_LONGITUDE); ExifEntry *longref_ent = exif_content_get_entry(ifd, (ExifTag)EXIF_TAG_GPS_LONGITUDE_REF); if (!lat_ent || !long_ent) { return; } double latitude, longitude; if (!rational_to_degrees(lat_ent, order, &latitude)) { return; } if (!rational_to_degrees(long_ent, order, &longitude)) { return; } if (latref_ent && latref_ent->data[0] == 'S') { latitude = -latitude; } if (longref_ent && longref_ent->data[0] == 'W') { longitude = -longitude; } mfb.setLatitude(latitude); mfb.setLongitude(longitude); } } namespace mediascanner { bool ImageExtractor::extract_exif(const DetectedFile &d, MediaFileBuilder &mfb) { std::unique_ptr loader( exif_loader_new(), exif_loader_unref); exif_loader_write_file(loader.get(), d.filename.c_str()); std::unique_ptr data( exif_loader_get_data(loader.get()), exif_data_unref); loader.reset(); if (!data) { return false; } exif_data_fix(data.get()); ExifByteOrder order = exif_data_get_byte_order(data.get()); parse_exif_date(data.get(), order, mfb); parse_exif_dimensions(data.get(), order, mfb); parse_exif_location(data.get(), order, mfb); return true; } void ImageExtractor::extract_pixbuf(const DetectedFile &d, MediaFileBuilder &builder) { gint width, height; if(!gdk_pixbuf_get_file_info(d.filename.c_str(), &width, &height)) { string msg("Could not determine resolution of "); msg += d.filename; msg += "."; throw runtime_error(msg); } builder.setWidth(width); builder.setHeight(height); if (d.mtime != 0) { auto t = static_cast(d.mtime); char buf[1024]; struct tm ptm; localtime_r(&t, &ptm); if (strftime(buf, sizeof(buf), iso8601_date_format, &ptm) != 0) { builder.setDate(buf); } } } void ImageExtractor::extract(const DetectedFile &d, MediaFileBuilder &builder) { if (!extract_exif(d, builder)) { extract_pixbuf(d, builder); } return; } } mediascanner2-0.115/src/extractor/ImageExtractor.hh000066400000000000000000000025401436755250000223040ustar00rootroot00000000000000/* * Copyright (C) 2013-2014 Canonical, Ltd. * * Authors: * Jussi Pakkanen * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef EXTRACTOR_IMAGEEXTRACTOR_H #define EXTRACTOR_IMAGEEXTRACTOR_H #include namespace mediascanner { class MediaFileBuilder; struct DetectedFile; class ImageExtractor final { public: ImageExtractor() = default; ~ImageExtractor() = default; ImageExtractor(const ImageExtractor&) = delete; ImageExtractor& operator=(ImageExtractor &o) = delete; void extract(const DetectedFile &d, MediaFileBuilder &builder); private: bool extract_exif(const DetectedFile &d, MediaFileBuilder &builder); void extract_pixbuf(const DetectedFile &d, MediaFileBuilder &builder); }; } #endif mediascanner2-0.115/src/extractor/MetadataExtractor.cc000066400000000000000000000147731436755250000230030ustar00rootroot00000000000000/* * Copyright (C) 2013-2014 Canonical, Ltd. * * Authors: * Jussi Pakkanen * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "MetadataExtractor.hh" #include "DetectedFile.hh" #include "dbus-generated.h" #include "dbus-marshal.hh" #include "../mediascanner/MediaFile.hh" #include "../mediascanner/MediaFileBuilder.hh" #include "../mediascanner/internal/utils.hh" #include #include #include #include #include #include #include #include using namespace std; namespace { const char BUS_NAME[] = "com.canonical.MediaScanner2.Extractor"; const char BUS_PATH[] = "/com/canonical/MediaScanner2/Extractor"; // This list was obtained by grepping /usr/share/mime/audio/. std::array blacklist{{"audio/x-iriver-pla", "audio/x-mpegurl", "audio/x-ms-asx", "audio/x-scpls"}}; void validate_against_blacklist(const std::string &filename, const std::string &content_type) { auto result = std::find(blacklist.begin(), blacklist.end(), content_type); if(result != blacklist.end()) { throw runtime_error("File " + filename + " is of blacklisted type " + content_type + "."); } } } namespace mediascanner { struct MetadataExtractorPrivate { std::unique_ptr bus; std::unique_ptr proxy {nullptr, g_object_unref}; MetadataExtractorPrivate(GDBusConnection *bus); void create_proxy(); }; MetadataExtractorPrivate::MetadataExtractorPrivate(GDBusConnection *bus) : bus(reinterpret_cast(g_object_ref(bus)), g_object_unref) { create_proxy(); } void MetadataExtractorPrivate::create_proxy() { GError *error = nullptr; proxy.reset(ms_extractor_proxy_new_sync( bus.get(), static_cast( G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES | G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS | G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START_AT_CONSTRUCTION), BUS_NAME, BUS_PATH, nullptr, &error)); if (not proxy) { string errortxt(error->message); g_error_free(error); string msg = "Failed to create D-Bus proxy: "; msg += errortxt; throw runtime_error(msg); } } MetadataExtractor::MetadataExtractor(GDBusConnection *bus) { p.reset(new MetadataExtractorPrivate(bus)); } MetadataExtractor::~MetadataExtractor() = default; DetectedFile MetadataExtractor::detect(const std::string &filename) { std::unique_ptr file( g_file_new_for_path(filename.c_str()), g_object_unref); if (!file) { throw runtime_error("Could not create file object"); } GError *error = nullptr; std::unique_ptr info( g_file_query_info( file.get(), G_FILE_ATTRIBUTE_TIME_MODIFIED "," G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE "," G_FILE_ATTRIBUTE_ETAG_VALUE, G_FILE_QUERY_INFO_NONE, /* cancellable */ nullptr, &error), g_object_unref); if (!info) { string errortxt(error->message); g_error_free(error); string msg("Query of file info for "); msg += filename; msg += " failed: "; msg += errortxt; throw runtime_error(msg); } uint64_t mtime = g_file_info_get_attribute_uint64( info.get(), G_FILE_ATTRIBUTE_TIME_MODIFIED); string etag(g_file_info_get_etag(info.get())); string content_type(g_file_info_get_attribute_string( info.get(), G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE)); if (content_type.empty()) { throw runtime_error("Could not determine content type."); } validate_against_blacklist(filename, content_type); MediaType type; if (content_type.find("audio/") == 0) { type = AudioMedia; } else if (content_type.find("video/") == 0) { type = VideoMedia; } else if (content_type.find("image/") == 0) { type = ImageMedia; } else { throw runtime_error(string("File ") + filename + " is not audio or video"); } return DetectedFile(filename, etag, content_type, mtime, type); } MediaFile MetadataExtractor::extract(const DetectedFile &d) { fprintf(stderr, "Extracting metadata from %s.\n", d.filename.c_str()); GError *error = nullptr; GVariant *res = nullptr; gboolean success = ms_extractor_call_extract_metadata_sync( p->proxy.get(), d.filename.c_str(), d.etag.c_str(), d.content_type.c_str(), d.mtime, d.type, &res, nullptr, &error); // If we get a synthesised "no reply" error, the server probably // crashed due to a codec bug. We retry the extraction once more // in case the crash was due to bad cleanup from a previous job. if (!success && error->domain == G_DBUS_ERROR && error->code == G_DBUS_ERROR_NO_REPLY) { g_error_free(error); error = nullptr; fprintf(stderr, "No reply from extractor daemon, retrying once.\n"); // Recreate the proxy, since the old one will have bound to // the old instance's unique name. p->create_proxy(); success = ms_extractor_call_extract_metadata_sync( p->proxy.get(), d.filename.c_str(), d.etag.c_str(), d.content_type.c_str(), d.mtime, d.type, &res, nullptr, &error); } if (!success) { string errortxt(error->message); g_error_free(error); string msg = "ExtractMetadata D-Bus call failed: "; msg += errortxt; throw runtime_error(msg); } // Place variant in a unique_ptr so it is guaranteed to be unrefed. std::unique_ptr result( res, g_variant_unref); return media_from_variant(result.get()); } MediaFile MetadataExtractor::fallback_extract(const DetectedFile &d) { return MediaFileBuilder(d.filename).setType(d.type); } } mediascanner2-0.115/src/extractor/MetadataExtractor.hh000066400000000000000000000030531436755250000230020ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef METADATAEXTRACTOR_H #define METADATAEXTRACTOR_H #include #include #include #include "../mediascanner/scannercore.hh" typedef struct _GDBusConnection GDBusConnection; namespace mediascanner { class MediaFile; struct DetectedFile; struct MetadataExtractorPrivate; class MetadataExtractor final { public: explicit MetadataExtractor(GDBusConnection *bus); ~MetadataExtractor(); MetadataExtractor(const MetadataExtractor&) = delete; MetadataExtractor& operator=(MetadataExtractor &o) = delete; DetectedFile detect(const std::string &filename); MediaFile extract(const DetectedFile &d); // In case the detected file is know to crash gstreamer, // use this to generate fallback data. MediaFile fallback_extract(const DetectedFile &d); private: std::unique_ptr p; }; } #endif mediascanner2-0.115/src/extractor/TaglibExtractor.cc000066400000000000000000000224071436755250000224560ustar00rootroot00000000000000/* * Copyright (C) 2016 Canonical, Ltd. * * Authors: * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "TaglibExtractor.hh" #include "DetectedFile.hh" #include "../mediascanner/MediaFile.hh" #include "../mediascanner/MediaFileBuilder.hh" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; using mediascanner::MediaFileBuilder; namespace { string get_content_type(const string &filename) { unique_ptr file( g_file_new_for_path(filename.c_str()), g_object_unref); assert(file); GError *error = nullptr; unique_ptr info( g_file_query_info( file.get(), G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, G_FILE_QUERY_INFO_NONE, nullptr, &error), g_object_unref); if (!info) { fprintf(stderr, "Failed to detect content type of '%s': %s\n", filename.c_str(), error->message); g_error_free(error); return string(); } return g_file_info_get_content_type(info.get()); } typedef unique_ptr DatePtr; DatePtr parse_iso_date(const string &date_string) { return DatePtr( gst_date_time_new_from_iso8601_string(date_string.c_str()), gst_date_time_unref); } string format_iso_date(const DatePtr &dt) { if (!dt) { return string(); } char *iso = gst_date_time_to_iso8601_string(dt.get()); if (!iso) { return string(); } string result(iso); g_free(iso); return result; } string check_date(const string &date_string) { // Parse and reserialise the date using GstDateTime to check its // consistency. return format_iso_date(parse_iso_date(date_string)); } void parse_common(const TagLib::File &file, MediaFileBuilder &builder) { TagLib::Tag *tag = file.tag(); const TagLib::AudioProperties *audio_props = file.audioProperties(); if (audio_props) { builder.setDuration(audio_props->length()); } TagLib::String s = tag->title(); if (!s.isEmpty()) { builder.setTitle(s.to8Bit(true)); } s = tag->artist(); if (!s.isEmpty()) { builder.setAuthor(s.to8Bit(true)); } s = tag->album(); if (!s.isEmpty()) { builder.setAlbum(s.to8Bit(true)); } s = tag->genre(); if (!s.isEmpty()) { builder.setGenre(s.to8Bit(true)); } unsigned int year = tag->year(); if (year > 0 && year <= 9999) { DatePtr dt(gst_date_time_new_y(year), gst_date_time_unref); builder.setDate(format_iso_date(dt)); } unsigned int track = tag->track(); if (track != 0) { builder.setTrackNumber(track); } } void parse_xiph_comment(TagLib::Ogg::XiphComment *tag, MediaFileBuilder &builder) { const auto& fields = tag->fieldListMap(); if (!fields["DATE"].isEmpty()) { builder.setDate(check_date(fields["DATE"].front().to8Bit(true))); } if (!fields["ALBUMARTIST"].isEmpty()) { builder.setAlbumArtist(fields["ALBUMARTIST"].front().to8Bit(true)); } if (!fields["DISCNUMBER"].isEmpty()) { builder.setDiscNumber(fields["DISCNUMBER"].front().toInt()); } if (!fields["COVERART"].isEmpty() || !fields["METADATA_BLOCK_PICTURE"].isEmpty()) { builder.setHasThumbnail(true); } #if TAGLIB_MAJOR_VERSION > 1 || (TAGLIB_MAJOR_VERSION == 1 && TAGLIB_MINOR_VERSION >= 11) if (!tag->pictureList().isEmpty()) { builder.setHasThumbnail(true); } #endif } void parse_id3v2(const TagLib::ID3v2::Tag *tag, MediaFileBuilder &builder) { const auto& frames = tag->frameListMap(); if (!frames["TDRC"].isEmpty()) { DatePtr dt = parse_iso_date(frames["TDRC"].front()->toString().to8Bit(true)); // Taglib automatically renames the old TYER frame to TDRC, // but doesn't migrate over the day and month from TDAT. if (!gst_date_time_has_month(dt.get()) && !frames["TDAT"].isEmpty()) { const TagLib::ID3v2::Frame *frame = frames["TDAT"].front(); // TagLib converts TDAT to an "UnknownFrame" type frame, // since it believes it should be ignored. Create a text // frame so we can get at the data. TagLib::String data; auto *unknown = dynamic_cast(frame); if (unknown) { // Text information frames consist of one byte for the // encoding, followed the string data. auto encoding = static_cast(unknown->data()[0]); data = TagLib::String(unknown->data().mid(1), encoding); } else { data = frame->toString(); } int ddmm = data.toInt(); int day = ddmm / 100; int month = ddmm % 100; dt.reset(gst_date_time_new_ymd( gst_date_time_get_year(dt.get()), g_date_valid_month(static_cast(month)) ? month : -1, g_date_valid_day(day) ? day : -1)); } builder.setDate(format_iso_date(dt)); } if (!frames["TPE2"].isEmpty()) { builder.setAlbumArtist(frames["TPE2"].front()->toString().to8Bit(true)); } if (!frames["TPOS"].isEmpty()) { builder.setDiscNumber(frames["TPOS"].front()->toString().toInt()); } if (!frames["APIC"].isEmpty()) { builder.setHasThumbnail(true); } } void parse_mp4(const TagLib::MP4::Tag *tag, MediaFileBuilder &builder) { const auto& items = const_cast(tag)->itemListMap(); if (items.contains("\251day")) { builder.setDate(check_date(items["\251day"].toStringList().front().to8Bit(true))); } if (items.contains("aART")) { builder.setAlbumArtist(items["aART"].toStringList().front().to8Bit(true)); } if (items.contains("disk")) { builder.setDiscNumber(items["disk"].toIntPair().first); } if (items.contains("covr")) { builder.setHasThumbnail(true); } } } namespace mediascanner { bool TaglibExtractor::extract(const DetectedFile &d, MediaFileBuilder &builder) { string content_type = get_content_type(d.filename); if (content_type.empty()) { return false; } unique_ptr fs(new TagLib::FileStream(d.filename.c_str(), true)); if (!fs->isOpen()) { return false; } if (content_type == "audio/x-vorbis+ogg") { TagLib::Ogg::Vorbis::File file(fs.get()); if (!file.isValid()) { return false; } parse_common(file, builder); parse_xiph_comment(file.tag(), builder); } else if (content_type == "audio/x-opus+ogg") { TagLib::Ogg::Opus::File file(fs.get()); if (!file.isValid()) { return false; } parse_common(file, builder); parse_xiph_comment(file.tag(), builder); } else if (content_type == "audio/x-flac+ogg") { TagLib::Ogg::FLAC::File file(fs.get()); if (!file.isValid()) { return false; } parse_common(file, builder); parse_xiph_comment(file.tag(), builder); } else if (content_type == "audio/flac") { TagLib::FLAC::File file(fs.get(), TagLib::ID3v2::FrameFactory::instance()); if (!file.isValid()) { return false; } parse_common(file, builder); if (file.hasID3v2Tag()) { parse_id3v2(file.ID3v2Tag(), builder); } if (file.hasXiphComment()) { parse_xiph_comment(file.xiphComment(), builder); } if (!file.pictureList().isEmpty()) { builder.setHasThumbnail(true); } } else if (content_type == "audio/mpeg") { TagLib::MPEG::File file(fs.get(), TagLib::ID3v2::FrameFactory::instance()); if (!file.isValid()) { return false; } parse_common(file, builder); if (file.hasID3v2Tag()) { parse_id3v2(file.ID3v2Tag(), builder); } } else if (content_type == "audio/mp4") { TagLib::MP4::File file(fs.get()); if (!file.isValid()) { return false; } parse_common(file, builder); parse_mp4(file.tag(), builder); } else { return false; } return true; } } mediascanner2-0.115/src/extractor/TaglibExtractor.hh000066400000000000000000000022221436755250000224610ustar00rootroot00000000000000/* * Copyright (C) 2016 Canonical, Ltd. * * Authors: * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef EXTRACTOR_TAGLIBEXTRACTOR_H #define EXTRACTOR_TAGLIBEXTRACTOR_H #include namespace mediascanner { class MediaFileBuilder; struct DetectedFile; class TaglibExtractor final { public: TaglibExtractor() = default; ~TaglibExtractor() = default; TaglibExtractor(const TaglibExtractor&) = delete; TaglibExtractor& operator=(TaglibExtractor &o) = delete; bool extract(const DetectedFile &d, MediaFileBuilder &builder); }; } #endif mediascanner2-0.115/src/extractor/com.canonical.MediaScanner2.Extractor.service.in000066400000000000000000000002101436755250000301140ustar00rootroot00000000000000[D-BUS Service] Name=com.canonical.MediaScanner2.Extractor Exec=@CMAKE_INSTALL_FULL_LIBEXECDIR@/mediascanner-2.0/mediascanner-extractor mediascanner2-0.115/src/extractor/dbus-interface.xml000066400000000000000000000007351436755250000224660ustar00rootroot00000000000000 mediascanner2-0.115/src/extractor/dbus-marshal.cc000066400000000000000000000075421436755250000217450ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "dbus-marshal.hh" #include #include #include namespace mediascanner { GVariant *media_to_variant(const MediaFile &media) { GVariantBuilder builder; g_variant_builder_init(&builder, G_VARIANT_TYPE("(sssssssssiiiiiddbti)")); g_variant_builder_add(&builder, "s", media.getFileName().c_str()); g_variant_builder_add(&builder, "s", media.getContentType().c_str()); g_variant_builder_add(&builder, "s", media.getETag().c_str()); g_variant_builder_add(&builder, "s", media.getTitle().c_str()); g_variant_builder_add(&builder, "s", media.getAuthor().c_str()); g_variant_builder_add(&builder, "s", media.getAlbum().c_str()); g_variant_builder_add(&builder, "s", media.getAlbumArtist().c_str()); g_variant_builder_add(&builder, "s", media.getDate().c_str()); g_variant_builder_add(&builder, "s", media.getGenre().c_str()); g_variant_builder_add(&builder, "i", media.getDiscNumber()); g_variant_builder_add(&builder, "i", media.getTrackNumber()); g_variant_builder_add(&builder, "i", media.getDuration()); g_variant_builder_add(&builder, "i", media.getWidth()); g_variant_builder_add(&builder, "i", media.getHeight()); g_variant_builder_add(&builder, "d", media.getLatitude()); g_variant_builder_add(&builder, "d", media.getLongitude()); g_variant_builder_add(&builder, "b", media.getHasThumbnail()); g_variant_builder_add(&builder, "t", media.getModificationTime()); g_variant_builder_add(&builder, "i", static_cast(media.getType())); return g_variant_builder_end(&builder); } MediaFile media_from_variant(GVariant *variant) { if (!g_variant_is_of_type(variant, G_VARIANT_TYPE("(sssssssssiiiiiddbti)"))) { throw std::runtime_error("variant is of wrong type"); } const char *filename = nullptr, *content_type = nullptr, *etag = nullptr, *title = nullptr, *author = nullptr, *album = nullptr, *album_artist = nullptr, *date = nullptr, *genre = nullptr; gint32 disc_number = 0, track_number = 0, duration = 0, width = 0, height = 0, type = 0; double latitude = 0, longitude = 0; gboolean has_thumbnail = false; uint64_t mtime = 0; g_variant_get(variant, "(&s&s&s&s&s&s&s&s&siiiiiddbti)", &filename, &content_type, &etag, &title, &author, &album, &album_artist, &date, &genre, &disc_number, &track_number, &duration, &width, &height, &latitude, &longitude, &has_thumbnail, &mtime, &type, nullptr); return MediaFileBuilder(filename) .setContentType(content_type) .setETag(etag) .setTitle(title) .setAuthor(author) .setAlbum(album) .setAlbumArtist(album_artist) .setDate(date) .setGenre(genre) .setDiscNumber(disc_number) .setTrackNumber(track_number) .setDuration(duration) .setWidth(width) .setHeight(height) .setLatitude(latitude) .setLongitude(longitude) .setHasThumbnail(has_thumbnail) .setModificationTime(mtime) .setType(static_cast(type)); } } mediascanner2-0.115/src/extractor/dbus-marshal.hh000066400000000000000000000016631436755250000217550ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef EXTRACTOR_DBUS_MARSHAL_HH #define EXTRACTOR_DBUS_MARSHAL_HH #include namespace mediascanner { class MediaFile; GVariant *media_to_variant(const MediaFile &media); MediaFile media_from_variant(GVariant *variant); } #endif mediascanner2-0.115/src/extractor/main.cc000066400000000000000000000152111436755250000202770ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include "DetectedFile.hh" #include "ExtractorBackend.hh" #include "dbus-generated.h" #include "dbus-marshal.hh" using namespace mediascanner; namespace { const char BUS_NAME[] = "com.canonical.MediaScanner2.Extractor"; const char BUS_PATH[] = "/com/canonical/MediaScanner2/Extractor"; const char EXTRACT_ERROR[] = "com.canonical.MediaScanner2.Error.Failed"; const int DELAY = 30; class ExtractorDaemon final { public: ExtractorDaemon(); ~ExtractorDaemon(); void run(); private: void setupBus(); void extract(const DetectedFile &file, GDBusMethodInvocation *invocation); void startExitTimer(); void cancelExitTimer(); static void busNameLostCallback(GDBusConnection *, const char *name, gpointer data); static gboolean handleExtractMetadata(MSExtractor *iface, GDBusMethodInvocation *invocation, const char *filename, const char *etag, const char *content_type, guint64 mtime, gint32 type, gpointer user_data); static gboolean handleExitTimer(gpointer user_data); ExtractorBackend extractor; std::unique_ptr main_loop; std::unique_ptr session_bus; unsigned int bus_name_id = 0; std::unique_ptr iface; unsigned int handler_id = 0; unsigned int timeout_id = 0; int crash_after = -1; }; } ExtractorDaemon::ExtractorDaemon() : main_loop(g_main_loop_new(nullptr, FALSE), g_main_loop_unref), session_bus(nullptr, g_object_unref), iface(ms_extractor_skeleton_new(), g_object_unref) { const char *crash_after_env = getenv("MEDIASCANNER_EXTRACTOR_CRASH_AFTER"); if (crash_after_env) { crash_after = std::stoi(crash_after_env); } setupBus(); } ExtractorDaemon::~ExtractorDaemon() { if (bus_name_id != 0) { g_bus_unown_name(bus_name_id); } g_dbus_interface_skeleton_unexport( G_DBUS_INTERFACE_SKELETON(iface.get())); if (handler_id != 0) { g_signal_handler_disconnect(iface.get(), handler_id); } cancelExitTimer(); } void ExtractorDaemon::setupBus() { GError *error = nullptr; session_bus.reset(g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error)); if (!session_bus) { std::string errortxt(error->message); g_error_free(error); throw std::runtime_error( std::string("Failed to connect to session bus: ") + errortxt); } handler_id = g_signal_connect( iface.get(), "handle-extract-metadata", G_CALLBACK(&ExtractorDaemon::handleExtractMetadata), this); if (!g_dbus_interface_skeleton_export( G_DBUS_INTERFACE_SKELETON(iface.get()), session_bus.get(), BUS_PATH, &error)) { std::string errortxt(error->message); g_error_free(error); throw std::runtime_error( std::string("Failed to export object: ") + errortxt); } bus_name_id = g_bus_own_name_on_connection( session_bus.get(), BUS_NAME, static_cast( G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | G_BUS_NAME_OWNER_FLAGS_REPLACE), nullptr, &ExtractorDaemon::busNameLostCallback, this, nullptr); } void ExtractorDaemon::busNameLostCallback(GDBusConnection *, const char *name, gpointer data) { ExtractorDaemon *daemon = reinterpret_cast(data); fprintf(stderr, "Exiting due to loss of control of bus name %s\n", name); daemon->bus_name_id = 0; g_main_loop_quit(daemon->main_loop.get()); } gboolean ExtractorDaemon::handleExtractMetadata(MSExtractor *, GDBusMethodInvocation *invocation, const char *filename, const char *etag, const char *content_type, guint64 mtime, gint32 type, gpointer user_data) { auto d = reinterpret_cast(user_data); // If the environment variable was set, crash the service after // the requested number of extractions. if (d->crash_after == 0) { abort(); } else if (d->crash_after > 0) { d->crash_after--; } DetectedFile file(filename, etag, content_type, mtime, static_cast(type)); d->extract(file, invocation); return TRUE; } void ExtractorDaemon::extract(const DetectedFile &file, GDBusMethodInvocation *invocation) { cancelExitTimer(); try { MediaFile media = extractor.extract(file); ms_extractor_complete_extract_metadata( iface.get(), invocation, media_to_variant(media)); } catch (const std::exception &e) { g_dbus_method_invocation_return_dbus_error( invocation, EXTRACT_ERROR, e.what()); } startExitTimer(); } void ExtractorDaemon::startExitTimer() { cancelExitTimer(); timeout_id = g_timeout_add_seconds( DELAY, &ExtractorDaemon::handleExitTimer, this); } void ExtractorDaemon::cancelExitTimer() { if (timeout_id != 0) { g_source_remove(timeout_id); timeout_id = 0; } } gboolean ExtractorDaemon::handleExitTimer(gpointer user_data) { auto d = reinterpret_cast(user_data); fprintf(stderr, "Exiting due to inactivity\n"); g_main_loop_quit(d->main_loop.get()); d->timeout_id = 0; return G_SOURCE_REMOVE; } void ExtractorDaemon::run() { g_main_loop_run(main_loop.get()); } int main(int argc, char **argv) { gst_init(&argc, &argv); try { ExtractorDaemon d; d.run(); } catch (const std::exception &e) { fprintf(stderr, "Error: %s\n", e.what()); return 1; } return 0; } mediascanner2-0.115/src/mediascanner/000077500000000000000000000000001436755250000174625ustar00rootroot00000000000000mediascanner2-0.115/src/mediascanner/Album.cc000066400000000000000000000101451436755250000210320ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "Album.hh" #include "internal/FolderArtCache.hh" #include "internal/utils.hh" using namespace std; namespace mediascanner { struct Album::Private { string title; string artist; string date; string genre; string filename; bool has_thumbnail; int artist_count; Private() {} Private(const string &title, const string &artist, const string &date, const string &genre, const string &filename, bool has_thumbnail, int artist_count) : title(title), artist(artist), date(date), genre(genre), filename(filename), has_thumbnail(has_thumbnail), artist_count(artist_count) {} Private(const Private &other) { *this = other; } }; Album::Album() : p(new Private){ } Album::Album(const std::string &title, const std::string &artist) : Album(title, artist, "", "", "", false, 1) { } Album::Album(const std::string &title, const std::string &artist, const std::string &date, const std::string &genre, const std::string &filename) : Album(title, artist, date, genre, filename, !filename.empty(), 1) { } Album::Album(const std::string &title, const std::string &artist, const std::string &date, const std::string &genre, const std::string &filename, bool has_thumbnail) : p(new Private(title, artist, date, genre, filename, has_thumbnail, 1)) { } Album::Album(const std::string &title, const std::string &artist, const std::string &date, const std::string &genre, const std::string &filename, bool has_thumbnail, int artist_count) : p(new Private(title, artist, date, genre, filename, has_thumbnail, artist_count)) { } Album::Album(const Album &other) : p(new Private(*other.p)) { } Album::Album(Album &&other) : p(nullptr) { *this = std::move(other); } Album::~Album() { delete p; } Album &Album::operator=(const Album &other) { *p = *other.p; return *this; } Album &Album::operator=(Album &&other) { if (this != &other) { delete p; p = other.p; other.p = nullptr; } return *this; } const std::string& Album::getTitle() const noexcept { return p->title; } const std::string& Album::getArtist() const noexcept { return p->artist; } const std::string& Album::getDate() const noexcept { return p->date; } const std::string& Album::getGenre() const noexcept { return p->genre; } const std::string& Album::getArtFile() const noexcept { return p->filename; } bool Album::getHasThumbnail() const noexcept { return p->has_thumbnail; } int Album::getArtistCount() const noexcept { return p->artist_count; } std::string Album::getArtUri() const { if (p->has_thumbnail) { return make_thumbnail_uri(getUri(p->filename)); } else { auto standalone = FolderArtCache::get().get_art_for_file(p->filename); if (!standalone.empty()) { return make_thumbnail_uri(getUri(standalone)); } return make_album_art_uri(p->artist, p->title); } } bool Album::operator==(const Album &other) const { return p->title == other.p->title && p->artist == other.p->artist && p->date == other.p->date && p->genre == other.p->genre && p->filename == other.p->filename && p->has_thumbnail == other.p->has_thumbnail; } bool Album::operator!=(const Album &other) const { return !(*this == other); } } mediascanner2-0.115/src/mediascanner/Album.hh000066400000000000000000000041271436755250000210470ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef ALBUM_HH #define ALBUM_HH #include namespace mediascanner { class Album final { public: Album(); Album(const std::string &title, const std::string &artist); Album(const std::string &title, const std::string &artist, const std::string &date, const std::string &genre, const std::string &filename); Album(const std::string &title, const std::string &artist, const std::string &date, const std::string &genre, const std::string &filename, bool has_thumbnail); Album(const std::string &title, const std::string &artist, const std::string &date, const std::string &genre, const std::string &filename, bool has_thumbnail, int artist_count); Album(const Album &other); Album(Album &&other); ~Album(); Album &operator=(const Album &other); Album &operator=(Album &&other); const std::string& getTitle() const noexcept; const std::string& getArtist() const noexcept; const std::string& getDate() const noexcept; const std::string& getGenre() const noexcept; const std::string& getArtFile() const noexcept; bool getHasThumbnail() const noexcept; int getArtistCount() const noexcept; std::string getArtUri() const; bool operator==(const Album &other) const; bool operator!=(const Album &other) const; private: struct Private; Private *p; }; } #endif mediascanner2-0.115/src/mediascanner/CMakeLists.txt000066400000000000000000000020131436755250000222160ustar00rootroot00000000000000add_library(mediascanner SHARED MediaFile.cc MediaFileBuilder.cc MediaFilePrivate.cc Filter.cc Album.cc MediaStore.cc MediaStoreBase.cc FolderArtCache.cc utils.cc mozilla/fts3_porter.c mozilla/Normalize.c ) set(symbol_map "${CMAKE_CURRENT_SOURCE_DIR}/mediascanner-2.0.map") set_target_properties(mediascanner PROPERTIES LINK_FLAGS "${ldflags} -Wl,--version-script,${symbol_map} ") set_target_properties(mediascanner PROPERTIES LINK_DEPENDS ${symbol_map}) add_definitions(${MEDIASCANNER_DEPS_CFLAGS}) target_link_libraries(mediascanner ${MEDIASCANNER_DEPS_LDFLAGS}) set_target_properties(mediascanner PROPERTIES OUTPUT_NAME "mediascanner-2.0" SOVERSION ${MEDIASCANNER_SOVERSION} VERSION ${MEDIASCANNER_LIBVERSION} ) install( TARGETS mediascanner LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ) install(FILES Album.hh Filter.hh MediaFile.hh MediaFileBuilder.hh MediaStore.hh MediaStoreBase.hh scannercore.hh DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/mediascanner-2.0/mediascanner" ) mediascanner2-0.115/src/mediascanner/Filter.cc000066400000000000000000000103701436755250000212170ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "Filter.hh" using std::string; namespace mediascanner { struct Filter::Private { string artist; string album; string album_artist; string genre; int offset = 0; int limit = -1; MediaOrder order = MediaOrder::Default; bool reverse = false; bool have_artist = false; bool have_album = false; bool have_album_artist = false; bool have_genre = false; Private() {} }; Filter::Filter() : p(new Private) { } Filter::Filter(const Filter &other) : Filter() { *p = *other.p; } Filter::Filter(Filter &&other) : p(nullptr) { *this = std::move(other); } Filter::~Filter() { delete p; } bool Filter::operator==(const Filter &other) const { return p->have_artist == other.p->have_artist && p->have_album == other.p->have_album && p->have_album_artist == other.p->have_album_artist && p->have_genre == other.p->have_genre && p->artist == other.p->artist && p->album == other.p->album && p->album_artist == other.p->album_artist && p->genre == other.p->genre && p->offset == other.p->offset && p->limit == other.p->limit && p->order == other.p->order && p->reverse == other.p->reverse; } bool Filter::operator!=(const Filter &other) const { return !(*this == other); } Filter &Filter::operator=(const Filter &other) { *p = *other.p; return *this; } Filter &Filter::operator=(Filter &&other) { if (this != &other) { delete p; p = other.p; other.p = nullptr; } return *this; } void Filter::clear() { unsetArtist(); unsetAlbum(); unsetAlbumArtist(); unsetGenre(); p->offset = 0; p->limit = -1; p->order = MediaOrder::Default; p->reverse = false; } void Filter::setArtist(const std::string &artist) { p->artist = artist; p->have_artist = true; } void Filter::unsetArtist() { p->artist = ""; p->have_artist = false; } bool Filter::hasArtist() const { return p->have_artist; } const std::string &Filter::getArtist() const { return p->artist; } void Filter::setAlbum(const std::string &album) { p->album = album; p->have_album = true; } void Filter::unsetAlbum() { p->album = ""; p->have_album = false; } bool Filter::hasAlbum() const { return p->have_album; } const std::string &Filter::getAlbum() const { return p->album; } void Filter::setAlbumArtist(const std::string &album_artist) { p->album_artist = album_artist; p->have_album_artist = true; } void Filter::unsetAlbumArtist() { p->album_artist = ""; p->have_album_artist = false; } bool Filter::hasAlbumArtist() const { return p->have_album_artist; } const std::string &Filter::getAlbumArtist() const { return p->album_artist; } void Filter::setGenre(const std::string &genre) { p->genre = genre; p->have_genre = true; } void Filter::unsetGenre() { p->genre = ""; p->have_genre = false; } bool Filter::hasGenre() const { return p->have_genre; } const std::string &Filter::getGenre() const { return p->genre; } void Filter::setOffset(int offset) { p->offset = offset; } int Filter::getOffset() const { return p->offset; } void Filter::setLimit(int limit) { p->limit = limit; } int Filter::getLimit() const { return p->limit; } void Filter::setOrder(MediaOrder order) { p->order = order; } MediaOrder Filter::getOrder() const { return p->order; } void Filter::setReverse(bool reverse) { p->reverse = reverse; } bool Filter::getReverse() const { return p->reverse; } } mediascanner2-0.115/src/mediascanner/Filter.hh000066400000000000000000000040051436755250000212270ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef MEDIAFILTER_H_ #define MEDIAFILTER_H_ #include #include "scannercore.hh" namespace mediascanner { class Filter final { public: Filter(); Filter(const Filter &other); Filter(Filter &&other); ~Filter(); Filter &operator=(const Filter &other); Filter &operator=(Filter &&other); bool operator==(const Filter &other) const; bool operator!=(const Filter &other) const; void clear(); void setArtist(const std::string &artist); void unsetArtist(); bool hasArtist() const; const std::string &getArtist() const; void setAlbum(const std::string &album); void unsetAlbum(); bool hasAlbum() const; const std::string &getAlbum() const; void setAlbumArtist(const std::string &album_artist); void unsetAlbumArtist(); bool hasAlbumArtist() const; const std::string &getAlbumArtist() const; void setGenre(const std::string &genre); void unsetGenre(); bool hasGenre() const; const std::string &getGenre() const; void setOffset(int offset); int getOffset() const; void setLimit(int limit); int getLimit() const; void setOrder(MediaOrder order); MediaOrder getOrder() const; void setReverse(bool reverse); bool getReverse() const; private: struct Private; Private *p; }; } #endif mediascanner2-0.115/src/mediascanner/FolderArtCache.cc000066400000000000000000000105621436755250000226030ustar00rootroot00000000000000/* * Copyright (C) 2015 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "internal/FolderArtCache.hh" #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; namespace { const int CACHE_SIZE = 50; string detect_albumart(string directory) { static const array art_basenames = { "cover", "album", "albumart", ".folder", "folder", }; static const array art_extensions = { "jpeg", "jpg", "png", }; if (!directory.empty() && directory[directory.size()-1] != '/') { directory += "/"; } string detected; int best_score = 0; for (const auto& entry: filesystem::directory_iterator(directory)) { const auto filename = entry.path().filename().string(); auto dot = filename.rfind('.'); // Ignore files with no extension if (dot == string::npos) { continue; } auto basename = filename.substr(0, dot); auto extension = filename.substr(dot+1); // Does the file name match one of the required names when // converted to lower case? transform(basename.begin(), basename.end(), basename.begin(), ::tolower); transform(extension.begin(), extension.end(), extension.begin(), ::tolower); auto base_pos = find(art_basenames.begin(), art_basenames.end(), basename); if (base_pos == art_basenames.end()) { continue; } auto ext_pos = find(art_extensions.begin(), art_extensions.end(), extension); if (ext_pos == art_extensions.end()) { continue; } int score = (base_pos - art_basenames.begin()) * art_basenames.size() + (ext_pos - art_extensions.begin()); if (detected.empty() || score < best_score) { detected = filename; best_score = score; } } if (detected.empty()) { return detected; } return directory + detected; } } namespace mediascanner { FolderArtCache::FolderArtCache() = default; FolderArtCache::~FolderArtCache() = default; // Get a singleton instance of the cache FolderArtCache& FolderArtCache::get() { static FolderArtCache cache; return cache; } string FolderArtCache::get_art_for_directory(const string &directory) { struct stat s; if (lstat(directory.c_str(), &s) < 0) { return ""; } if (!S_ISDIR(s.st_mode)) { return ""; } FolderArtInfo info; bool update = false; try { info = cache_.at(directory); } catch (const out_of_range &) { // Fall back to checking the previous iteration of the cache try { info = old_cache_.at(directory); update = true; } catch (const out_of_range &) { } } if (info.dir_mtime.tv_sec != s.st_mtim.tv_sec || info.dir_mtime.tv_nsec != s.st_mtim.tv_nsec) { info.art = detect_albumart(directory); info.dir_mtime = s.st_mtim; update = true; } if (update) { cache_[directory] = info; // Start new cache generation if we've exceeded the size. if (cache_.size() > CACHE_SIZE) { old_cache_ = move(cache_); cache_.clear(); } } return info.art; } string FolderArtCache::get_art_for_file(const string &filename) { auto slash = filename.rfind('/'); if (slash == string::npos) { return ""; } auto directory = filename.substr(0, slash + 1); return get_art_for_directory(directory); } } mediascanner2-0.115/src/mediascanner/MediaFile.cc000066400000000000000000000103321436755250000216070ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "MediaFile.hh" #include "MediaFileBuilder.hh" #include "internal/MediaFilePrivate.hh" #include "internal/FolderArtCache.hh" #include "internal/utils.hh" #include using namespace std; namespace mediascanner { MediaFile::MediaFile() : p(new MediaFilePrivate) { } MediaFile::MediaFile(const MediaFile &other) : p(new MediaFilePrivate(*other.p)) { } MediaFile::MediaFile(MediaFile &&other) : p(nullptr) { *this = std::move(other); } MediaFile::MediaFile(const MediaFileBuilder &builder) { if(!builder.p) { throw logic_error("Tried to construct a Mediafile with an empty MediaFileBuilder."); } p = new MediaFilePrivate(*builder.p); p->setFallbackMetadata(); } MediaFile::MediaFile(MediaFileBuilder &&builder) { if(!builder.p) { throw logic_error("Tried to construct a Mediafile with an empty MediaFileBuilder."); } p = builder.p; builder.p = nullptr; p->setFallbackMetadata(); } MediaFile::~MediaFile() { delete p; } MediaFile &MediaFile::operator=(const MediaFile &other) { *p = *other.p; return *this; } MediaFile &MediaFile::operator=(MediaFile &&other) { if (this != &other) { delete p; p = other.p; other.p = nullptr; } return *this; } const std::string& MediaFile::getFileName() const noexcept { return p->filename; } const std::string& MediaFile::getContentType() const noexcept { return p->content_type; } const std::string& MediaFile::getETag() const noexcept { return p->etag; } const std::string& MediaFile::getTitle() const noexcept { return p->title; } const std::string& MediaFile::getAuthor() const noexcept { return p->author; } const std::string& MediaFile::getAlbum() const noexcept { return p->album; } const std::string& MediaFile::getAlbumArtist() const noexcept { return p->album_artist; } const std::string& MediaFile::getDate() const noexcept { return p->date; } const std::string& MediaFile::getGenre() const noexcept { return p->genre; } int MediaFile::getDiscNumber() const noexcept { return p->disc_number; } int MediaFile::getTrackNumber() const noexcept { return p->track_number; } int MediaFile::getDuration() const noexcept { return p->duration; } int MediaFile::getWidth() const noexcept { return p->width; } int MediaFile::getHeight() const noexcept { return p->height; } double MediaFile::getLatitude() const noexcept { return p->latitude; } double MediaFile::getLongitude() const noexcept { return p->longitude; } bool MediaFile::getHasThumbnail() const noexcept { return p->has_thumbnail; } uint64_t MediaFile::getModificationTime() const noexcept { return p->modification_time; } MediaType MediaFile::getType() const noexcept { return p->type; } std::string MediaFile::getUri() const { return mediascanner::getUri(p->filename); } std::string MediaFile::getArtUri() const { switch (p->type) { case AudioMedia: { if (p->has_thumbnail) { return make_thumbnail_uri(getUri()); } auto standalone = FolderArtCache::get().get_art_for_file(p->filename); if(!standalone.empty()) { return make_thumbnail_uri(mediascanner::getUri(standalone)); } return make_album_art_uri(getAuthor(), getAlbum()); } default: return make_thumbnail_uri(getUri()); } } bool MediaFile::operator==(const MediaFile &other) const { return *p == *other.p; } bool MediaFile::operator!=(const MediaFile &other) const { return !(*this == other); } } mediascanner2-0.115/src/mediascanner/MediaFile.hh000066400000000000000000000046441436755250000216320ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef MEDIAFILE_HH #define MEDIAFILE_HH #include "scannercore.hh" #include #include namespace mediascanner { class MediaFileBuilder; struct MediaFilePrivate; class MediaFile final { friend class MediaFileBuilder; public: MediaFile(); MediaFile(const MediaFile &other); MediaFile(MediaFile &&other); MediaFile(const MediaFileBuilder &builder); MediaFile(MediaFileBuilder &&builder); ~MediaFile(); const std::string& getFileName() const noexcept; const std::string& getContentType() const noexcept; const std::string& getETag() const noexcept; const std::string& getTitle() const noexcept; const std::string& getAuthor() const noexcept; const std::string& getAlbum() const noexcept; const std::string& getAlbumArtist() const noexcept; const std::string& getDate() const noexcept; const std::string& getGenre() const noexcept; std::string getUri() const; std::string getArtUri() const; int getDiscNumber() const noexcept; int getTrackNumber() const noexcept; int getDuration() const noexcept; int getWidth() const noexcept; int getHeight() const noexcept; double getLatitude() const noexcept; double getLongitude() const noexcept; bool getHasThumbnail() const noexcept; uint64_t getModificationTime() const noexcept; MediaType getType() const noexcept; bool operator==(const MediaFile &other) const; bool operator!=(const MediaFile &other) const; MediaFile &operator=(const MediaFile &other); MediaFile &operator=(MediaFile &&other); // There are no setters. MediaFiles are immutable. // For piecewise construction use MediaFileBuilder. private: MediaFilePrivate *p; }; } #endif mediascanner2-0.115/src/mediascanner/MediaFileBuilder.cc000066400000000000000000000060251436755250000231220ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include"MediaFileBuilder.hh" #include"MediaFile.hh" #include"internal/MediaFilePrivate.hh" namespace mediascanner { MediaFileBuilder::MediaFileBuilder(const std::string &fname) : p(new MediaFilePrivate(fname)) { } MediaFileBuilder::MediaFileBuilder(const MediaFile &mf) : p(new MediaFilePrivate(*mf.p)) { } MediaFileBuilder::~MediaFileBuilder() { delete p; } MediaFile MediaFileBuilder::build() const { return MediaFile(*this); } MediaFileBuilder &MediaFileBuilder::setType(MediaType t) { p->type = t; return *this; } MediaFileBuilder &MediaFileBuilder::setETag(const std::string &e) { p->etag = e; return *this; } MediaFileBuilder &MediaFileBuilder::setContentType(const std::string &c) { p->content_type = c; return *this; } MediaFileBuilder &MediaFileBuilder::setTitle(const std::string &t) { p->title = t; return *this; } MediaFileBuilder &MediaFileBuilder::setDate(const std::string &d) { p->date = d; return *this; } MediaFileBuilder &MediaFileBuilder::setAuthor(const std::string &a) { p->author = a; return *this; } MediaFileBuilder &MediaFileBuilder::setAlbum(const std::string &a) { p->album = a; return *this; } MediaFileBuilder &MediaFileBuilder::setAlbumArtist(const std::string &a) { p->album_artist = a; return *this; } MediaFileBuilder &MediaFileBuilder::setGenre(const std::string &g) { p->genre = g; return *this; } MediaFileBuilder &MediaFileBuilder::setDiscNumber(int n) { p->disc_number = n; return *this; } MediaFileBuilder &MediaFileBuilder::setTrackNumber(int n) { p->track_number = n; return *this; } MediaFileBuilder &MediaFileBuilder::setDuration(int n) { p->duration = n; return *this; } MediaFileBuilder &MediaFileBuilder::setWidth(int w) { p->width = w; return *this; } MediaFileBuilder &MediaFileBuilder::setHeight(int h) { p->height = h; return *this; } MediaFileBuilder &MediaFileBuilder::setLatitude(double l) { p->latitude = l; return *this; } MediaFileBuilder &MediaFileBuilder::setLongitude(double l) { p->longitude = l; return *this; } MediaFileBuilder & MediaFileBuilder::setHasThumbnail(bool t) { p->has_thumbnail = t; return *this; } MediaFileBuilder & MediaFileBuilder::setModificationTime(uint64_t t) { p->modification_time = t; return *this; } } mediascanner2-0.115/src/mediascanner/MediaFileBuilder.hh000066400000000000000000000047071436755250000231410ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef MEDIAFILEBUILDER_H_ #define MEDIAFILEBUILDER_H_ #include"scannercore.hh" #include #include namespace mediascanner { class MediaFile; struct MediaFilePrivate; /** * This is a helper class to build MediaFiles. Since we want MediaFiles * to be immutable and always valid, the user needs to always list * all variables in the constructor. This is cumbersome so this class * allows you to gather them one by one and then finally construct * a fully valid MediaFile. */ class MediaFileBuilder final { friend class MediaFile; public: explicit MediaFileBuilder(const std::string &filename); MediaFileBuilder(const MediaFile &mf); MediaFileBuilder(const MediaFileBuilder &) = delete; MediaFileBuilder& operator=(MediaFileBuilder &) = delete; ~MediaFileBuilder(); MediaFile build() const; MediaFileBuilder &setType(MediaType t); MediaFileBuilder &setETag(const std::string &e); MediaFileBuilder &setContentType(const std::string &c); MediaFileBuilder &setTitle(const std::string &t); MediaFileBuilder &setDate(const std::string &d); MediaFileBuilder &setAuthor(const std::string &a); MediaFileBuilder &setAlbum(const std::string &a); MediaFileBuilder &setAlbumArtist(const std::string &a); MediaFileBuilder &setGenre(const std::string &g); MediaFileBuilder &setDiscNumber(int n); MediaFileBuilder &setTrackNumber(int n); MediaFileBuilder &setDuration(int d); MediaFileBuilder &setWidth(int w); MediaFileBuilder &setHeight(int h); MediaFileBuilder &setLatitude(double l); MediaFileBuilder &setLongitude(double l); MediaFileBuilder &setHasThumbnail(bool t); MediaFileBuilder &setModificationTime(uint64_t t); private: MediaFilePrivate *p; }; } #endif mediascanner2-0.115/src/mediascanner/MediaFilePrivate.cc000066400000000000000000000043021436755250000231420ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * Jussi Pakkanen * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "scannercore.hh" #include "internal/MediaFilePrivate.hh" #include "internal/utils.hh" namespace mediascanner { MediaFilePrivate::MediaFilePrivate() = default; MediaFilePrivate::MediaFilePrivate(const std::string &filename) : filename(filename) { } MediaFilePrivate::MediaFilePrivate(const MediaFilePrivate &other) { *this = other; } bool MediaFilePrivate::operator==(const MediaFilePrivate &other) const { return filename == other.filename && content_type == other.content_type && etag == other.etag && title == other.title && author == other.author && album == other.album && album_artist == other.album_artist && date == other.date && genre == other.genre && disc_number == other.disc_number && track_number == other.track_number && duration == other.duration && width == other.width && height == other.height && latitude == other.latitude && longitude == other.longitude && has_thumbnail == other.has_thumbnail && modification_time == other.modification_time && type == other.type; } bool MediaFilePrivate::operator!=(const MediaFilePrivate &other) const { return !(*this == other); } void MediaFilePrivate::setFallbackMetadata() { if (title.empty()) { title = filenameToTitle(filename); } if (album_artist.empty()) { album_artist = author; } } } mediascanner2-0.115/src/mediascanner/MediaStore.cc000066400000000000000000001106451436755250000220340ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "MediaStore.hh" #include #include #include #include #include #include #include #include #include #include #include #include "mozilla/fts3_tokenizer.h" #include "MediaFile.hh" #include "MediaFileBuilder.hh" #include "Album.hh" #include "Filter.hh" #include "internal/sqliteutils.hh" #include "internal/utils.hh" using namespace std; namespace mediascanner { // Increment this whenever changing db schema. // It will cause dbstore to rebuild its tables. static const int schemaVersion = 10; struct MediaStorePrivate { sqlite3 *db; // https://www.sqlite.org/cvstrac/wiki?p=DatabaseIsLocked // http://sqlite.com/faq.html#q6 std::mutex dbMutex; void insert(const MediaFile &m) const; void remove(const std::string &fname) const; void insert_broken_file(const std::string &fname, const std::string &etag) const; void remove_broken_file(const std::string &fname) const; bool is_broken_file(const std::string &fname, const std::string &etag) const; MediaFile lookup(const std::string &filename) const; std::vector query(const std::string &q, MediaType type, const Filter &filter) const; std::vector queryAlbums(const std::string &core_term, const Filter &filter) const; std::vector queryArtists(const std::string &q, const Filter &filter) const; std::vector getAlbumSongs(const Album& album) const; std::string getETag(const std::string &filename) const; std::vector listSongs(const Filter &filter) const; std::vector listAlbums(const Filter &filter) const; std::vector listArtists(const Filter &filter) const; std::vector listAlbumArtists(const Filter &filter) const; std::vector listGenres(const Filter &filter) const; bool hasMedia(MediaType type) const; size_t size() const; void pruneDeleted(); void archiveItems(const std::string &prefix); void restoreItems(const std::string &prefix); void removeSubtree(const std::string &directory); void begin(); void commit(); void rollback(); }; extern "C" void sqlite3Fts3PorterTokenizerModule( sqlite3_tokenizer_module const**ppModule); static void register_tokenizer(sqlite3 *db) { Statement query(db, "SELECT fts3_tokenizer(?, ?)"); query.bind(1, "mozporter"); const sqlite3_tokenizer_module *p = nullptr; sqlite3Fts3PorterTokenizerModule(&p); query.bind(2, &p, sizeof(p)); query.step(); } /* ranking function adapted from http://sqlite.org/fts3.html#appendix_a */ static void rankfunc(sqlite3_context *pCtx, int nVal, sqlite3_value **apVal) { const int32_t *aMatchinfo; /* Return value of matchinfo() */ int32_t nCol; /* Number of columns in the table */ int32_t nPhrase; /* Number of phrases in the query */ int32_t iPhrase; /* Current phrase */ double score = 0.0; /* Value to return */ /* Check that the number of arguments passed to this function is correct. ** If not, jump to wrong_number_args. Set aMatchinfo to point to the array ** of unsigned integer values returned by FTS function matchinfo. Set ** nPhrase to contain the number of reportable phrases in the users full-text ** query, and nCol to the number of columns in the table. */ if( nVal<1 ) goto wrong_number_args; aMatchinfo = static_cast(sqlite3_value_blob(apVal[0])); nPhrase = aMatchinfo[0]; nCol = aMatchinfo[1]; if( nVal!=(1+nCol) ) goto wrong_number_args; /* Iterate through each phrase in the users query. */ for(iPhrase=0; iPhrase / ) * ** ** aPhraseinfo[] points to the start of the data for phrase iPhrase. So ** the hit count and global hit counts for each column are found in ** aPhraseinfo[iCol*3] and aPhraseinfo[iCol*3+1], respectively. */ const int32_t *aPhraseinfo = &aMatchinfo[2 + iPhrase*nCol*3]; for(iCol=0; iCol0 ){ score += ((double)nHitCount / (double)nGlobalHitCount) * weight; } } } sqlite3_result_double(pCtx, score); return; /* Jump here if the wrong number of arguments are passed to this function */ wrong_number_args: sqlite3_result_error(pCtx, "wrong number of arguments to function rank()", -1); } struct FirstContext { int type; union { int i; double f; struct { void *blob; int length; } b; } data; }; static void first_step(sqlite3_context *ctx, int /*argc*/, sqlite3_value **argv) { FirstContext *d = static_cast(sqlite3_aggregate_context(ctx, sizeof(FirstContext))); if (d->type != 0) { return; } sqlite3_value *arg = argv[0]; d->type = sqlite3_value_type(arg); switch (d->type) { case SQLITE_INTEGER: d->data.i = sqlite3_value_int(arg); break; case SQLITE_FLOAT: d->data.f = sqlite3_value_double(arg); break; case SQLITE_NULL: break; case SQLITE_TEXT: d->data.b.length = sqlite3_value_bytes(arg); d->data.b.blob = malloc(d->data.b.length); memcpy(d->data.b.blob, sqlite3_value_text(arg), d->data.b.length); break; case SQLITE_BLOB: d->data.b.length = sqlite3_value_bytes(arg); d->data.b.blob = malloc(d->data.b.length); memcpy(d->data.b.blob, sqlite3_value_blob(arg), d->data.b.length); break; default: sqlite3_result_error(ctx, "Unhandled data type", -1); sqlite3_result_error_code(ctx, SQLITE_MISMATCH); } } static void first_finalize(sqlite3_context *ctx) { FirstContext *d = static_cast(sqlite3_aggregate_context(ctx, 0)); if (d == nullptr) { sqlite3_result_null(ctx); return; } switch (d->type) { case SQLITE_INTEGER: sqlite3_result_int(ctx, d->data.i); break; case SQLITE_FLOAT: sqlite3_result_double(ctx, d->data.f); break; case SQLITE_NULL: sqlite3_result_null(ctx); break; case SQLITE_TEXT: sqlite3_result_text(ctx, reinterpret_cast(d->data.b.blob), d->data.b.length, free); d->data.b.blob = nullptr; break; case SQLITE_BLOB: sqlite3_result_blob(ctx, d->data.b.blob, d->data.b.length, free); d->data.b.blob = nullptr; break; default: sqlite3_result_error(ctx, "Unhandled data type", -1); sqlite3_result_error_code(ctx, SQLITE_MISMATCH); } } static bool has_block_in_path(std::map &cache, const std::string &filename) { std::vector path_segments; std::istringstream f(filename); std::string s; while (std::getline(f, s, '/')) { path_segments.push_back(s); } path_segments.pop_back(); std::string trial_path; for(const auto &seg : path_segments) { if(trial_path != "/") trial_path += "/"; trial_path += seg; auto r = cache.find(trial_path); if(r != cache.end() && r->second) { return true; } if(has_scanblock(trial_path)) { cache[trial_path] = true; return true; } else { cache[trial_path] = false; } } return false; } static void register_functions(sqlite3 *db) { if (sqlite3_create_function(db, "rank", -1, SQLITE_ANY, nullptr, rankfunc, nullptr, nullptr) != SQLITE_OK) { throw runtime_error(sqlite3_errmsg(db)); } if (sqlite3_create_function(db, "first", 1, SQLITE_ANY, nullptr, nullptr, first_step, first_finalize) != SQLITE_OK) { throw runtime_error(sqlite3_errmsg(db)); } } static void execute_sql(sqlite3 *db, const string &cmd) { char *errmsg = nullptr; if(sqlite3_exec(db, cmd.c_str(), nullptr, nullptr, &errmsg) != SQLITE_OK) { throw runtime_error(errmsg); } } static int getSchemaVersion(sqlite3 *db) { int version = -1; try { Statement select(db, "SELECT version FROM schemaVersion"); if (select.step()) version = select.getInt(0); } catch (const exception &e) { /* schemaVersion table might not exist */ } return version; } void deleteTables(sqlite3 *db) { string deleteCmd(R"( DROP TABLE IF EXISTS media; DROP TABLE IF EXISTS media_fts; DROP TABLE IF EXISTS media_attic; DROP TABLE IF EXISTS schemaVersion; DROP TABLE IF EXISTS broken_files; )"); execute_sql(db, deleteCmd); } void createTables(sqlite3 *db) { string schema(R"( CREATE TABLE schemaVersion (version INTEGER); CREATE TABLE media ( id INTEGER PRIMARY KEY, filename TEXT UNIQUE NOT NULL CHECK (filename LIKE '/%'), content_type TEXT, etag TEXT, title TEXT, date TEXT, artist TEXT, -- Only relevant to audio album TEXT, -- Only relevant to audio album_artist TEXT, -- Only relevant to audio genre TEXT, -- Only relevant to audio disc_number INTEGER, -- Only relevant to audio track_number INTEGER, -- Only relevant to audio duration INTEGER, width INTEGER, -- Only relevant to video/images height INTEGER, -- Only relevant to video/images latitude DOUBLE, longitude DOUBLE, has_thumbnail INTEGER CHECK (has_thumbnail IN (0, 1)), mtime INTEGER, type INTEGER CHECK (type IN (1, 2, 3)) -- MediaType enum ); CREATE INDEX media_type_idx ON media(type); CREATE INDEX media_song_info_idx ON media(type, album_artist, album, disc_number, track_number, title) WHERE type = 1; CREATE INDEX media_artist_idx ON media(type, artist) WHERE type = 1; CREATE INDEX media_genre_idx ON media(type, genre) WHERE type = 1; CREATE INDEX media_mtime_idx ON media(type, mtime); CREATE TABLE media_attic ( filename TEXT UNIQUE NOT NULL, content_type TEXT, etag TEXT, title TEXT, date TEXT, artist TEXT, -- Only relevant to audio album TEXT, -- Only relevant to audio album_artist TEXT, -- Only relevant to audio genre TEXT, -- Only relevant to audio disc_number INTEGER, -- Only relevant to audio track_number INTEGER, -- Only relevant to audio duration INTEGER, width INTEGER, -- Only relevant to video/images height INTEGER, -- Only relevant to video/images latitude DOUBLE, longitude DOUBLE, has_thumbnail INTEGER, mtime INTEGER, type INTEGER -- 0=Audio, 1=Video ); CREATE VIRTUAL TABLE media_fts USING fts4(content='media', title, artist, album, tokenize=mozporter); CREATE TRIGGER media_bu BEFORE UPDATE ON media BEGIN DELETE FROM media_fts WHERE docid=old.id; END; CREATE TRIGGER media_au AFTER UPDATE ON media BEGIN INSERT INTO media_fts(docid, title, artist, album) VALUES (new.id, new.title, new.artist, new.album); END; CREATE TRIGGER media_bd BEFORE DELETE ON media BEGIN DELETE FROM media_fts WHERE docid=old.id; END; CREATE TRIGGER media_ai AFTER INSERT ON media BEGIN INSERT INTO media_fts(docid, title, artist, album) VALUES (new.id, new.title, new.artist, new.album); END; CREATE TABLE broken_files ( filename TEXT PRIMARY KEY NOT NULL, etag TEXT NOT NULL ); )"); execute_sql(db, schema); Statement version(db, "INSERT INTO schemaVersion (version) VALUES (?)"); version.bind(1, schemaVersion); version.step(); } static std::string get_default_database() { std::string cachedir; char *env_cachedir = getenv("MEDIASCANNER_CACHEDIR"); if (env_cachedir) { cachedir = env_cachedir; } else { cachedir = g_get_user_cache_dir(); cachedir += "/mediascanner-2.0"; } if (g_mkdir_with_parents(cachedir.c_str(), S_IRWXU) < 0) { std::string msg("Could not create cache dir: "); msg += strerror(errno); throw runtime_error(msg); } return cachedir + "/mediastore.db"; } MediaStore::MediaStore(OpenType access, const std::string &retireprefix) : MediaStore(get_default_database(), access, retireprefix) { } MediaStore::MediaStore(const std::string &filename, OpenType access, const std::string &retireprefix) { int sqliteFlags = access == MS_READ_WRITE ? SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE : SQLITE_OPEN_READONLY; p = new MediaStorePrivate(); if(sqlite3_open_v2(filename.c_str(), &p->db, sqliteFlags, nullptr) != SQLITE_OK) { throw runtime_error(sqlite3_errmsg(p->db)); } register_tokenizer(p->db); register_functions(p->db); int detectedSchemaVersion = getSchemaVersion(p->db); if(access == MS_READ_WRITE) { if(detectedSchemaVersion != schemaVersion) { deleteTables(p->db); createTables(p->db); } if(!retireprefix.empty()) archiveItems(retireprefix); } else { if(detectedSchemaVersion != schemaVersion) { std::string msg("Tried to open a db with schema version "); msg += std::to_string(detectedSchemaVersion); msg += ", while supported version is "; msg += std::to_string(schemaVersion) + "."; throw runtime_error(msg); } } } MediaStore::~MediaStore() { sqlite3_close(p->db); delete p; } size_t MediaStorePrivate::size() const { Statement count(db, "SELECT COUNT(*) FROM media"); count.step(); return count.getInt(0); } void MediaStorePrivate::insert(const MediaFile &m) const { Statement query(db, "INSERT OR REPLACE INTO media (filename, content_type, etag, title, date, artist, album, album_artist, genre, disc_number, track_number, duration, width, height, latitude, longitude, has_thumbnail, mtime, type) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); query.bind(1, m.getFileName()); query.bind(2, m.getContentType()); query.bind(3, m.getETag()); query.bind(4, m.getTitle()); query.bind(5, m.getDate()); query.bind(6, m.getAuthor()); query.bind(7, m.getAlbum()); query.bind(8, m.getAlbumArtist()); query.bind(9, m.getGenre()); query.bind(10, m.getDiscNumber()); query.bind(11, m.getTrackNumber()); query.bind(12, m.getDuration()); query.bind(13, m.getWidth()); query.bind(14, m.getHeight()); query.bind(15, m.getLatitude()); query.bind(16, m.getLongitude()); query.bind(17, (int)m.getHasThumbnail()); query.bind(18, (int64_t)m.getModificationTime()); query.bind(19, (int)m.getType()); query.step(); const char *typestr = m.getType() == AudioMedia ? "song" : "video"; printf("Added %s to backing store: %s\n", typestr, m.getFileName().c_str()); printf(" author : %s\n", m.getAuthor().c_str()); printf(" title : %s\n", m.getTitle().c_str()); printf(" album : %s\n", m.getAlbum().c_str()); printf(" duration : %d\n", m.getDuration()); // Not atomic with the addition above but very unlikely to crash between the two. // Even if it does, only one residual line remains and that will be cleaned up // on the next scan. remove_broken_file(m.getFileName()); } void MediaStorePrivate::remove(const string &fname) const { Statement del(db, "DELETE FROM media WHERE filename = ?"); del.bind(1, fname); del.step(); } void MediaStorePrivate::insert_broken_file(const std::string &fname, const std::string &etag) const { Statement del(db, "INSERT OR REPLACE INTO broken_files (filename, etag) VALUES (?, ?)"); del.bind(1, fname); del.bind(2, etag); del.step(); } void MediaStorePrivate::remove_broken_file(const std::string &fname) const { Statement del(db, "DELETE FROM broken_files WHERE filename = ?"); del.bind(1, fname); del.step(); } bool MediaStorePrivate::is_broken_file(const std::string &fname, const std::string &etag) const { Statement query(db, "SELECT * FROM broken_files WHERE filename = ? AND etag = ?"); query.bind(1, fname); query.bind(2, etag); return query.step(); } static MediaFile make_media(Statement &query) { return MediaFileBuilder(query.getText(0)) .setContentType(query.getText(1)) .setETag(query.getText(2)) .setTitle(query.getText(3)) .setDate(query.getText(4)) .setAuthor(query.getText(5)) .setAlbum(query.getText(6)) .setAlbumArtist(query.getText(7)) .setGenre(query.getText(8)) .setDiscNumber(query.getInt(9)) .setTrackNumber(query.getInt(10)) .setDuration(query.getInt(11)) .setWidth(query.getInt(12)) .setHeight(query.getInt(13)) .setLatitude(query.getDouble(14)) .setLongitude(query.getDouble(15)) .setHasThumbnail(query.getInt(16)) .setModificationTime(query.getInt64(17)) .setType((MediaType)query.getInt(18)); } static vector collect_media(Statement &query) { vector result; while (query.step()) { result.push_back(make_media(query)); } return result; } MediaFile MediaStorePrivate::lookup(const std::string &filename) const { Statement query(db, R"( SELECT filename, content_type, etag, title, date, artist, album, album_artist, genre, disc_number, track_number, duration, width, height, latitude, longitude, has_thumbnail, mtime, type FROM media WHERE filename = ? )"); query.bind(1, filename); if (!query.step()) { throw runtime_error("Could not find media " + filename); } return make_media(query); } vector MediaStorePrivate::query(const std::string &core_term, MediaType type, const Filter &filter) const { string qs(R"( SELECT filename, content_type, etag, title, date, artist, album, album_artist, genre, disc_number, track_number, duration, width, height, latitude, longitude, has_thumbnail, mtime, type FROM media )"); if (!core_term.empty()) { qs += R"( JOIN ( SELECT docid, rank(matchinfo(media_fts), 1.0, 0.5, 0.75) AS rank FROM media_fts WHERE media_fts MATCH ? ) AS ranktable ON (media.id = ranktable.docid) )"; } qs += " WHERE type = ?"; switch (filter.getOrder()) { case MediaOrder::Default: case MediaOrder::Rank: // We can only sort by rank if there was a query term if (!core_term.empty()) { qs += " ORDER BY ranktable.rank"; if (!filter.getReverse()) { // Normal order is descending qs += " DESC"; } } break; case MediaOrder::Title: qs += " ORDER BY title"; if (filter.getReverse()) { qs += " DESC"; } break; case MediaOrder::Date: qs += " ORDER BY date"; if (filter.getReverse()) { qs += " DESC"; } break; case MediaOrder::Modified: qs += " ORDER BY mtime"; if (filter.getReverse()) { qs += " DESC"; } break; } qs += " LIMIT ? OFFSET ?"; Statement query(db, qs.c_str()); int param = 1; if (!core_term.empty()) { query.bind(param++, core_term + "*"); } query.bind(param++, (int)type); query.bind(param++, filter.getLimit()); query.bind(param++, filter.getOffset()); return collect_media(query); } static Album make_album(Statement &query) { const string album = query.getText(0); const string album_artist = query.getText(1); const string date = query.getText(2); const string genre = query.getText(3); const string filename = query.getText(4); const bool has_thumbnail = query.getInt(5); const int artist_count = query.getInt(6); return Album(album, album_artist, date, genre, filename, has_thumbnail, artist_count); } static vector collect_albums(Statement &query) { vector result; while (query.step()) { result.push_back(make_album(query)); } return result; } vector MediaStorePrivate::queryAlbums(const std::string &core_term, const Filter &filter) const { string qs(R"( SELECT album, album_artist, first(date) as date, first(genre) as genre, first(filename) as filename, first(has_thumbnail) as has_thumbnail, count(distinct album_artist) as artist_count, first(mtime) as mtime FROM media WHERE type = ? AND album <> '' )"); if (!core_term.empty()) { qs += " AND id IN (SELECT docid FROM media_fts WHERE media_fts MATCH ?)"; } qs += " GROUP BY album"; switch (filter.getOrder()) { case MediaOrder::Default: case MediaOrder::Title: qs += " ORDER BY album"; if (filter.getReverse()) { qs += " DESC"; } break; case MediaOrder::Rank: throw std::runtime_error("Can not query albums by rank"); case MediaOrder::Date: throw std::runtime_error("Can not query albums by date"); case MediaOrder::Modified: qs += " ORDER BY mtime"; if (filter.getReverse()) { qs += " DESC"; } break; } qs += " LIMIT ? OFFSET ?"; Statement query(db, qs.c_str()); int param = 1; query.bind(param++, (int)AudioMedia); if (!core_term.empty()) { query.bind(param++, core_term + "*"); } query.bind(param++, filter.getLimit()); query.bind(param++, filter.getOffset()); return collect_albums(query); } vector MediaStorePrivate::queryArtists(const string &q, const Filter &filter) const { string qs(R"( SELECT artist FROM media WHERE type = ? AND artist <> '' )"); if (!q.empty()) { qs += "AND id IN (SELECT docid FROM media_fts WHERE media_fts MATCH ?)"; } qs += " GROUP BY artist"; switch (filter.getOrder()) { case MediaOrder::Default: case MediaOrder::Title: qs += " ORDER BY artist"; if (filter.getReverse()) { qs += " DESC"; } break; case MediaOrder::Rank: throw std::runtime_error("Can not query artists by rank"); case MediaOrder::Date: throw std::runtime_error("Can not query artists by date"); case MediaOrder::Modified: throw std::runtime_error("Can not query artists by modification date"); } qs += " LIMIT ? OFFSET ?"; Statement query(db, qs.c_str()); int param = 1; query.bind(param++, (int)AudioMedia); if (!q.empty()) { query.bind(param++, q + "*"); } query.bind(param++, filter.getLimit()); query.bind(param++, filter.getOffset()); vector result; while (query.step()) { result.push_back(query.getText(0)); } return result; } vector MediaStorePrivate::getAlbumSongs(const Album& album) const { Statement query(db, R"( SELECT filename, content_type, etag, title, date, artist, album, album_artist, genre, disc_number, track_number, duration, width, height, latitude, longitude, has_thumbnail, mtime, type FROM media WHERE album = ? AND album_artist = ? AND type = ? ORDER BY disc_number, track_number )"); query.bind(1, album.getTitle()); query.bind(2, album.getArtist()); query.bind(3, (int)AudioMedia); return collect_media(query); } std::string MediaStorePrivate::getETag(const std::string &filename) const { Statement query(db, R"( SELECT etag FROM media WHERE filename = ? )"); query.bind(1, filename); if (query.step()) { return query.getText(0); } else { return ""; } } std::vector MediaStorePrivate::listSongs(const Filter &filter) const { std::string qs(R"( SELECT filename, content_type, etag, title, date, artist, album, album_artist, genre, disc_number, track_number, duration, width, height, latitude, longitude, has_thumbnail, mtime, type FROM media WHERE type = ? )"); if (filter.hasArtist()) { qs += " AND artist = ?"; } if (filter.hasAlbum()) { qs += " AND album = ?"; } if (filter.hasAlbumArtist()) { qs += " AND album_artist = ?"; } if (filter.hasGenre()) { qs += " AND genre = ?"; } qs += R"( ORDER BY album_artist, album, disc_number, track_number, title LIMIT ? OFFSET ? )"; Statement query(db, qs.c_str()); int param = 1; query.bind(param++, (int)AudioMedia); if (filter.hasArtist()) { query.bind(param++, filter.getArtist()); } if (filter.hasAlbum()) { query.bind(param++, filter.getAlbum()); } if (filter.hasAlbumArtist()) { query.bind(param++, filter.getAlbumArtist()); } if (filter.hasGenre()) { query.bind(param++, filter.getGenre()); } query.bind(param++, filter.getLimit()); query.bind(param++, filter.getOffset()); return collect_media(query); } std::vector MediaStorePrivate::listAlbums(const Filter &filter) const { std::string qs(R"( SELECT album, album_artist, first(date) as date, first(genre) as genre, first(filename) as filename, first(has_thumbnail) as has_thumbnail, count(distinct album_artist) as artist_count FROM media WHERE type = ? )"); if (filter.hasArtist()) { qs += " AND artist = ?"; } if (filter.hasAlbumArtist()) { qs += " AND album_artist = ?"; } if (filter.hasGenre()) { qs += "AND genre = ?"; } qs += R"( GROUP BY album ORDER BY album LIMIT ? OFFSET ? )"; Statement query(db, qs.c_str()); int param = 1; query.bind(param++, (int)AudioMedia); if (filter.hasArtist()) { query.bind(param++, filter.getArtist()); } if (filter.hasAlbumArtist()) { query.bind(param++, filter.getAlbumArtist()); } if (filter.hasGenre()) { query.bind(param++, filter.getGenre()); } query.bind(param++, filter.getLimit()); query.bind(param++, filter.getOffset()); return collect_albums(query); } vector MediaStorePrivate::listArtists(const Filter &filter) const { string qs(R"( SELECT artist FROM media WHERE type = ? )"); if (filter.hasGenre()) { qs += " AND genre = ?"; } qs += R"( GROUP BY artist ORDER BY artist LIMIT ? OFFSET ? )"; Statement query(db, qs.c_str()); int param = 1; query.bind(param++, (int)AudioMedia); if (filter.hasGenre()) { query.bind(param++, filter.getGenre()); } query.bind(param++, filter.getLimit()); query.bind(param++, filter.getOffset()); vector artists; while (query.step()) { artists.push_back(query.getText(0)); } return artists; } vector MediaStorePrivate::listAlbumArtists(const Filter &filter) const { string qs(R"( SELECT album_artist FROM media WHERE type = ? )"); if (filter.hasGenre()) { qs += " AND genre = ?"; } qs += R"( GROUP BY album_artist ORDER BY album_artist LIMIT ? OFFSET ? )"; Statement query(db, qs.c_str()); int param = 1; query.bind(param++, (int)AudioMedia); if (filter.hasGenre()) { query.bind(param++, filter.getGenre()); } query.bind(param++, filter.getLimit()); query.bind(param++, filter.getOffset()); vector artists; while (query.step()) { artists.push_back(query.getText(0)); } return artists; } vector MediaStorePrivate::listGenres(const Filter &filter) const { Statement query(db, R"( SELECT genre FROM media WHERE type = ? GROUP BY genre ORDER BY genre LIMIT ? OFFSET ? )"); query.bind(1, (int)AudioMedia); query.bind(2, filter.getLimit()); query.bind(3, filter.getOffset()); vector genres; while (query.step()) { genres.push_back(query.getText(0)); } return genres; } bool MediaStorePrivate::hasMedia(MediaType type) const { if (type == AllMedia) { Statement query(db, R"( SELECT id FROM media LIMIT 1 )"); return query.step(); } else { Statement query(db, R"( SELECT id FROM media WHERE type = ? LIMIT 1 )"); query.bind(1, (int)type); return query.step(); } } void MediaStorePrivate::pruneDeleted() { std::map path_cache; vector deleted; Statement query(db, "SELECT filename FROM media"); while (query.step()) { const string filename = query.getText(0); if (access(filename.c_str(), F_OK) != 0 || has_block_in_path(path_cache, filename)) { deleted.push_back(filename); continue; } } query.finalize(); printf("%d files deleted from disk or in scanblocked directories.\n", (int)deleted.size()); for(const auto &i : deleted) { remove(i); } } void MediaStorePrivate::archiveItems(const std::string &prefix) { const char *templ = R"(BEGIN TRANSACTION; INSERT INTO media_attic (filename, content_type, etag, title, date, artist, album, album_artist, genre, disc_number, track_number, duration, width, height, latitude, longitude, has_thumbnail, mtime, type) SELECT filename, content_type, etag, title, date, artist, album, album_artist, genre, disc_number, track_number, duration, width, height, latitude, longitude, has_thumbnail, mtime, type FROM media WHERE filename LIKE %s; DELETE FROM media WHERE filename LIKE %s; COMMIT; )"; string cond = sqlQuote(prefix + "%"); const size_t bufsize = 1024; char cmd[bufsize]; snprintf(cmd, bufsize, templ, cond.c_str(), cond.c_str()); char *errmsg; if(sqlite3_exec(db, cmd, nullptr, nullptr, &errmsg) != SQLITE_OK) { sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); throw runtime_error(errmsg); } } void MediaStorePrivate::restoreItems(const std::string &prefix) { const char *templ = R"(BEGIN TRANSACTION; INSERT INTO media (filename, content_type, etag, title, date, artist, album, album_artist, genre, disc_number, track_number, duration, width, height, latitude, longitude, has_thumbnail, mtime, type) SELECT filename, content_type, etag, title, date, artist, album, album_artist, genre, disc_number, track_number, duration, width, height, latitude, longitude, has_thumbnail, mtime, type FROM media_attic WHERE filename LIKE %s; DELETE FROM media_attic WHERE filename LIKE %s; COMMIT; )"; string cond = sqlQuote(prefix + "%"); const size_t bufsize = 1024; char cmd[bufsize]; snprintf(cmd, bufsize, templ, cond.c_str(), cond.c_str()); char *errmsg; if(sqlite3_exec(db, cmd, nullptr, nullptr, &errmsg) != SQLITE_OK) { sqlite3_exec(db, "ROLLBACK;", nullptr, nullptr, nullptr); throw runtime_error(errmsg); } } void MediaStorePrivate::removeSubtree(const std::string &directory) { string escaped = directory; string::size_type pos = 0; // Escape instances of like expression special characters and the escape character. while (true) { pos = escaped.find_first_of("%_!", pos); if (pos == string::npos) { break; } escaped.replace(pos, 0, "!"); pos += 2; } if (escaped.empty() || escaped[escaped.size() - 1] != '/') { escaped += '/'; } escaped += '%'; Statement query(db, "DELETE FROM media WHERE filename LIKE ? ESCAPE '!'"); query.bind(1, escaped); query.step(); } void MediaStorePrivate::begin() { Statement query(db, "BEGIN TRANSACTION"); query.step(); } void MediaStorePrivate::commit() { Statement query(db, "COMMIT TRANSACTION"); query.step(); } void MediaStorePrivate::rollback() { Statement query(db, "ROLLBACK TRANSACTION"); query.step(); } void MediaStore::insert(const MediaFile &m) const { std::lock_guard lock(p->dbMutex); p->insert(m); } void MediaStore::remove(const std::string &fname) const { std::lock_guard lock(p->dbMutex); p->remove(fname); } void MediaStore::insert_broken_file(const std::string &fname, const std::string &etag) const { std::lock_guard lock(p->dbMutex); p->insert_broken_file(fname, etag); } void MediaStore::remove_broken_file(const std::string &fname) const { std::lock_guard lock(p->dbMutex); p->remove_broken_file(fname); } bool MediaStore::is_broken_file(const std::string &fname, const std::string &etag) const { std::lock_guard lock(p->dbMutex); return p->is_broken_file(fname, etag); } MediaFile MediaStore::lookup(const std::string &filename) const { std::lock_guard lock(p->dbMutex); return p->lookup(filename); } std::vector MediaStore::query(const std::string &q, MediaType type, const Filter &filter) const { std::lock_guard lock(p->dbMutex); return p->query(q, type, filter); } std::vector MediaStore::queryAlbums(const std::string &core_term, const Filter &filter) const { std::lock_guard lock(p->dbMutex); return p->queryAlbums(core_term, filter); } std::vector MediaStore::queryArtists(const std::string &q, const Filter &filter) const { std::lock_guard lock(p->dbMutex); return p->queryArtists(q, filter); } std::vector MediaStore::getAlbumSongs(const Album& album) const { std::lock_guard lock(p->dbMutex); return p->getAlbumSongs(album); } std::string MediaStore::getETag(const std::string &filename) const { std::lock_guard lock(p->dbMutex); return p->getETag(filename); } std::vector MediaStore::listSongs(const Filter &filter) const { std::lock_guard lock(p->dbMutex); return p->listSongs(filter); } std::vector MediaStore::listAlbums(const Filter &filter) const { std::lock_guard lock(p->dbMutex); return p->listAlbums(filter); } std::vector MediaStore::listArtists(const Filter &filter) const { std::lock_guard lock(p->dbMutex); return p->listArtists(filter); } std::vector MediaStore::listAlbumArtists(const Filter &filter) const { std::lock_guard lock(p->dbMutex); return p->listAlbumArtists(filter); } std::vector MediaStore::listGenres(const Filter &filter) const { std::lock_guard lock(p->dbMutex); return p->listGenres(filter); } bool MediaStore::hasMedia(MediaType type) const { std::lock_guard lock(p->dbMutex); return p->hasMedia(type); } size_t MediaStore::size() const { std::lock_guard lock(p->dbMutex); return p->size(); } void MediaStore::pruneDeleted() { std::lock_guard lock(p->dbMutex); p->pruneDeleted(); } void MediaStore::archiveItems(const std::string &prefix) { std::lock_guard lock(p->dbMutex); p->archiveItems(prefix); } void MediaStore::restoreItems(const std::string &prefix) { std::lock_guard lock(p->dbMutex); p->restoreItems(prefix); } void MediaStore::removeSubtree(const std::string &directory) { std::lock_guard lock(p->dbMutex); p->removeSubtree(directory); } MediaStoreTransaction MediaStore::beginTransaction() { std::lock_guard lock(p->dbMutex); p->begin(); return MediaStoreTransaction(p); } MediaStoreTransaction::MediaStoreTransaction(MediaStorePrivate *p) : p(p) { } MediaStoreTransaction::MediaStoreTransaction(MediaStoreTransaction &&other) { *this = std::move(other); } MediaStoreTransaction::~MediaStoreTransaction() { if (!p) { return; } std::lock_guard lock(p->dbMutex); try { p->rollback(); } catch (const std::exception &e) { fprintf(stderr, "MediaStoreTransaction: error rolling back in destructor: %s\n", e.what()); } } MediaStoreTransaction& MediaStoreTransaction::operator=(MediaStoreTransaction &&other) { p = other.p; other.p = nullptr; return *this; } void MediaStoreTransaction::commit() { std::lock_guard lock(p->dbMutex); p->commit(); p->begin(); } } mediascanner2-0.115/src/mediascanner/MediaStore.hh000066400000000000000000000070131436755250000220400ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef MEDIASTORE_HH_ #define MEDIASTORE_HH_ #include "MediaStoreBase.hh" #include #include namespace mediascanner { enum OpenType { MS_READ_ONLY, MS_READ_WRITE }; struct MediaStorePrivate; class MediaStoreTransaction; class MediaStore final : public virtual MediaStoreBase { private: MediaStorePrivate *p; public: MediaStore(OpenType access, const std::string &retireprefix=""); MediaStore(const std::string &filename, OpenType access, const std::string &retireprefix=""); MediaStore(const MediaStore &other) = delete; MediaStore& operator=(const MediaStore &other) = delete; virtual ~MediaStore(); void insert(const MediaFile &m) const; void remove(const std::string &fname) const; // Maintain a list of files known to crash GStreamer // metadata scanner. void insert_broken_file(const std::string &fname, const std::string &etag) const; void remove_broken_file(const std::string &fname) const; bool is_broken_file(const std::string &fname, const std::string &etag) const; virtual MediaFile lookup(const std::string &filename) const override; virtual std::vector query(const std::string &q, MediaType type, const Filter &filter) const override; virtual std::vector queryAlbums(const std::string &core_term, const Filter &filter) const override; virtual std::vector queryArtists(const std::string &q, const Filter &filter) const override; virtual std::vector getAlbumSongs(const Album& album) const override; virtual std::string getETag(const std::string &filename) const override; virtual std::vector listSongs(const Filter &filter) const override; virtual std::vector listAlbums(const Filter &filter) const override; virtual std::vector listArtists(const Filter &filter) const override; virtual std::vectorlistAlbumArtists(const Filter &filter) const override; virtual std::vectorlistGenres(const Filter &filter) const override; virtual bool hasMedia(MediaType type) const override; size_t size() const; void pruneDeleted(); void archiveItems(const std::string &prefix); void restoreItems(const std::string &prefix); void removeSubtree(const std::string &directory); MediaStoreTransaction beginTransaction(); }; class MediaStoreTransaction final { friend MediaStore; public: MediaStoreTransaction(MediaStoreTransaction &&other); ~MediaStoreTransaction(); MediaStoreTransaction(const MediaStoreTransaction &other) = delete; MediaStoreTransaction& operator=(const MediaStoreTransaction &other) = delete; MediaStoreTransaction& operator=(MediaStoreTransaction &&other); void commit(); private: MediaStoreTransaction(MediaStorePrivate *p); MediaStorePrivate *p; }; } #endif mediascanner2-0.115/src/mediascanner/MediaStoreBase.cc000066400000000000000000000015311436755250000226200ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "MediaStoreBase.hh" namespace mediascanner { MediaStoreBase::MediaStoreBase() { } MediaStoreBase::~MediaStoreBase() { } } mediascanner2-0.115/src/mediascanner/MediaStoreBase.hh000066400000000000000000000041251436755250000226340ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef MEDIASTOREBASE_HH_ #define MEDIASTOREBASE_HH_ #include"scannercore.hh" #include #include namespace mediascanner { class MediaFile; class Album; class Filter; class MediaStoreBase { public: MediaStoreBase(); virtual ~MediaStoreBase(); MediaStoreBase(const MediaStoreBase &other) = delete; MediaStoreBase& operator=(const MediaStoreBase &other) = delete; virtual MediaFile lookup(const std::string &filename) const = 0; virtual std::vector query(const std::string &q, MediaType type, const Filter& filter) const = 0; virtual std::vector queryAlbums(const std::string &core_term, const Filter &filter) const = 0; virtual std::vector queryArtists(const std::string &q, const Filter &filter) const = 0; virtual std::vector getAlbumSongs(const Album& album) const = 0; virtual std::string getETag(const std::string &filename) const = 0; virtual std::vector listSongs(const Filter &filter) const = 0; virtual std::vector listAlbums(const Filter &filter) const = 0; virtual std::vector listArtists(const Filter &filter) const = 0; virtual std::vectorlistAlbumArtists(const Filter &filter) const = 0; virtual std::vectorlistGenres(const Filter &filter) const = 0; virtual bool hasMedia(MediaType type) const = 0; }; } #endif mediascanner2-0.115/src/mediascanner/internal/000077500000000000000000000000001436755250000212765ustar00rootroot00000000000000mediascanner2-0.115/src/mediascanner/internal/FolderArtCache.hh000066400000000000000000000027411436755250000244310ustar00rootroot00000000000000/* * Copyright (C) 2015 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef FOLDERARTCACHE_HH #define FOLDERARTCACHE_HH #include #include #include #include namespace mediascanner { struct FolderArtInfo { std::string art; struct timespec dir_mtime {0, 0}; }; class FolderArtCache final { public: FolderArtCache(); ~FolderArtCache(); FolderArtCache(const FolderArtCache &other) = delete; FolderArtCache& operator=(const FolderArtCache &other) = delete; // Get a singleton instance of the cache static FolderArtCache& get(); std::string get_art_for_directory(const std::string &directory); std::string get_art_for_file(const std::string &filename); private: std::map cache_; std::map old_cache_; }; } #endif mediascanner2-0.115/src/mediascanner/internal/MediaFilePrivate.hh000066400000000000000000000034021436755250000247700ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef MEDIAFILEPRIVATE_HH #define MEDIAFILEPRIVATE_HH #include #include namespace mediascanner { struct MediaFilePrivate { std::string filename; std::string content_type; std::string etag; std::string title; std::string date; // ISO date string. Should this be time since epoch? std::string author; std::string album; std::string album_artist; std::string genre; int disc_number = 0; int track_number = 0; int duration = 0; // In seconds. int width = 0; int height = 0; double latitude = 0.0; // In degrees, negative for South double longitude = 0.0; // In degrees, negative for West bool has_thumbnail = false; uint64_t modification_time = 0; MediaType type = UnknownMedia; MediaFilePrivate(); MediaFilePrivate(const std::string &filename); MediaFilePrivate(const MediaFilePrivate &other); bool operator==(const MediaFilePrivate &other) const; bool operator!=(const MediaFilePrivate &other) const; void setFallbackMetadata(); }; } #endif mediascanner2-0.115/src/mediascanner/internal/sqliteutils.hh000066400000000000000000000111151436755250000242000ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef SCAN_SQLITEUTILS_H #define SCAN_SQLITEUTILS_H #include #include #include #include namespace mediascanner { class Statement { public: Statement(sqlite3 *db, const char *sql) { rc = sqlite3_prepare_v2(db, sql, -1, &statement, nullptr); if (rc != SQLITE_OK) { throw std::runtime_error(sqlite3_errmsg(db)); } } ~Statement() { try { finalize(); } catch(const std::exception &e) { fprintf(stderr, "Error finalising statement: %s\n", e.what()); } catch(...) { fprintf(stderr, "Unknown error finalising statement.\n"); } } void bind(int pos, int value) { rc = sqlite3_bind_int(statement, pos, value); if (rc != SQLITE_OK) throw std::runtime_error(sqlite3_errstr(rc)); } void bind(int pos, int64_t value) { rc = sqlite3_bind_int64(statement, pos, value); if (rc != SQLITE_OK) throw std::runtime_error(sqlite3_errstr(rc)); } void bind(int pos, double value) { rc = sqlite3_bind_double(statement, pos, value); if (rc != SQLITE_OK) throw std::runtime_error(sqlite3_errstr(rc)); } void bind(int pos, const std::string &value) { rc = sqlite3_bind_text(statement, pos, value.c_str(), value.size(), SQLITE_TRANSIENT); if (rc != SQLITE_OK) throw std::runtime_error(sqlite3_errstr(rc)); } void bind(int pos, void *blob, int length) { rc = sqlite3_bind_blob(statement, pos, blob, length, SQLITE_STATIC); if (rc != SQLITE_OK) throw std::runtime_error(sqlite3_errstr(rc)); } bool step() { // Sqlite docs list a few cases where you need to to a rollback // if a calling step fails. We don't match those cases but if // we change queries that may start to happen. // https://sqlite.org/c3ref/step.html // // The proper fix would probably be to move to a WAL log, but // it seems to require write access to the mediastore dir // even for readers, which is problematic for confined apps. int retry_count=0; const int max_retries = 100; do { rc = sqlite3_step(statement); retry_count++; } while(rc == SQLITE_BUSY && retry_count < max_retries); switch (rc) { case SQLITE_DONE: return false; case SQLITE_ROW: return true; default: throw std::runtime_error(sqlite3_errstr(rc)); } } std::string getText(int column) { if (rc != SQLITE_ROW) throw std::runtime_error("Statement hasn't been executed, or no more results"); return (const char *)sqlite3_column_text(statement, column); } int getInt(int column) { if (rc != SQLITE_ROW) throw std::runtime_error("Statement hasn't been executed, or no more results"); return sqlite3_column_int(statement, column); } int64_t getInt64(int column) { if (rc != SQLITE_ROW) throw std::runtime_error("Statement hasn't been executed, or no more results"); return sqlite3_column_int64(statement, column); } double getDouble(int column) { if (rc != SQLITE_ROW) throw std::runtime_error("Statement hasn't been executed, or no more results"); return sqlite3_column_double(statement, column); } void finalize() { if (statement != nullptr) { rc = sqlite3_finalize(statement); if (rc != SQLITE_OK) { std::string msg("Could not finalize statement: "); msg += sqlite3_errstr(rc); throw std::runtime_error(msg); } statement = nullptr; } } private: sqlite3_stmt *statement; int rc; }; } #endif mediascanner2-0.115/src/mediascanner/internal/utils.hh000066400000000000000000000023401436755250000227560ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef SCAN_UTILS_H #define SCAN_UTILS_H #include namespace mediascanner { std::string sqlQuote(const std::string &input); std::string filenameToTitle(const std::string &filename); std::string getUri(const std::string &filename); bool is_rootlike(const std::string &path); bool is_optical_disc(const std::string &path); bool has_scanblock(const std::string &path); std::string make_album_art_uri(const std::string &artist, const std::string &album); std::string make_thumbnail_uri(const std::string &uri); } #endif mediascanner2-0.115/src/mediascanner/mediascanner-2.0.map000066400000000000000000000012301436755250000231030ustar00rootroot00000000000000{ global: # We can do this because we only export classes that have public # methods. If they had private ones, we would need to enumerate # what to export. extern "C++" { mediascanner::MediaFile::*; mediascanner::Album::*; mediascanner::MediaFileBuilder::*; mediascanner::MediaStore::*; mediascanner::MediaStoreBase::*; mediascanner::MediaStoreTransaction::*; mediascanner::Filter::*; typeinfo?for?mediascanner::*; typeinfo?name?for?mediascanner::*; VTT?for?mediascanner::*; virtual?thunk?to?mediascanner::*; vtable?for?mediascanner::*; }; local: *; }; mediascanner2-0.115/src/mediascanner/mozilla/000077500000000000000000000000001436755250000211315ustar00rootroot00000000000000mediascanner2-0.115/src/mediascanner/mozilla/Normalize.c000066400000000000000000002567571436755250000232630ustar00rootroot00000000000000/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* THIS FILE IS GENERATED BY generate_table.py. DON'T EDIT THIS */ static const unsigned short gNormalizeTable0040[] = { /* U+0040 */ 0x0040, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, 0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f, 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078, 0x0079, 0x007a, 0x005b, 0x005c, 0x005d, 0x005e, 0x005f, 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, 0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f, 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078, 0x0079, 0x007a, 0x007b, 0x007c, 0x007d, 0x007e, 0x007f, }; static const unsigned short gNormalizeTable0080[] = { /* U+0080 */ 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, 0x0089, 0x008a, 0x008b, 0x008c, 0x008d, 0x008e, 0x008f, 0x0090, 0x0091, 0x0092, 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009a, 0x009b, 0x009c, 0x009d, 0x009e, 0x009f, 0x0020, 0x00a1, 0x00a2, 0x00a3, 0x00a4, 0x00a5, 0x00a6, 0x00a7, 0x0020, 0x00a9, 0x0061, 0x00ab, 0x00ac, 0x0020, 0x00ae, 0x0020, 0x00b0, 0x00b1, 0x0032, 0x0033, 0x0020, 0x03bc, 0x00b6, 0x00b7, 0x0020, 0x0031, 0x006f, 0x00bb, 0x0031, 0x0031, 0x0033, 0x00bf, }; static const unsigned short gNormalizeTable00c0[] = { /* U+00c0 */ 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x00e6, 0x0063, 0x0065, 0x0065, 0x0065, 0x0065, 0x0069, 0x0069, 0x0069, 0x0069, 0x00f0, 0x006e, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x00d7, 0x00f8, 0x0075, 0x0075, 0x0075, 0x0075, 0x0079, 0x00fe, 0x0073, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x00e6, 0x0063, 0x0065, 0x0065, 0x0065, 0x0065, 0x0069, 0x0069, 0x0069, 0x0069, 0x00f0, 0x006e, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x00f7, 0x00f8, 0x0075, 0x0075, 0x0075, 0x0075, 0x0079, 0x00fe, 0x0079, }; static const unsigned short gNormalizeTable0100[] = { /* U+0100 */ 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0063, 0x0063, 0x0063, 0x0063, 0x0063, 0x0063, 0x0063, 0x0063, 0x0064, 0x0064, 0x0111, 0x0111, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0067, 0x0067, 0x0067, 0x0067, 0x0067, 0x0067, 0x0067, 0x0067, 0x0068, 0x0068, 0x0127, 0x0127, 0x0069, 0x0069, 0x0069, 0x0069, 0x0069, 0x0069, 0x0069, 0x0069, 0x0069, 0x0131, 0x0069, 0x0069, 0x006a, 0x006a, 0x006b, 0x006b, 0x0138, 0x006c, 0x006c, 0x006c, 0x006c, 0x006c, 0x006c, 0x006c, }; static const unsigned short gNormalizeTable0140[] = { /* U+0140 */ 0x006c, 0x0142, 0x0142, 0x006e, 0x006e, 0x006e, 0x006e, 0x006e, 0x006e, 0x02bc, 0x014b, 0x014b, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x0153, 0x0153, 0x0072, 0x0072, 0x0072, 0x0072, 0x0072, 0x0072, 0x0073, 0x0073, 0x0073, 0x0073, 0x0073, 0x0073, 0x0073, 0x0073, 0x0074, 0x0074, 0x0074, 0x0074, 0x0167, 0x0167, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0077, 0x0077, 0x0079, 0x0079, 0x0079, 0x007a, 0x007a, 0x007a, 0x007a, 0x007a, 0x007a, 0x0073, }; static const unsigned short gNormalizeTable0180[] = { /* U+0180 */ 0x0180, 0x0253, 0x0183, 0x0183, 0x0185, 0x0185, 0x0254, 0x0188, 0x0188, 0x0256, 0x0257, 0x018c, 0x018c, 0x018d, 0x01dd, 0x0259, 0x025b, 0x0192, 0x0192, 0x0260, 0x0263, 0x0195, 0x0269, 0x0268, 0x0199, 0x0199, 0x019a, 0x019b, 0x026f, 0x0272, 0x019e, 0x0275, 0x006f, 0x006f, 0x01a3, 0x01a3, 0x01a5, 0x01a5, 0x0280, 0x01a8, 0x01a8, 0x0283, 0x01aa, 0x01ab, 0x01ad, 0x01ad, 0x0288, 0x0075, 0x0075, 0x028a, 0x028b, 0x01b4, 0x01b4, 0x01b6, 0x01b6, 0x0292, 0x01b9, 0x01b9, 0x01ba, 0x01bb, 0x01bd, 0x01bd, 0x01be, 0x01bf, }; static const unsigned short gNormalizeTable01c0[] = { /* U+01c0 */ 0x01c0, 0x01c1, 0x01c2, 0x01c3, 0x0064, 0x0064, 0x0064, 0x006c, 0x006c, 0x006c, 0x006e, 0x006e, 0x006e, 0x0061, 0x0061, 0x0069, 0x0069, 0x006f, 0x006f, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x01dd, 0x0061, 0x0061, 0x0061, 0x0061, 0x00e6, 0x00e6, 0x01e5, 0x01e5, 0x0067, 0x0067, 0x006b, 0x006b, 0x006f, 0x006f, 0x006f, 0x006f, 0x0292, 0x0292, 0x006a, 0x0064, 0x0064, 0x0064, 0x0067, 0x0067, 0x0195, 0x01bf, 0x006e, 0x006e, 0x0061, 0x0061, 0x00e6, 0x00e6, 0x00f8, 0x00f8, }; static const unsigned short gNormalizeTable0200[] = { /* U+0200 */ 0x0061, 0x0061, 0x0061, 0x0061, 0x0065, 0x0065, 0x0065, 0x0065, 0x0069, 0x0069, 0x0069, 0x0069, 0x006f, 0x006f, 0x006f, 0x006f, 0x0072, 0x0072, 0x0072, 0x0072, 0x0075, 0x0075, 0x0075, 0x0075, 0x0073, 0x0073, 0x0074, 0x0074, 0x021d, 0x021d, 0x0068, 0x0068, 0x019e, 0x0221, 0x0223, 0x0223, 0x0225, 0x0225, 0x0061, 0x0061, 0x0065, 0x0065, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x0079, 0x0079, 0x0234, 0x0235, 0x0236, 0x0237, 0x0238, 0x0239, 0x2c65, 0x023c, 0x023c, 0x019a, 0x2c66, 0x023f, }; static const unsigned short gNormalizeTable0240[] = { /* U+0240 */ 0x0240, 0x0242, 0x0242, 0x0180, 0x0289, 0x028c, 0x0247, 0x0247, 0x0249, 0x0249, 0x024b, 0x024b, 0x024d, 0x024d, 0x024f, 0x024f, 0x0250, 0x0251, 0x0252, 0x0253, 0x0254, 0x0255, 0x0256, 0x0257, 0x0258, 0x0259, 0x025a, 0x025b, 0x025c, 0x025d, 0x025e, 0x025f, 0x0260, 0x0261, 0x0262, 0x0263, 0x0264, 0x0265, 0x0266, 0x0267, 0x0268, 0x0269, 0x026a, 0x026b, 0x026c, 0x026d, 0x026e, 0x026f, 0x0270, 0x0271, 0x0272, 0x0273, 0x0274, 0x0275, 0x0276, 0x0277, 0x0278, 0x0279, 0x027a, 0x027b, 0x027c, 0x027d, 0x027e, 0x027f, }; static const unsigned short gNormalizeTable0280[] = { /* U+0280 */ 0x0280, 0x0281, 0x0282, 0x0283, 0x0284, 0x0285, 0x0286, 0x0287, 0x0288, 0x0289, 0x028a, 0x028b, 0x028c, 0x028d, 0x028e, 0x028f, 0x0290, 0x0291, 0x0292, 0x0293, 0x0294, 0x0295, 0x0296, 0x0297, 0x0298, 0x0299, 0x029a, 0x029b, 0x029c, 0x029d, 0x029e, 0x029f, 0x02a0, 0x02a1, 0x02a2, 0x02a3, 0x02a4, 0x02a5, 0x02a6, 0x02a7, 0x02a8, 0x02a9, 0x02aa, 0x02ab, 0x02ac, 0x02ad, 0x02ae, 0x02af, 0x0068, 0x0266, 0x006a, 0x0072, 0x0279, 0x027b, 0x0281, 0x0077, 0x0079, 0x02b9, 0x02ba, 0x02bb, 0x02bc, 0x02bd, 0x02be, 0x02bf, }; static const unsigned short gNormalizeTable02c0[] = { /* U+02c0 */ 0x02c0, 0x02c1, 0x02c2, 0x02c3, 0x02c4, 0x02c5, 0x02c6, 0x02c7, 0x02c8, 0x02c9, 0x02ca, 0x02cb, 0x02cc, 0x02cd, 0x02ce, 0x02cf, 0x02d0, 0x02d1, 0x02d2, 0x02d3, 0x02d4, 0x02d5, 0x02d6, 0x02d7, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x02de, 0x02df, 0x0263, 0x006c, 0x0073, 0x0078, 0x0295, 0x02e5, 0x02e6, 0x02e7, 0x02e8, 0x02e9, 0x02ea, 0x02eb, 0x02ec, 0x02ed, 0x02ee, 0x02ef, 0x02f0, 0x02f1, 0x02f2, 0x02f3, 0x02f4, 0x02f5, 0x02f6, 0x02f7, 0x02f8, 0x02f9, 0x02fa, 0x02fb, 0x02fc, 0x02fd, 0x02fe, 0x02ff, }; static const unsigned short gNormalizeTable0340[] = { /* U+0340 */ 0x0300, 0x0301, 0x0342, 0x0313, 0x0308, 0x03b9, 0x0346, 0x0347, 0x0348, 0x0349, 0x034a, 0x034b, 0x034c, 0x034d, 0x034e, 0x0020, 0x0350, 0x0351, 0x0352, 0x0353, 0x0354, 0x0355, 0x0356, 0x0357, 0x0358, 0x0359, 0x035a, 0x035b, 0x035c, 0x035d, 0x035e, 0x035f, 0x0360, 0x0361, 0x0362, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367, 0x0368, 0x0369, 0x036a, 0x036b, 0x036c, 0x036d, 0x036e, 0x036f, 0x0371, 0x0371, 0x0373, 0x0373, 0x02b9, 0x0375, 0x0377, 0x0377, 0x0378, 0x0379, 0x0020, 0x037b, 0x037c, 0x037d, 0x003b, 0x037f, }; static const unsigned short gNormalizeTable0380[] = { /* U+0380 */ 0x0380, 0x0381, 0x0382, 0x0383, 0x0020, 0x0020, 0x03b1, 0x00b7, 0x03b5, 0x03b7, 0x03b9, 0x038b, 0x03bf, 0x038d, 0x03c5, 0x03c9, 0x03b9, 0x03b1, 0x03b2, 0x03b3, 0x03b4, 0x03b5, 0x03b6, 0x03b7, 0x03b8, 0x03b9, 0x03ba, 0x03bb, 0x03bc, 0x03bd, 0x03be, 0x03bf, 0x03c0, 0x03c1, 0x03a2, 0x03c3, 0x03c4, 0x03c5, 0x03c6, 0x03c7, 0x03c8, 0x03c9, 0x03b9, 0x03c5, 0x03b1, 0x03b5, 0x03b7, 0x03b9, 0x03c5, 0x03b1, 0x03b2, 0x03b3, 0x03b4, 0x03b5, 0x03b6, 0x03b7, 0x03b8, 0x03b9, 0x03ba, 0x03bb, 0x03bc, 0x03bd, 0x03be, 0x03bf, }; static const unsigned short gNormalizeTable03c0[] = { /* U+03c0 */ 0x03c0, 0x03c1, 0x03c3, 0x03c3, 0x03c4, 0x03c5, 0x03c6, 0x03c7, 0x03c8, 0x03c9, 0x03b9, 0x03c5, 0x03bf, 0x03c5, 0x03c9, 0x03d7, 0x03b2, 0x03b8, 0x03c5, 0x03c5, 0x03c5, 0x03c6, 0x03c0, 0x03d7, 0x03d9, 0x03d9, 0x03db, 0x03db, 0x03dd, 0x03dd, 0x03df, 0x03df, 0x03e1, 0x03e1, 0x03e3, 0x03e3, 0x03e5, 0x03e5, 0x03e7, 0x03e7, 0x03e9, 0x03e9, 0x03eb, 0x03eb, 0x03ed, 0x03ed, 0x03ef, 0x03ef, 0x03ba, 0x03c1, 0x03c3, 0x03f3, 0x03b8, 0x03b5, 0x03f6, 0x03f8, 0x03f8, 0x03c3, 0x03fb, 0x03fb, 0x03fc, 0x037b, 0x037c, 0x037d, }; static const unsigned short gNormalizeTable0400[] = { /* U+0400 */ 0x0435, 0x0435, 0x0452, 0x0433, 0x0454, 0x0455, 0x0456, 0x0456, 0x0458, 0x0459, 0x045a, 0x045b, 0x043a, 0x0438, 0x0443, 0x045f, 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437, 0x0438, 0x0438, 0x043a, 0x043b, 0x043c, 0x043d, 0x043e, 0x043f, 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, 0x0448, 0x0449, 0x044a, 0x044b, 0x044c, 0x044d, 0x044e, 0x044f, 0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437, 0x0438, 0x0438, 0x043a, 0x043b, 0x043c, 0x043d, 0x043e, 0x043f, }; static const unsigned short gNormalizeTable0440[] = { /* U+0440 */ 0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447, 0x0448, 0x0449, 0x044a, 0x044b, 0x044c, 0x044d, 0x044e, 0x044f, 0x0435, 0x0435, 0x0452, 0x0433, 0x0454, 0x0455, 0x0456, 0x0456, 0x0458, 0x0459, 0x045a, 0x045b, 0x043a, 0x0438, 0x0443, 0x045f, 0x0461, 0x0461, 0x0463, 0x0463, 0x0465, 0x0465, 0x0467, 0x0467, 0x0469, 0x0469, 0x046b, 0x046b, 0x046d, 0x046d, 0x046f, 0x046f, 0x0471, 0x0471, 0x0473, 0x0473, 0x0475, 0x0475, 0x0475, 0x0475, 0x0479, 0x0479, 0x047b, 0x047b, 0x047d, 0x047d, 0x047f, 0x047f, }; static const unsigned short gNormalizeTable0480[] = { /* U+0480 */ 0x0481, 0x0481, 0x0482, 0x0483, 0x0484, 0x0485, 0x0486, 0x0487, 0x0488, 0x0489, 0x048b, 0x048b, 0x048d, 0x048d, 0x048f, 0x048f, 0x0491, 0x0491, 0x0493, 0x0493, 0x0495, 0x0495, 0x0497, 0x0497, 0x0499, 0x0499, 0x049b, 0x049b, 0x049d, 0x049d, 0x049f, 0x049f, 0x04a1, 0x04a1, 0x04a3, 0x04a3, 0x04a5, 0x04a5, 0x04a7, 0x04a7, 0x04a9, 0x04a9, 0x04ab, 0x04ab, 0x04ad, 0x04ad, 0x04af, 0x04af, 0x04b1, 0x04b1, 0x04b3, 0x04b3, 0x04b5, 0x04b5, 0x04b7, 0x04b7, 0x04b9, 0x04b9, 0x04bb, 0x04bb, 0x04bd, 0x04bd, 0x04bf, 0x04bf, }; static const unsigned short gNormalizeTable04c0[] = { /* U+04c0 */ 0x04cf, 0x0436, 0x0436, 0x04c4, 0x04c4, 0x04c6, 0x04c6, 0x04c8, 0x04c8, 0x04ca, 0x04ca, 0x04cc, 0x04cc, 0x04ce, 0x04ce, 0x04cf, 0x0430, 0x0430, 0x0430, 0x0430, 0x04d5, 0x04d5, 0x0435, 0x0435, 0x04d9, 0x04d9, 0x04d9, 0x04d9, 0x0436, 0x0436, 0x0437, 0x0437, 0x04e1, 0x04e1, 0x0438, 0x0438, 0x0438, 0x0438, 0x043e, 0x043e, 0x04e9, 0x04e9, 0x04e9, 0x04e9, 0x044d, 0x044d, 0x0443, 0x0443, 0x0443, 0x0443, 0x0443, 0x0443, 0x0447, 0x0447, 0x04f7, 0x04f7, 0x044b, 0x044b, 0x04fb, 0x04fb, 0x04fd, 0x04fd, 0x04ff, 0x04ff, }; static const unsigned short gNormalizeTable0500[] = { /* U+0500 */ 0x0501, 0x0501, 0x0503, 0x0503, 0x0505, 0x0505, 0x0507, 0x0507, 0x0509, 0x0509, 0x050b, 0x050b, 0x050d, 0x050d, 0x050f, 0x050f, 0x0511, 0x0511, 0x0513, 0x0513, 0x0515, 0x0515, 0x0517, 0x0517, 0x0519, 0x0519, 0x051b, 0x051b, 0x051d, 0x051d, 0x051f, 0x051f, 0x0521, 0x0521, 0x0523, 0x0523, 0x0525, 0x0525, 0x0526, 0x0527, 0x0528, 0x0529, 0x052a, 0x052b, 0x052c, 0x052d, 0x052e, 0x052f, 0x0530, 0x0561, 0x0562, 0x0563, 0x0564, 0x0565, 0x0566, 0x0567, 0x0568, 0x0569, 0x056a, 0x056b, 0x056c, 0x056d, 0x056e, 0x056f, }; static const unsigned short gNormalizeTable0540[] = { /* U+0540 */ 0x0570, 0x0571, 0x0572, 0x0573, 0x0574, 0x0575, 0x0576, 0x0577, 0x0578, 0x0579, 0x057a, 0x057b, 0x057c, 0x057d, 0x057e, 0x057f, 0x0580, 0x0581, 0x0582, 0x0583, 0x0584, 0x0585, 0x0586, 0x0557, 0x0558, 0x0559, 0x055a, 0x055b, 0x055c, 0x055d, 0x055e, 0x055f, 0x0560, 0x0561, 0x0562, 0x0563, 0x0564, 0x0565, 0x0566, 0x0567, 0x0568, 0x0569, 0x056a, 0x056b, 0x056c, 0x056d, 0x056e, 0x056f, 0x0570, 0x0571, 0x0572, 0x0573, 0x0574, 0x0575, 0x0576, 0x0577, 0x0578, 0x0579, 0x057a, 0x057b, 0x057c, 0x057d, 0x057e, 0x057f, }; static const unsigned short gNormalizeTable0580[] = { /* U+0580 */ 0x0580, 0x0581, 0x0582, 0x0583, 0x0584, 0x0585, 0x0586, 0x0565, 0x0588, 0x0589, 0x058a, 0x058b, 0x058c, 0x058d, 0x058e, 0x058f, 0x0590, 0x0591, 0x0592, 0x0593, 0x0594, 0x0595, 0x0596, 0x0597, 0x0598, 0x0599, 0x059a, 0x059b, 0x059c, 0x059d, 0x059e, 0x059f, 0x05a0, 0x05a1, 0x05a2, 0x05a3, 0x05a4, 0x05a5, 0x05a6, 0x05a7, 0x05a8, 0x05a9, 0x05aa, 0x05ab, 0x05ac, 0x05ad, 0x05ae, 0x05af, 0x05b0, 0x05b1, 0x05b2, 0x05b3, 0x05b4, 0x05b5, 0x05b6, 0x05b7, 0x05b8, 0x05b9, 0x05ba, 0x05bb, 0x05bc, 0x05bd, 0x05be, 0x05bf, }; static const unsigned short gNormalizeTable0600[] = { /* U+0600 */ 0x0600, 0x0601, 0x0602, 0x0603, 0x0604, 0x0605, 0x0606, 0x0607, 0x0608, 0x0609, 0x060a, 0x060b, 0x060c, 0x060d, 0x060e, 0x060f, 0x0610, 0x0611, 0x0612, 0x0613, 0x0614, 0x0615, 0x0616, 0x0617, 0x0618, 0x0619, 0x061a, 0x061b, 0x061c, 0x061d, 0x061e, 0x061f, 0x0620, 0x0621, 0x0627, 0x0627, 0x0648, 0x0627, 0x064a, 0x0627, 0x0628, 0x0629, 0x062a, 0x062b, 0x062c, 0x062d, 0x062e, 0x062f, 0x0630, 0x0631, 0x0632, 0x0633, 0x0634, 0x0635, 0x0636, 0x0637, 0x0638, 0x0639, 0x063a, 0x063b, 0x063c, 0x063d, 0x063e, 0x063f, }; static const unsigned short gNormalizeTable0640[] = { /* U+0640 */ 0x0640, 0x0641, 0x0642, 0x0643, 0x0644, 0x0645, 0x0646, 0x0647, 0x0648, 0x0649, 0x064a, 0x064b, 0x064c, 0x064d, 0x064e, 0x064f, 0x0650, 0x0651, 0x0652, 0x0653, 0x0654, 0x0655, 0x0656, 0x0657, 0x0658, 0x0659, 0x065a, 0x065b, 0x065c, 0x065d, 0x065e, 0x065f, 0x0660, 0x0661, 0x0662, 0x0663, 0x0664, 0x0665, 0x0666, 0x0667, 0x0668, 0x0669, 0x066a, 0x066b, 0x066c, 0x066d, 0x066e, 0x066f, 0x0670, 0x0671, 0x0672, 0x0673, 0x0674, 0x0627, 0x0648, 0x06c7, 0x064a, 0x0679, 0x067a, 0x067b, 0x067c, 0x067d, 0x067e, 0x067f, }; static const unsigned short gNormalizeTable06c0[] = { /* U+06c0 */ 0x06d5, 0x06c1, 0x06c1, 0x06c3, 0x06c4, 0x06c5, 0x06c6, 0x06c7, 0x06c8, 0x06c9, 0x06ca, 0x06cb, 0x06cc, 0x06cd, 0x06ce, 0x06cf, 0x06d0, 0x06d1, 0x06d2, 0x06d2, 0x06d4, 0x06d5, 0x06d6, 0x06d7, 0x06d8, 0x06d9, 0x06da, 0x06db, 0x06dc, 0x06dd, 0x06de, 0x06df, 0x06e0, 0x06e1, 0x06e2, 0x06e3, 0x06e4, 0x06e5, 0x06e6, 0x06e7, 0x06e8, 0x06e9, 0x06ea, 0x06eb, 0x06ec, 0x06ed, 0x06ee, 0x06ef, 0x06f0, 0x06f1, 0x06f2, 0x06f3, 0x06f4, 0x06f5, 0x06f6, 0x06f7, 0x06f8, 0x06f9, 0x06fa, 0x06fb, 0x06fc, 0x06fd, 0x06fe, 0x06ff, }; static const unsigned short gNormalizeTable0900[] = { /* U+0900 */ 0x0900, 0x0901, 0x0902, 0x0903, 0x0904, 0x0905, 0x0906, 0x0907, 0x0908, 0x0909, 0x090a, 0x090b, 0x090c, 0x090d, 0x090e, 0x090f, 0x0910, 0x0911, 0x0912, 0x0913, 0x0914, 0x0915, 0x0916, 0x0917, 0x0918, 0x0919, 0x091a, 0x091b, 0x091c, 0x091d, 0x091e, 0x091f, 0x0920, 0x0921, 0x0922, 0x0923, 0x0924, 0x0925, 0x0926, 0x0927, 0x0928, 0x0928, 0x092a, 0x092b, 0x092c, 0x092d, 0x092e, 0x092f, 0x0930, 0x0930, 0x0932, 0x0933, 0x0933, 0x0935, 0x0936, 0x0937, 0x0938, 0x0939, 0x093a, 0x093b, 0x093c, 0x093d, 0x093e, 0x093f, }; static const unsigned short gNormalizeTable0940[] = { /* U+0940 */ 0x0940, 0x0941, 0x0942, 0x0943, 0x0944, 0x0945, 0x0946, 0x0947, 0x0948, 0x0949, 0x094a, 0x094b, 0x094c, 0x094d, 0x094e, 0x094f, 0x0950, 0x0951, 0x0952, 0x0953, 0x0954, 0x0955, 0x0956, 0x0957, 0x0915, 0x0916, 0x0917, 0x091c, 0x0921, 0x0922, 0x092b, 0x092f, 0x0960, 0x0961, 0x0962, 0x0963, 0x0964, 0x0965, 0x0966, 0x0967, 0x0968, 0x0969, 0x096a, 0x096b, 0x096c, 0x096d, 0x096e, 0x096f, 0x0970, 0x0971, 0x0972, 0x0973, 0x0974, 0x0975, 0x0976, 0x0977, 0x0978, 0x0979, 0x097a, 0x097b, 0x097c, 0x097d, 0x097e, 0x097f, }; static const unsigned short gNormalizeTable09c0[] = { /* U+09c0 */ 0x09c0, 0x09c1, 0x09c2, 0x09c3, 0x09c4, 0x09c5, 0x09c6, 0x09c7, 0x09c8, 0x09c9, 0x09ca, 0x09c7, 0x09c7, 0x09cd, 0x09ce, 0x09cf, 0x09d0, 0x09d1, 0x09d2, 0x09d3, 0x09d4, 0x09d5, 0x09d6, 0x09d7, 0x09d8, 0x09d9, 0x09da, 0x09db, 0x09a1, 0x09a2, 0x09de, 0x09af, 0x09e0, 0x09e1, 0x09e2, 0x09e3, 0x09e4, 0x09e5, 0x09e6, 0x09e7, 0x09e8, 0x09e9, 0x09ea, 0x09eb, 0x09ec, 0x09ed, 0x09ee, 0x09ef, 0x09f0, 0x09f1, 0x09f2, 0x09f3, 0x09f4, 0x09f5, 0x09f6, 0x09f7, 0x09f8, 0x09f9, 0x09fa, 0x09fb, 0x09fc, 0x09fd, 0x09fe, 0x09ff, }; static const unsigned short gNormalizeTable0a00[] = { /* U+0a00 */ 0x0a00, 0x0a01, 0x0a02, 0x0a03, 0x0a04, 0x0a05, 0x0a06, 0x0a07, 0x0a08, 0x0a09, 0x0a0a, 0x0a0b, 0x0a0c, 0x0a0d, 0x0a0e, 0x0a0f, 0x0a10, 0x0a11, 0x0a12, 0x0a13, 0x0a14, 0x0a15, 0x0a16, 0x0a17, 0x0a18, 0x0a19, 0x0a1a, 0x0a1b, 0x0a1c, 0x0a1d, 0x0a1e, 0x0a1f, 0x0a20, 0x0a21, 0x0a22, 0x0a23, 0x0a24, 0x0a25, 0x0a26, 0x0a27, 0x0a28, 0x0a29, 0x0a2a, 0x0a2b, 0x0a2c, 0x0a2d, 0x0a2e, 0x0a2f, 0x0a30, 0x0a31, 0x0a32, 0x0a32, 0x0a34, 0x0a35, 0x0a38, 0x0a37, 0x0a38, 0x0a39, 0x0a3a, 0x0a3b, 0x0a3c, 0x0a3d, 0x0a3e, 0x0a3f, }; static const unsigned short gNormalizeTable0a40[] = { /* U+0a40 */ 0x0a40, 0x0a41, 0x0a42, 0x0a43, 0x0a44, 0x0a45, 0x0a46, 0x0a47, 0x0a48, 0x0a49, 0x0a4a, 0x0a4b, 0x0a4c, 0x0a4d, 0x0a4e, 0x0a4f, 0x0a50, 0x0a51, 0x0a52, 0x0a53, 0x0a54, 0x0a55, 0x0a56, 0x0a57, 0x0a58, 0x0a16, 0x0a17, 0x0a1c, 0x0a5c, 0x0a5d, 0x0a2b, 0x0a5f, 0x0a60, 0x0a61, 0x0a62, 0x0a63, 0x0a64, 0x0a65, 0x0a66, 0x0a67, 0x0a68, 0x0a69, 0x0a6a, 0x0a6b, 0x0a6c, 0x0a6d, 0x0a6e, 0x0a6f, 0x0a70, 0x0a71, 0x0a72, 0x0a73, 0x0a74, 0x0a75, 0x0a76, 0x0a77, 0x0a78, 0x0a79, 0x0a7a, 0x0a7b, 0x0a7c, 0x0a7d, 0x0a7e, 0x0a7f, }; static const unsigned short gNormalizeTable0b40[] = { /* U+0b40 */ 0x0b40, 0x0b41, 0x0b42, 0x0b43, 0x0b44, 0x0b45, 0x0b46, 0x0b47, 0x0b47, 0x0b49, 0x0b4a, 0x0b47, 0x0b47, 0x0b4d, 0x0b4e, 0x0b4f, 0x0b50, 0x0b51, 0x0b52, 0x0b53, 0x0b54, 0x0b55, 0x0b56, 0x0b57, 0x0b58, 0x0b59, 0x0b5a, 0x0b5b, 0x0b21, 0x0b22, 0x0b5e, 0x0b5f, 0x0b60, 0x0b61, 0x0b62, 0x0b63, 0x0b64, 0x0b65, 0x0b66, 0x0b67, 0x0b68, 0x0b69, 0x0b6a, 0x0b6b, 0x0b6c, 0x0b6d, 0x0b6e, 0x0b6f, 0x0b70, 0x0b71, 0x0b72, 0x0b73, 0x0b74, 0x0b75, 0x0b76, 0x0b77, 0x0b78, 0x0b79, 0x0b7a, 0x0b7b, 0x0b7c, 0x0b7d, 0x0b7e, 0x0b7f, }; static const unsigned short gNormalizeTable0b80[] = { /* U+0b80 */ 0x0b80, 0x0b81, 0x0b82, 0x0b83, 0x0b84, 0x0b85, 0x0b86, 0x0b87, 0x0b88, 0x0b89, 0x0b8a, 0x0b8b, 0x0b8c, 0x0b8d, 0x0b8e, 0x0b8f, 0x0b90, 0x0b91, 0x0b92, 0x0b93, 0x0b92, 0x0b95, 0x0b96, 0x0b97, 0x0b98, 0x0b99, 0x0b9a, 0x0b9b, 0x0b9c, 0x0b9d, 0x0b9e, 0x0b9f, 0x0ba0, 0x0ba1, 0x0ba2, 0x0ba3, 0x0ba4, 0x0ba5, 0x0ba6, 0x0ba7, 0x0ba8, 0x0ba9, 0x0baa, 0x0bab, 0x0bac, 0x0bad, 0x0bae, 0x0baf, 0x0bb0, 0x0bb1, 0x0bb2, 0x0bb3, 0x0bb4, 0x0bb5, 0x0bb6, 0x0bb7, 0x0bb8, 0x0bb9, 0x0bba, 0x0bbb, 0x0bbc, 0x0bbd, 0x0bbe, 0x0bbf, }; static const unsigned short gNormalizeTable0bc0[] = { /* U+0bc0 */ 0x0bc0, 0x0bc1, 0x0bc2, 0x0bc3, 0x0bc4, 0x0bc5, 0x0bc6, 0x0bc7, 0x0bc8, 0x0bc9, 0x0bc6, 0x0bc7, 0x0bc6, 0x0bcd, 0x0bce, 0x0bcf, 0x0bd0, 0x0bd1, 0x0bd2, 0x0bd3, 0x0bd4, 0x0bd5, 0x0bd6, 0x0bd7, 0x0bd8, 0x0bd9, 0x0bda, 0x0bdb, 0x0bdc, 0x0bdd, 0x0bde, 0x0bdf, 0x0be0, 0x0be1, 0x0be2, 0x0be3, 0x0be4, 0x0be5, 0x0be6, 0x0be7, 0x0be8, 0x0be9, 0x0bea, 0x0beb, 0x0bec, 0x0bed, 0x0bee, 0x0bef, 0x0bf0, 0x0bf1, 0x0bf2, 0x0bf3, 0x0bf4, 0x0bf5, 0x0bf6, 0x0bf7, 0x0bf8, 0x0bf9, 0x0bfa, 0x0bfb, 0x0bfc, 0x0bfd, 0x0bfe, 0x0bff, }; static const unsigned short gNormalizeTable0c40[] = { /* U+0c40 */ 0x0c40, 0x0c41, 0x0c42, 0x0c43, 0x0c44, 0x0c45, 0x0c46, 0x0c47, 0x0c46, 0x0c49, 0x0c4a, 0x0c4b, 0x0c4c, 0x0c4d, 0x0c4e, 0x0c4f, 0x0c50, 0x0c51, 0x0c52, 0x0c53, 0x0c54, 0x0c55, 0x0c56, 0x0c57, 0x0c58, 0x0c59, 0x0c5a, 0x0c5b, 0x0c5c, 0x0c5d, 0x0c5e, 0x0c5f, 0x0c60, 0x0c61, 0x0c62, 0x0c63, 0x0c64, 0x0c65, 0x0c66, 0x0c67, 0x0c68, 0x0c69, 0x0c6a, 0x0c6b, 0x0c6c, 0x0c6d, 0x0c6e, 0x0c6f, 0x0c70, 0x0c71, 0x0c72, 0x0c73, 0x0c74, 0x0c75, 0x0c76, 0x0c77, 0x0c78, 0x0c79, 0x0c7a, 0x0c7b, 0x0c7c, 0x0c7d, 0x0c7e, 0x0c7f, }; static const unsigned short gNormalizeTable0cc0[] = { /* U+0cc0 */ 0x0cbf, 0x0cc1, 0x0cc2, 0x0cc3, 0x0cc4, 0x0cc5, 0x0cc6, 0x0cc6, 0x0cc6, 0x0cc9, 0x0cc6, 0x0cc6, 0x0ccc, 0x0ccd, 0x0cce, 0x0ccf, 0x0cd0, 0x0cd1, 0x0cd2, 0x0cd3, 0x0cd4, 0x0cd5, 0x0cd6, 0x0cd7, 0x0cd8, 0x0cd9, 0x0cda, 0x0cdb, 0x0cdc, 0x0cdd, 0x0cde, 0x0cdf, 0x0ce0, 0x0ce1, 0x0ce2, 0x0ce3, 0x0ce4, 0x0ce5, 0x0ce6, 0x0ce7, 0x0ce8, 0x0ce9, 0x0cea, 0x0ceb, 0x0cec, 0x0ced, 0x0cee, 0x0cef, 0x0cf0, 0x0cf1, 0x0cf2, 0x0cf3, 0x0cf4, 0x0cf5, 0x0cf6, 0x0cf7, 0x0cf8, 0x0cf9, 0x0cfa, 0x0cfb, 0x0cfc, 0x0cfd, 0x0cfe, 0x0cff, }; static const unsigned short gNormalizeTable0d40[] = { /* U+0d40 */ 0x0d40, 0x0d41, 0x0d42, 0x0d43, 0x0d44, 0x0d45, 0x0d46, 0x0d47, 0x0d48, 0x0d49, 0x0d46, 0x0d47, 0x0d46, 0x0d4d, 0x0d4e, 0x0d4f, 0x0d50, 0x0d51, 0x0d52, 0x0d53, 0x0d54, 0x0d55, 0x0d56, 0x0d57, 0x0d58, 0x0d59, 0x0d5a, 0x0d5b, 0x0d5c, 0x0d5d, 0x0d5e, 0x0d5f, 0x0d60, 0x0d61, 0x0d62, 0x0d63, 0x0d64, 0x0d65, 0x0d66, 0x0d67, 0x0d68, 0x0d69, 0x0d6a, 0x0d6b, 0x0d6c, 0x0d6d, 0x0d6e, 0x0d6f, 0x0d70, 0x0d71, 0x0d72, 0x0d73, 0x0d74, 0x0d75, 0x0d76, 0x0d77, 0x0d78, 0x0d79, 0x0d7a, 0x0d7b, 0x0d7c, 0x0d7d, 0x0d7e, 0x0d7f, }; static const unsigned short gNormalizeTable0dc0[] = { /* U+0dc0 */ 0x0dc0, 0x0dc1, 0x0dc2, 0x0dc3, 0x0dc4, 0x0dc5, 0x0dc6, 0x0dc7, 0x0dc8, 0x0dc9, 0x0dca, 0x0dcb, 0x0dcc, 0x0dcd, 0x0dce, 0x0dcf, 0x0dd0, 0x0dd1, 0x0dd2, 0x0dd3, 0x0dd4, 0x0dd5, 0x0dd6, 0x0dd7, 0x0dd8, 0x0dd9, 0x0dd9, 0x0ddb, 0x0dd9, 0x0dd9, 0x0dd9, 0x0ddf, 0x0de0, 0x0de1, 0x0de2, 0x0de3, 0x0de4, 0x0de5, 0x0de6, 0x0de7, 0x0de8, 0x0de9, 0x0dea, 0x0deb, 0x0dec, 0x0ded, 0x0dee, 0x0def, 0x0df0, 0x0df1, 0x0df2, 0x0df3, 0x0df4, 0x0df5, 0x0df6, 0x0df7, 0x0df8, 0x0df9, 0x0dfa, 0x0dfb, 0x0dfc, 0x0dfd, 0x0dfe, 0x0dff, }; static const unsigned short gNormalizeTable0e00[] = { /* U+0e00 */ 0x0e00, 0x0e01, 0x0e02, 0x0e03, 0x0e04, 0x0e05, 0x0e06, 0x0e07, 0x0e08, 0x0e09, 0x0e0a, 0x0e0b, 0x0e0c, 0x0e0d, 0x0e0e, 0x0e0f, 0x0e10, 0x0e11, 0x0e12, 0x0e13, 0x0e14, 0x0e15, 0x0e16, 0x0e17, 0x0e18, 0x0e19, 0x0e1a, 0x0e1b, 0x0e1c, 0x0e1d, 0x0e1e, 0x0e1f, 0x0e20, 0x0e21, 0x0e22, 0x0e23, 0x0e24, 0x0e25, 0x0e26, 0x0e27, 0x0e28, 0x0e29, 0x0e2a, 0x0e2b, 0x0e2c, 0x0e2d, 0x0e2e, 0x0e2f, 0x0e30, 0x0e31, 0x0e32, 0x0e4d, 0x0e34, 0x0e35, 0x0e36, 0x0e37, 0x0e38, 0x0e39, 0x0e3a, 0x0e3b, 0x0e3c, 0x0e3d, 0x0e3e, 0x0e3f, }; static const unsigned short gNormalizeTable0e80[] = { /* U+0e80 */ 0x0e80, 0x0e81, 0x0e82, 0x0e83, 0x0e84, 0x0e85, 0x0e86, 0x0e87, 0x0e88, 0x0e89, 0x0e8a, 0x0e8b, 0x0e8c, 0x0e8d, 0x0e8e, 0x0e8f, 0x0e90, 0x0e91, 0x0e92, 0x0e93, 0x0e94, 0x0e95, 0x0e96, 0x0e97, 0x0e98, 0x0e99, 0x0e9a, 0x0e9b, 0x0e9c, 0x0e9d, 0x0e9e, 0x0e9f, 0x0ea0, 0x0ea1, 0x0ea2, 0x0ea3, 0x0ea4, 0x0ea5, 0x0ea6, 0x0ea7, 0x0ea8, 0x0ea9, 0x0eaa, 0x0eab, 0x0eac, 0x0ead, 0x0eae, 0x0eaf, 0x0eb0, 0x0eb1, 0x0eb2, 0x0ecd, 0x0eb4, 0x0eb5, 0x0eb6, 0x0eb7, 0x0eb8, 0x0eb9, 0x0eba, 0x0ebb, 0x0ebc, 0x0ebd, 0x0ebe, 0x0ebf, }; static const unsigned short gNormalizeTable0ec0[] = { /* U+0ec0 */ 0x0ec0, 0x0ec1, 0x0ec2, 0x0ec3, 0x0ec4, 0x0ec5, 0x0ec6, 0x0ec7, 0x0ec8, 0x0ec9, 0x0eca, 0x0ecb, 0x0ecc, 0x0ecd, 0x0ece, 0x0ecf, 0x0ed0, 0x0ed1, 0x0ed2, 0x0ed3, 0x0ed4, 0x0ed5, 0x0ed6, 0x0ed7, 0x0ed8, 0x0ed9, 0x0eda, 0x0edb, 0x0eab, 0x0eab, 0x0ede, 0x0edf, 0x0ee0, 0x0ee1, 0x0ee2, 0x0ee3, 0x0ee4, 0x0ee5, 0x0ee6, 0x0ee7, 0x0ee8, 0x0ee9, 0x0eea, 0x0eeb, 0x0eec, 0x0eed, 0x0eee, 0x0eef, 0x0ef0, 0x0ef1, 0x0ef2, 0x0ef3, 0x0ef4, 0x0ef5, 0x0ef6, 0x0ef7, 0x0ef8, 0x0ef9, 0x0efa, 0x0efb, 0x0efc, 0x0efd, 0x0efe, 0x0eff, }; static const unsigned short gNormalizeTable0f00[] = { /* U+0f00 */ 0x0f00, 0x0f01, 0x0f02, 0x0f03, 0x0f04, 0x0f05, 0x0f06, 0x0f07, 0x0f08, 0x0f09, 0x0f0a, 0x0f0b, 0x0f0b, 0x0f0d, 0x0f0e, 0x0f0f, 0x0f10, 0x0f11, 0x0f12, 0x0f13, 0x0f14, 0x0f15, 0x0f16, 0x0f17, 0x0f18, 0x0f19, 0x0f1a, 0x0f1b, 0x0f1c, 0x0f1d, 0x0f1e, 0x0f1f, 0x0f20, 0x0f21, 0x0f22, 0x0f23, 0x0f24, 0x0f25, 0x0f26, 0x0f27, 0x0f28, 0x0f29, 0x0f2a, 0x0f2b, 0x0f2c, 0x0f2d, 0x0f2e, 0x0f2f, 0x0f30, 0x0f31, 0x0f32, 0x0f33, 0x0f34, 0x0f35, 0x0f36, 0x0f37, 0x0f38, 0x0f39, 0x0f3a, 0x0f3b, 0x0f3c, 0x0f3d, 0x0f3e, 0x0f3f, }; static const unsigned short gNormalizeTable0f40[] = { /* U+0f40 */ 0x0f40, 0x0f41, 0x0f42, 0x0f42, 0x0f44, 0x0f45, 0x0f46, 0x0f47, 0x0f48, 0x0f49, 0x0f4a, 0x0f4b, 0x0f4c, 0x0f4c, 0x0f4e, 0x0f4f, 0x0f50, 0x0f51, 0x0f51, 0x0f53, 0x0f54, 0x0f55, 0x0f56, 0x0f56, 0x0f58, 0x0f59, 0x0f5a, 0x0f5b, 0x0f5b, 0x0f5d, 0x0f5e, 0x0f5f, 0x0f60, 0x0f61, 0x0f62, 0x0f63, 0x0f64, 0x0f65, 0x0f66, 0x0f67, 0x0f68, 0x0f40, 0x0f6a, 0x0f6b, 0x0f6c, 0x0f6d, 0x0f6e, 0x0f6f, 0x0f70, 0x0f71, 0x0f72, 0x0f71, 0x0f74, 0x0f71, 0x0fb2, 0x0fb2, 0x0fb3, 0x0fb3, 0x0f7a, 0x0f7b, 0x0f7c, 0x0f7d, 0x0f7e, 0x0f7f, }; static const unsigned short gNormalizeTable0f80[] = { /* U+0f80 */ 0x0f80, 0x0f71, 0x0f82, 0x0f83, 0x0f84, 0x0f85, 0x0f86, 0x0f87, 0x0f88, 0x0f89, 0x0f8a, 0x0f8b, 0x0f8c, 0x0f8d, 0x0f8e, 0x0f8f, 0x0f90, 0x0f91, 0x0f92, 0x0f92, 0x0f94, 0x0f95, 0x0f96, 0x0f97, 0x0f98, 0x0f99, 0x0f9a, 0x0f9b, 0x0f9c, 0x0f9c, 0x0f9e, 0x0f9f, 0x0fa0, 0x0fa1, 0x0fa1, 0x0fa3, 0x0fa4, 0x0fa5, 0x0fa6, 0x0fa6, 0x0fa8, 0x0fa9, 0x0faa, 0x0fab, 0x0fab, 0x0fad, 0x0fae, 0x0faf, 0x0fb0, 0x0fb1, 0x0fb2, 0x0fb3, 0x0fb4, 0x0fb5, 0x0fb6, 0x0fb7, 0x0fb8, 0x0f90, 0x0fba, 0x0fbb, 0x0fbc, 0x0fbd, 0x0fbe, 0x0fbf, }; static const unsigned short gNormalizeTable1000[] = { /* U+1000 */ 0x1000, 0x1001, 0x1002, 0x1003, 0x1004, 0x1005, 0x1006, 0x1007, 0x1008, 0x1009, 0x100a, 0x100b, 0x100c, 0x100d, 0x100e, 0x100f, 0x1010, 0x1011, 0x1012, 0x1013, 0x1014, 0x1015, 0x1016, 0x1017, 0x1018, 0x1019, 0x101a, 0x101b, 0x101c, 0x101d, 0x101e, 0x101f, 0x1020, 0x1021, 0x1022, 0x1023, 0x1024, 0x1025, 0x1025, 0x1027, 0x1028, 0x1029, 0x102a, 0x102b, 0x102c, 0x102d, 0x102e, 0x102f, 0x1030, 0x1031, 0x1032, 0x1033, 0x1034, 0x1035, 0x1036, 0x1037, 0x1038, 0x1039, 0x103a, 0x103b, 0x103c, 0x103d, 0x103e, 0x103f, }; static const unsigned short gNormalizeTable1080[] = { /* U+1080 */ 0x1080, 0x1081, 0x1082, 0x1083, 0x1084, 0x1085, 0x1086, 0x1087, 0x1088, 0x1089, 0x108a, 0x108b, 0x108c, 0x108d, 0x108e, 0x108f, 0x1090, 0x1091, 0x1092, 0x1093, 0x1094, 0x1095, 0x1096, 0x1097, 0x1098, 0x1099, 0x109a, 0x109b, 0x109c, 0x109d, 0x109e, 0x109f, 0x2d00, 0x2d01, 0x2d02, 0x2d03, 0x2d04, 0x2d05, 0x2d06, 0x2d07, 0x2d08, 0x2d09, 0x2d0a, 0x2d0b, 0x2d0c, 0x2d0d, 0x2d0e, 0x2d0f, 0x2d10, 0x2d11, 0x2d12, 0x2d13, 0x2d14, 0x2d15, 0x2d16, 0x2d17, 0x2d18, 0x2d19, 0x2d1a, 0x2d1b, 0x2d1c, 0x2d1d, 0x2d1e, 0x2d1f, }; static const unsigned short gNormalizeTable10c0[] = { /* U+10c0 */ 0x2d20, 0x2d21, 0x2d22, 0x2d23, 0x2d24, 0x2d25, 0x10c6, 0x10c7, 0x10c8, 0x10c9, 0x10ca, 0x10cb, 0x10cc, 0x10cd, 0x10ce, 0x10cf, 0x10d0, 0x10d1, 0x10d2, 0x10d3, 0x10d4, 0x10d5, 0x10d6, 0x10d7, 0x10d8, 0x10d9, 0x10da, 0x10db, 0x10dc, 0x10dd, 0x10de, 0x10df, 0x10e0, 0x10e1, 0x10e2, 0x10e3, 0x10e4, 0x10e5, 0x10e6, 0x10e7, 0x10e8, 0x10e9, 0x10ea, 0x10eb, 0x10ec, 0x10ed, 0x10ee, 0x10ef, 0x10f0, 0x10f1, 0x10f2, 0x10f3, 0x10f4, 0x10f5, 0x10f6, 0x10f7, 0x10f8, 0x10f9, 0x10fa, 0x10fb, 0x10dc, 0x10fd, 0x10fe, 0x10ff, }; static const unsigned short gNormalizeTable1140[] = { /* U+1140 */ 0x1140, 0x1141, 0x1142, 0x1143, 0x1144, 0x1145, 0x1146, 0x1147, 0x1148, 0x1149, 0x114a, 0x114b, 0x114c, 0x114d, 0x114e, 0x114f, 0x1150, 0x1151, 0x1152, 0x1153, 0x1154, 0x1155, 0x1156, 0x1157, 0x1158, 0x1159, 0x115a, 0x115b, 0x115c, 0x115d, 0x115e, 0x0020, 0x0020, 0x1161, 0x1162, 0x1163, 0x1164, 0x1165, 0x1166, 0x1167, 0x1168, 0x1169, 0x116a, 0x116b, 0x116c, 0x116d, 0x116e, 0x116f, 0x1170, 0x1171, 0x1172, 0x1173, 0x1174, 0x1175, 0x1176, 0x1177, 0x1178, 0x1179, 0x117a, 0x117b, 0x117c, 0x117d, 0x117e, 0x117f, }; static const unsigned short gNormalizeTable1780[] = { /* U+1780 */ 0x1780, 0x1781, 0x1782, 0x1783, 0x1784, 0x1785, 0x1786, 0x1787, 0x1788, 0x1789, 0x178a, 0x178b, 0x178c, 0x178d, 0x178e, 0x178f, 0x1790, 0x1791, 0x1792, 0x1793, 0x1794, 0x1795, 0x1796, 0x1797, 0x1798, 0x1799, 0x179a, 0x179b, 0x179c, 0x179d, 0x179e, 0x179f, 0x17a0, 0x17a1, 0x17a2, 0x17a3, 0x17a4, 0x17a5, 0x17a6, 0x17a7, 0x17a8, 0x17a9, 0x17aa, 0x17ab, 0x17ac, 0x17ad, 0x17ae, 0x17af, 0x17b0, 0x17b1, 0x17b2, 0x17b3, 0x0020, 0x0020, 0x17b6, 0x17b7, 0x17b8, 0x17b9, 0x17ba, 0x17bb, 0x17bc, 0x17bd, 0x17be, 0x17bf, }; static const unsigned short gNormalizeTable1800[] = { /* U+1800 */ 0x1800, 0x1801, 0x1802, 0x1803, 0x1804, 0x1805, 0x1806, 0x1807, 0x1808, 0x1809, 0x180a, 0x0020, 0x0020, 0x0020, 0x180e, 0x180f, 0x1810, 0x1811, 0x1812, 0x1813, 0x1814, 0x1815, 0x1816, 0x1817, 0x1818, 0x1819, 0x181a, 0x181b, 0x181c, 0x181d, 0x181e, 0x181f, 0x1820, 0x1821, 0x1822, 0x1823, 0x1824, 0x1825, 0x1826, 0x1827, 0x1828, 0x1829, 0x182a, 0x182b, 0x182c, 0x182d, 0x182e, 0x182f, 0x1830, 0x1831, 0x1832, 0x1833, 0x1834, 0x1835, 0x1836, 0x1837, 0x1838, 0x1839, 0x183a, 0x183b, 0x183c, 0x183d, 0x183e, 0x183f, }; static const unsigned short gNormalizeTable1b00[] = { /* U+1b00 */ 0x1b00, 0x1b01, 0x1b02, 0x1b03, 0x1b04, 0x1b05, 0x1b05, 0x1b07, 0x1b07, 0x1b09, 0x1b09, 0x1b0b, 0x1b0b, 0x1b0d, 0x1b0d, 0x1b0f, 0x1b10, 0x1b11, 0x1b11, 0x1b13, 0x1b14, 0x1b15, 0x1b16, 0x1b17, 0x1b18, 0x1b19, 0x1b1a, 0x1b1b, 0x1b1c, 0x1b1d, 0x1b1e, 0x1b1f, 0x1b20, 0x1b21, 0x1b22, 0x1b23, 0x1b24, 0x1b25, 0x1b26, 0x1b27, 0x1b28, 0x1b29, 0x1b2a, 0x1b2b, 0x1b2c, 0x1b2d, 0x1b2e, 0x1b2f, 0x1b30, 0x1b31, 0x1b32, 0x1b33, 0x1b34, 0x1b35, 0x1b36, 0x1b37, 0x1b38, 0x1b39, 0x1b3a, 0x1b3a, 0x1b3c, 0x1b3c, 0x1b3e, 0x1b3f, }; static const unsigned short gNormalizeTable1b40[] = { /* U+1b40 */ 0x1b3e, 0x1b3f, 0x1b42, 0x1b42, 0x1b44, 0x1b45, 0x1b46, 0x1b47, 0x1b48, 0x1b49, 0x1b4a, 0x1b4b, 0x1b4c, 0x1b4d, 0x1b4e, 0x1b4f, 0x1b50, 0x1b51, 0x1b52, 0x1b53, 0x1b54, 0x1b55, 0x1b56, 0x1b57, 0x1b58, 0x1b59, 0x1b5a, 0x1b5b, 0x1b5c, 0x1b5d, 0x1b5e, 0x1b5f, 0x1b60, 0x1b61, 0x1b62, 0x1b63, 0x1b64, 0x1b65, 0x1b66, 0x1b67, 0x1b68, 0x1b69, 0x1b6a, 0x1b6b, 0x1b6c, 0x1b6d, 0x1b6e, 0x1b6f, 0x1b70, 0x1b71, 0x1b72, 0x1b73, 0x1b74, 0x1b75, 0x1b76, 0x1b77, 0x1b78, 0x1b79, 0x1b7a, 0x1b7b, 0x1b7c, 0x1b7d, 0x1b7e, 0x1b7f, }; static const unsigned short gNormalizeTable1d00[] = { /* U+1d00 */ 0x1d00, 0x1d01, 0x1d02, 0x1d03, 0x1d04, 0x1d05, 0x1d06, 0x1d07, 0x1d08, 0x1d09, 0x1d0a, 0x1d0b, 0x1d0c, 0x1d0d, 0x1d0e, 0x1d0f, 0x1d10, 0x1d11, 0x1d12, 0x1d13, 0x1d14, 0x1d15, 0x1d16, 0x1d17, 0x1d18, 0x1d19, 0x1d1a, 0x1d1b, 0x1d1c, 0x1d1d, 0x1d1e, 0x1d1f, 0x1d20, 0x1d21, 0x1d22, 0x1d23, 0x1d24, 0x1d25, 0x1d26, 0x1d27, 0x1d28, 0x1d29, 0x1d2a, 0x1d2b, 0x0061, 0x00e6, 0x0062, 0x1d2f, 0x0064, 0x0065, 0x01dd, 0x0067, 0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x1d3b, 0x006f, 0x0223, 0x0070, 0x0072, }; static const unsigned short gNormalizeTable1d40[] = { /* U+1d40 */ 0x0074, 0x0075, 0x0077, 0x0061, 0x0250, 0x0251, 0x1d02, 0x0062, 0x0064, 0x0065, 0x0259, 0x025b, 0x025c, 0x0067, 0x1d4e, 0x006b, 0x006d, 0x014b, 0x006f, 0x0254, 0x1d16, 0x1d17, 0x0070, 0x0074, 0x0075, 0x1d1d, 0x026f, 0x0076, 0x1d25, 0x03b2, 0x03b3, 0x03b4, 0x03c6, 0x03c7, 0x0069, 0x0072, 0x0075, 0x0076, 0x03b2, 0x03b3, 0x03c1, 0x03c6, 0x03c7, 0x1d6b, 0x1d6c, 0x1d6d, 0x1d6e, 0x1d6f, 0x1d70, 0x1d71, 0x1d72, 0x1d73, 0x1d74, 0x1d75, 0x1d76, 0x1d77, 0x043d, 0x1d79, 0x1d7a, 0x1d7b, 0x1d7c, 0x1d7d, 0x1d7e, 0x1d7f, }; static const unsigned short gNormalizeTable1d80[] = { /* U+1d80 */ 0x1d80, 0x1d81, 0x1d82, 0x1d83, 0x1d84, 0x1d85, 0x1d86, 0x1d87, 0x1d88, 0x1d89, 0x1d8a, 0x1d8b, 0x1d8c, 0x1d8d, 0x1d8e, 0x1d8f, 0x1d90, 0x1d91, 0x1d92, 0x1d93, 0x1d94, 0x1d95, 0x1d96, 0x1d97, 0x1d98, 0x1d99, 0x1d9a, 0x0252, 0x0063, 0x0255, 0x00f0, 0x025c, 0x0066, 0x025f, 0x0261, 0x0265, 0x0268, 0x0269, 0x026a, 0x1d7b, 0x029d, 0x026d, 0x1d85, 0x029f, 0x0271, 0x0270, 0x0272, 0x0273, 0x0274, 0x0275, 0x0278, 0x0282, 0x0283, 0x01ab, 0x0289, 0x028a, 0x1d1c, 0x028b, 0x028c, 0x007a, 0x0290, 0x0291, 0x0292, 0x03b8, }; static const unsigned short gNormalizeTable1e00[] = { /* U+1e00 */ 0x0061, 0x0061, 0x0062, 0x0062, 0x0062, 0x0062, 0x0062, 0x0062, 0x0063, 0x0063, 0x0064, 0x0064, 0x0064, 0x0064, 0x0064, 0x0064, 0x0064, 0x0064, 0x0064, 0x0064, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0066, 0x0066, 0x0067, 0x0067, 0x0068, 0x0068, 0x0068, 0x0068, 0x0068, 0x0068, 0x0068, 0x0068, 0x0068, 0x0068, 0x0069, 0x0069, 0x0069, 0x0069, 0x006b, 0x006b, 0x006b, 0x006b, 0x006b, 0x006b, 0x006c, 0x006c, 0x006c, 0x006c, 0x006c, 0x006c, 0x006c, 0x006c, 0x006d, 0x006d, }; static const unsigned short gNormalizeTable1e40[] = { /* U+1e40 */ 0x006d, 0x006d, 0x006d, 0x006d, 0x006e, 0x006e, 0x006e, 0x006e, 0x006e, 0x006e, 0x006e, 0x006e, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x0070, 0x0070, 0x0070, 0x0070, 0x0072, 0x0072, 0x0072, 0x0072, 0x0072, 0x0072, 0x0072, 0x0072, 0x0073, 0x0073, 0x0073, 0x0073, 0x0073, 0x0073, 0x0073, 0x0073, 0x0073, 0x0073, 0x0074, 0x0074, 0x0074, 0x0074, 0x0074, 0x0074, 0x0074, 0x0074, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0076, 0x0076, 0x0076, 0x0076, }; static const unsigned short gNormalizeTable1e80[] = { /* U+1e80 */ 0x0077, 0x0077, 0x0077, 0x0077, 0x0077, 0x0077, 0x0077, 0x0077, 0x0077, 0x0077, 0x0078, 0x0078, 0x0078, 0x0078, 0x0079, 0x0079, 0x007a, 0x007a, 0x007a, 0x007a, 0x007a, 0x007a, 0x0068, 0x0074, 0x0077, 0x0079, 0x0061, 0x0073, 0x1e9c, 0x1e9d, 0x0073, 0x1e9f, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0061, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, }; static const unsigned short gNormalizeTable1ec0[] = { /* U+1ec0 */ 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0065, 0x0069, 0x0069, 0x0069, 0x0069, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x006f, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0075, 0x0079, 0x0079, 0x0079, 0x0079, 0x0079, 0x0079, 0x0079, 0x0079, 0x1efb, 0x1efb, 0x1efd, 0x1efd, 0x1eff, 0x1eff, }; static const unsigned short gNormalizeTable1f00[] = { /* U+1f00 */ 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b5, 0x03b5, 0x03b5, 0x03b5, 0x03b5, 0x03b5, 0x1f16, 0x1f17, 0x03b5, 0x03b5, 0x03b5, 0x03b5, 0x03b5, 0x03b5, 0x1f1e, 0x1f1f, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9, }; static const unsigned short gNormalizeTable1f40[] = { /* U+1f40 */ 0x03bf, 0x03bf, 0x03bf, 0x03bf, 0x03bf, 0x03bf, 0x1f46, 0x1f47, 0x03bf, 0x03bf, 0x03bf, 0x03bf, 0x03bf, 0x03bf, 0x1f4e, 0x1f4f, 0x03c5, 0x03c5, 0x03c5, 0x03c5, 0x03c5, 0x03c5, 0x03c5, 0x03c5, 0x1f58, 0x03c5, 0x1f5a, 0x03c5, 0x1f5c, 0x03c5, 0x1f5e, 0x03c5, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03b1, 0x03b1, 0x03b5, 0x03b5, 0x03b7, 0x03b7, 0x03b9, 0x03b9, 0x03bf, 0x03bf, 0x03c5, 0x03c5, 0x03c9, 0x03c9, 0x1f7e, 0x1f7f, }; static const unsigned short gNormalizeTable1f80[] = { /* U+1f80 */ 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03b7, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03c9, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x1fb5, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x03b1, 0x0020, 0x03b9, 0x0020, }; static const unsigned short gNormalizeTable1fc0[] = { /* U+1fc0 */ 0x0020, 0x0020, 0x03b7, 0x03b7, 0x03b7, 0x1fc5, 0x03b7, 0x03b7, 0x03b5, 0x03b5, 0x03b7, 0x03b7, 0x03b7, 0x0020, 0x0020, 0x0020, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x1fd4, 0x1fd5, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x03b9, 0x1fdc, 0x0020, 0x0020, 0x0020, 0x03c5, 0x03c5, 0x03c5, 0x03c5, 0x03c1, 0x03c1, 0x03c5, 0x03c5, 0x03c5, 0x03c5, 0x03c5, 0x03c5, 0x03c1, 0x0020, 0x0020, 0x0060, 0x1ff0, 0x1ff1, 0x03c9, 0x03c9, 0x03c9, 0x1ff5, 0x03c9, 0x03c9, 0x03bf, 0x03bf, 0x03c9, 0x03c9, 0x03c9, 0x0020, 0x0020, 0x1fff, }; static const unsigned short gNormalizeTable2000[] = { /* U+2000 */ 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x2010, 0x2010, 0x2012, 0x2013, 0x2014, 0x2015, 0x2016, 0x0020, 0x2018, 0x2019, 0x201a, 0x201b, 0x201c, 0x201d, 0x201e, 0x201f, 0x2020, 0x2021, 0x2022, 0x2023, 0x002e, 0x002e, 0x002e, 0x2027, 0x2028, 0x2029, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x2030, 0x2031, 0x2032, 0x2032, 0x2032, 0x2035, 0x2035, 0x2035, 0x2038, 0x2039, 0x203a, 0x203b, 0x0021, 0x203d, 0x0020, 0x203f, }; static const unsigned short gNormalizeTable2040[] = { /* U+2040 */ 0x2040, 0x2041, 0x2042, 0x2043, 0x2044, 0x2045, 0x2046, 0x003f, 0x003f, 0x0021, 0x204a, 0x204b, 0x204c, 0x204d, 0x204e, 0x204f, 0x2050, 0x2051, 0x2052, 0x2053, 0x2054, 0x2055, 0x2056, 0x2032, 0x2058, 0x2059, 0x205a, 0x205b, 0x205c, 0x205d, 0x205e, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0030, 0x0069, 0x2072, 0x2073, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, 0x0039, 0x002b, 0x2212, 0x003d, 0x0028, 0x0029, 0x006e, }; static const unsigned short gNormalizeTable2080[] = { /* U+2080 */ 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, 0x0039, 0x002b, 0x2212, 0x003d, 0x0028, 0x0029, 0x208f, 0x0061, 0x0065, 0x006f, 0x0078, 0x0259, 0x2095, 0x2096, 0x2097, 0x2098, 0x2099, 0x209a, 0x209b, 0x209c, 0x209d, 0x209e, 0x209f, 0x20a0, 0x20a1, 0x20a2, 0x20a3, 0x20a4, 0x20a5, 0x20a6, 0x20a7, 0x0072, 0x20a9, 0x20aa, 0x20ab, 0x20ac, 0x20ad, 0x20ae, 0x20af, 0x20b0, 0x20b1, 0x20b2, 0x20b3, 0x20b4, 0x20b5, 0x20b6, 0x20b7, 0x20b8, 0x20b9, 0x20ba, 0x20bb, 0x20bc, 0x20bd, 0x20be, 0x20bf, }; static const unsigned short gNormalizeTable2100[] = { /* U+2100 */ 0x0061, 0x0061, 0x0063, 0x00b0, 0x2104, 0x0063, 0x0063, 0x025b, 0x2108, 0x00b0, 0x0067, 0x0068, 0x0068, 0x0068, 0x0068, 0x0127, 0x0069, 0x0069, 0x006c, 0x006c, 0x2114, 0x006e, 0x006e, 0x2117, 0x2118, 0x0070, 0x0071, 0x0072, 0x0072, 0x0072, 0x211e, 0x211f, 0x0073, 0x0074, 0x0074, 0x2123, 0x007a, 0x2125, 0x03c9, 0x2127, 0x007a, 0x2129, 0x006b, 0x0061, 0x0062, 0x0063, 0x212e, 0x0065, 0x0065, 0x0066, 0x214e, 0x006d, 0x006f, 0x05d0, 0x05d1, 0x05d2, 0x05d3, 0x0069, 0x213a, 0x0066, 0x03c0, 0x03b3, 0x03b3, 0x03c0, }; static const unsigned short gNormalizeTable2140[] = { /* U+2140 */ 0x2211, 0x2141, 0x2142, 0x2143, 0x2144, 0x0064, 0x0064, 0x0065, 0x0069, 0x006a, 0x214a, 0x214b, 0x214c, 0x214d, 0x214e, 0x214f, 0x0031, 0x0031, 0x0031, 0x0031, 0x0032, 0x0031, 0x0032, 0x0033, 0x0034, 0x0031, 0x0035, 0x0031, 0x0033, 0x0035, 0x0037, 0x0031, 0x0069, 0x0069, 0x0069, 0x0069, 0x0076, 0x0076, 0x0076, 0x0076, 0x0069, 0x0078, 0x0078, 0x0078, 0x006c, 0x0063, 0x0064, 0x006d, 0x0069, 0x0069, 0x0069, 0x0069, 0x0076, 0x0076, 0x0076, 0x0076, 0x0069, 0x0078, 0x0078, 0x0078, 0x006c, 0x0063, 0x0064, 0x006d, }; static const unsigned short gNormalizeTable2180[] = { /* U+2180 */ 0x2180, 0x2181, 0x2182, 0x2184, 0x2184, 0x2185, 0x2186, 0x2187, 0x2188, 0x0030, 0x218a, 0x218b, 0x218c, 0x218d, 0x218e, 0x218f, 0x2190, 0x2191, 0x2192, 0x2193, 0x2194, 0x2195, 0x2196, 0x2197, 0x2198, 0x2199, 0x2190, 0x2192, 0x219c, 0x219d, 0x219e, 0x219f, 0x21a0, 0x21a1, 0x21a2, 0x21a3, 0x21a4, 0x21a5, 0x21a6, 0x21a7, 0x21a8, 0x21a9, 0x21aa, 0x21ab, 0x21ac, 0x21ad, 0x2194, 0x21af, 0x21b0, 0x21b1, 0x21b2, 0x21b3, 0x21b4, 0x21b5, 0x21b6, 0x21b7, 0x21b8, 0x21b9, 0x21ba, 0x21bb, 0x21bc, 0x21bd, 0x21be, 0x21bf, }; static const unsigned short gNormalizeTable21c0[] = { /* U+21c0 */ 0x21c0, 0x21c1, 0x21c2, 0x21c3, 0x21c4, 0x21c5, 0x21c6, 0x21c7, 0x21c8, 0x21c9, 0x21ca, 0x21cb, 0x21cc, 0x21d0, 0x21d4, 0x21d2, 0x21d0, 0x21d1, 0x21d2, 0x21d3, 0x21d4, 0x21d5, 0x21d6, 0x21d7, 0x21d8, 0x21d9, 0x21da, 0x21db, 0x21dc, 0x21dd, 0x21de, 0x21df, 0x21e0, 0x21e1, 0x21e2, 0x21e3, 0x21e4, 0x21e5, 0x21e6, 0x21e7, 0x21e8, 0x21e9, 0x21ea, 0x21eb, 0x21ec, 0x21ed, 0x21ee, 0x21ef, 0x21f0, 0x21f1, 0x21f2, 0x21f3, 0x21f4, 0x21f5, 0x21f6, 0x21f7, 0x21f8, 0x21f9, 0x21fa, 0x21fb, 0x21fc, 0x21fd, 0x21fe, 0x21ff, }; static const unsigned short gNormalizeTable2200[] = { /* U+2200 */ 0x2200, 0x2201, 0x2202, 0x2203, 0x2203, 0x2205, 0x2206, 0x2207, 0x2208, 0x2208, 0x220a, 0x220b, 0x220b, 0x220d, 0x220e, 0x220f, 0x2210, 0x2211, 0x2212, 0x2213, 0x2214, 0x2215, 0x2216, 0x2217, 0x2218, 0x2219, 0x221a, 0x221b, 0x221c, 0x221d, 0x221e, 0x221f, 0x2220, 0x2221, 0x2222, 0x2223, 0x2223, 0x2225, 0x2225, 0x2227, 0x2228, 0x2229, 0x222a, 0x222b, 0x222b, 0x222b, 0x222e, 0x222e, 0x222e, 0x2231, 0x2232, 0x2233, 0x2234, 0x2235, 0x2236, 0x2237, 0x2238, 0x2239, 0x223a, 0x223b, 0x223c, 0x223d, 0x223e, 0x223f, }; static const unsigned short gNormalizeTable2240[] = { /* U+2240 */ 0x2240, 0x223c, 0x2242, 0x2243, 0x2243, 0x2245, 0x2246, 0x2245, 0x2248, 0x2248, 0x224a, 0x224b, 0x224c, 0x224d, 0x224e, 0x224f, 0x2250, 0x2251, 0x2252, 0x2253, 0x2254, 0x2255, 0x2256, 0x2257, 0x2258, 0x2259, 0x225a, 0x225b, 0x225c, 0x225d, 0x225e, 0x225f, 0x003d, 0x2261, 0x2261, 0x2263, 0x2264, 0x2265, 0x2266, 0x2267, 0x2268, 0x2269, 0x226a, 0x226b, 0x226c, 0x224d, 0x003c, 0x003e, 0x2264, 0x2265, 0x2272, 0x2273, 0x2272, 0x2273, 0x2276, 0x2277, 0x2276, 0x2277, 0x227a, 0x227b, 0x227c, 0x227d, 0x227e, 0x227f, }; static const unsigned short gNormalizeTable2280[] = { /* U+2280 */ 0x227a, 0x227b, 0x2282, 0x2283, 0x2282, 0x2283, 0x2286, 0x2287, 0x2286, 0x2287, 0x228a, 0x228b, 0x228c, 0x228d, 0x228e, 0x228f, 0x2290, 0x2291, 0x2292, 0x2293, 0x2294, 0x2295, 0x2296, 0x2297, 0x2298, 0x2299, 0x229a, 0x229b, 0x229c, 0x229d, 0x229e, 0x229f, 0x22a0, 0x22a1, 0x22a2, 0x22a3, 0x22a4, 0x22a5, 0x22a6, 0x22a7, 0x22a8, 0x22a9, 0x22aa, 0x22ab, 0x22a2, 0x22a8, 0x22a9, 0x22ab, 0x22b0, 0x22b1, 0x22b2, 0x22b3, 0x22b4, 0x22b5, 0x22b6, 0x22b7, 0x22b8, 0x22b9, 0x22ba, 0x22bb, 0x22bc, 0x22bd, 0x22be, 0x22bf, }; static const unsigned short gNormalizeTable22c0[] = { /* U+22c0 */ 0x22c0, 0x22c1, 0x22c2, 0x22c3, 0x22c4, 0x22c5, 0x22c6, 0x22c7, 0x22c8, 0x22c9, 0x22ca, 0x22cb, 0x22cc, 0x22cd, 0x22ce, 0x22cf, 0x22d0, 0x22d1, 0x22d2, 0x22d3, 0x22d4, 0x22d5, 0x22d6, 0x22d7, 0x22d8, 0x22d9, 0x22da, 0x22db, 0x22dc, 0x22dd, 0x22de, 0x22df, 0x227c, 0x227d, 0x2291, 0x2292, 0x22e4, 0x22e5, 0x22e6, 0x22e7, 0x22e8, 0x22e9, 0x22b2, 0x22b3, 0x22b4, 0x22b5, 0x22ee, 0x22ef, 0x22f0, 0x22f1, 0x22f2, 0x22f3, 0x22f4, 0x22f5, 0x22f6, 0x22f7, 0x22f8, 0x22f9, 0x22fa, 0x22fb, 0x22fc, 0x22fd, 0x22fe, 0x22ff, }; static const unsigned short gNormalizeTable2300[] = { /* U+2300 */ 0x2300, 0x2301, 0x2302, 0x2303, 0x2304, 0x2305, 0x2306, 0x2307, 0x2308, 0x2309, 0x230a, 0x230b, 0x230c, 0x230d, 0x230e, 0x230f, 0x2310, 0x2311, 0x2312, 0x2313, 0x2314, 0x2315, 0x2316, 0x2317, 0x2318, 0x2319, 0x231a, 0x231b, 0x231c, 0x231d, 0x231e, 0x231f, 0x2320, 0x2321, 0x2322, 0x2323, 0x2324, 0x2325, 0x2326, 0x2327, 0x2328, 0x3008, 0x3009, 0x232b, 0x232c, 0x232d, 0x232e, 0x232f, 0x2330, 0x2331, 0x2332, 0x2333, 0x2334, 0x2335, 0x2336, 0x2337, 0x2338, 0x2339, 0x233a, 0x233b, 0x233c, 0x233d, 0x233e, 0x233f, }; static const unsigned short gNormalizeTable2440[] = { /* U+2440 */ 0x2440, 0x2441, 0x2442, 0x2443, 0x2444, 0x2445, 0x2446, 0x2447, 0x2448, 0x2449, 0x244a, 0x244b, 0x244c, 0x244d, 0x244e, 0x244f, 0x2450, 0x2451, 0x2452, 0x2453, 0x2454, 0x2455, 0x2456, 0x2457, 0x2458, 0x2459, 0x245a, 0x245b, 0x245c, 0x245d, 0x245e, 0x245f, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, 0x0039, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0032, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, }; static const unsigned short gNormalizeTable2480[] = { /* U+2480 */ 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, 0x0039, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0032, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, 0x0068, 0x0069, 0x006a, }; static const unsigned short gNormalizeTable24c0[] = { /* U+24c0 */ 0x006b, 0x006c, 0x006d, 0x006e, 0x006f, 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078, 0x0079, 0x007a, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, 0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f, 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078, 0x0079, 0x007a, 0x0030, 0x24eb, 0x24ec, 0x24ed, 0x24ee, 0x24ef, 0x24f0, 0x24f1, 0x24f2, 0x24f3, 0x24f4, 0x24f5, 0x24f6, 0x24f7, 0x24f8, 0x24f9, 0x24fa, 0x24fb, 0x24fc, 0x24fd, 0x24fe, 0x24ff, }; static const unsigned short gNormalizeTable2a00[] = { /* U+2a00 */ 0x2a00, 0x2a01, 0x2a02, 0x2a03, 0x2a04, 0x2a05, 0x2a06, 0x2a07, 0x2a08, 0x2a09, 0x2a0a, 0x2a0b, 0x222b, 0x2a0d, 0x2a0e, 0x2a0f, 0x2a10, 0x2a11, 0x2a12, 0x2a13, 0x2a14, 0x2a15, 0x2a16, 0x2a17, 0x2a18, 0x2a19, 0x2a1a, 0x2a1b, 0x2a1c, 0x2a1d, 0x2a1e, 0x2a1f, 0x2a20, 0x2a21, 0x2a22, 0x2a23, 0x2a24, 0x2a25, 0x2a26, 0x2a27, 0x2a28, 0x2a29, 0x2a2a, 0x2a2b, 0x2a2c, 0x2a2d, 0x2a2e, 0x2a2f, 0x2a30, 0x2a31, 0x2a32, 0x2a33, 0x2a34, 0x2a35, 0x2a36, 0x2a37, 0x2a38, 0x2a39, 0x2a3a, 0x2a3b, 0x2a3c, 0x2a3d, 0x2a3e, 0x2a3f, }; static const unsigned short gNormalizeTable2a40[] = { /* U+2a40 */ 0x2a40, 0x2a41, 0x2a42, 0x2a43, 0x2a44, 0x2a45, 0x2a46, 0x2a47, 0x2a48, 0x2a49, 0x2a4a, 0x2a4b, 0x2a4c, 0x2a4d, 0x2a4e, 0x2a4f, 0x2a50, 0x2a51, 0x2a52, 0x2a53, 0x2a54, 0x2a55, 0x2a56, 0x2a57, 0x2a58, 0x2a59, 0x2a5a, 0x2a5b, 0x2a5c, 0x2a5d, 0x2a5e, 0x2a5f, 0x2a60, 0x2a61, 0x2a62, 0x2a63, 0x2a64, 0x2a65, 0x2a66, 0x2a67, 0x2a68, 0x2a69, 0x2a6a, 0x2a6b, 0x2a6c, 0x2a6d, 0x2a6e, 0x2a6f, 0x2a70, 0x2a71, 0x2a72, 0x2a73, 0x003a, 0x003d, 0x003d, 0x2a77, 0x2a78, 0x2a79, 0x2a7a, 0x2a7b, 0x2a7c, 0x2a7d, 0x2a7e, 0x2a7f, }; static const unsigned short gNormalizeTable2ac0[] = { /* U+2ac0 */ 0x2ac0, 0x2ac1, 0x2ac2, 0x2ac3, 0x2ac4, 0x2ac5, 0x2ac6, 0x2ac7, 0x2ac8, 0x2ac9, 0x2aca, 0x2acb, 0x2acc, 0x2acd, 0x2ace, 0x2acf, 0x2ad0, 0x2ad1, 0x2ad2, 0x2ad3, 0x2ad4, 0x2ad5, 0x2ad6, 0x2ad7, 0x2ad8, 0x2ad9, 0x2ada, 0x2adb, 0x2add, 0x2add, 0x2ade, 0x2adf, 0x2ae0, 0x2ae1, 0x2ae2, 0x2ae3, 0x2ae4, 0x2ae5, 0x2ae6, 0x2ae7, 0x2ae8, 0x2ae9, 0x2aea, 0x2aeb, 0x2aec, 0x2aed, 0x2aee, 0x2aef, 0x2af0, 0x2af1, 0x2af2, 0x2af3, 0x2af4, 0x2af5, 0x2af6, 0x2af7, 0x2af8, 0x2af9, 0x2afa, 0x2afb, 0x2afc, 0x2afd, 0x2afe, 0x2aff, }; static const unsigned short gNormalizeTable2c00[] = { /* U+2c00 */ 0x2c30, 0x2c31, 0x2c32, 0x2c33, 0x2c34, 0x2c35, 0x2c36, 0x2c37, 0x2c38, 0x2c39, 0x2c3a, 0x2c3b, 0x2c3c, 0x2c3d, 0x2c3e, 0x2c3f, 0x2c40, 0x2c41, 0x2c42, 0x2c43, 0x2c44, 0x2c45, 0x2c46, 0x2c47, 0x2c48, 0x2c49, 0x2c4a, 0x2c4b, 0x2c4c, 0x2c4d, 0x2c4e, 0x2c4f, 0x2c50, 0x2c51, 0x2c52, 0x2c53, 0x2c54, 0x2c55, 0x2c56, 0x2c57, 0x2c58, 0x2c59, 0x2c5a, 0x2c5b, 0x2c5c, 0x2c5d, 0x2c5e, 0x2c2f, 0x2c30, 0x2c31, 0x2c32, 0x2c33, 0x2c34, 0x2c35, 0x2c36, 0x2c37, 0x2c38, 0x2c39, 0x2c3a, 0x2c3b, 0x2c3c, 0x2c3d, 0x2c3e, 0x2c3f, }; static const unsigned short gNormalizeTable2c40[] = { /* U+2c40 */ 0x2c40, 0x2c41, 0x2c42, 0x2c43, 0x2c44, 0x2c45, 0x2c46, 0x2c47, 0x2c48, 0x2c49, 0x2c4a, 0x2c4b, 0x2c4c, 0x2c4d, 0x2c4e, 0x2c4f, 0x2c50, 0x2c51, 0x2c52, 0x2c53, 0x2c54, 0x2c55, 0x2c56, 0x2c57, 0x2c58, 0x2c59, 0x2c5a, 0x2c5b, 0x2c5c, 0x2c5d, 0x2c5e, 0x2c5f, 0x2c61, 0x2c61, 0x026b, 0x1d7d, 0x027d, 0x2c65, 0x2c66, 0x2c68, 0x2c68, 0x2c6a, 0x2c6a, 0x2c6c, 0x2c6c, 0x0251, 0x0271, 0x0250, 0x0252, 0x2c71, 0x2c73, 0x2c73, 0x2c74, 0x2c76, 0x2c76, 0x2c77, 0x2c78, 0x2c79, 0x2c7a, 0x2c7b, 0x006a, 0x0076, 0x023f, 0x0240, }; static const unsigned short gNormalizeTable2c80[] = { /* U+2c80 */ 0x2c81, 0x2c81, 0x2c83, 0x2c83, 0x2c85, 0x2c85, 0x2c87, 0x2c87, 0x2c89, 0x2c89, 0x2c8b, 0x2c8b, 0x2c8d, 0x2c8d, 0x2c8f, 0x2c8f, 0x2c91, 0x2c91, 0x2c93, 0x2c93, 0x2c95, 0x2c95, 0x2c97, 0x2c97, 0x2c99, 0x2c99, 0x2c9b, 0x2c9b, 0x2c9d, 0x2c9d, 0x2c9f, 0x2c9f, 0x2ca1, 0x2ca1, 0x2ca3, 0x2ca3, 0x2ca5, 0x2ca5, 0x2ca7, 0x2ca7, 0x2ca9, 0x2ca9, 0x2cab, 0x2cab, 0x2cad, 0x2cad, 0x2caf, 0x2caf, 0x2cb1, 0x2cb1, 0x2cb3, 0x2cb3, 0x2cb5, 0x2cb5, 0x2cb7, 0x2cb7, 0x2cb9, 0x2cb9, 0x2cbb, 0x2cbb, 0x2cbd, 0x2cbd, 0x2cbf, 0x2cbf, }; static const unsigned short gNormalizeTable2cc0[] = { /* U+2cc0 */ 0x2cc1, 0x2cc1, 0x2cc3, 0x2cc3, 0x2cc5, 0x2cc5, 0x2cc7, 0x2cc7, 0x2cc9, 0x2cc9, 0x2ccb, 0x2ccb, 0x2ccd, 0x2ccd, 0x2ccf, 0x2ccf, 0x2cd1, 0x2cd1, 0x2cd3, 0x2cd3, 0x2cd5, 0x2cd5, 0x2cd7, 0x2cd7, 0x2cd9, 0x2cd9, 0x2cdb, 0x2cdb, 0x2cdd, 0x2cdd, 0x2cdf, 0x2cdf, 0x2ce1, 0x2ce1, 0x2ce3, 0x2ce3, 0x2ce4, 0x2ce5, 0x2ce6, 0x2ce7, 0x2ce8, 0x2ce9, 0x2cea, 0x2cec, 0x2cec, 0x2cee, 0x2cee, 0x2cef, 0x2cf0, 0x2cf1, 0x2cf2, 0x2cf3, 0x2cf4, 0x2cf5, 0x2cf6, 0x2cf7, 0x2cf8, 0x2cf9, 0x2cfa, 0x2cfb, 0x2cfc, 0x2cfd, 0x2cfe, 0x2cff, }; static const unsigned short gNormalizeTable2d40[] = { /* U+2d40 */ 0x2d40, 0x2d41, 0x2d42, 0x2d43, 0x2d44, 0x2d45, 0x2d46, 0x2d47, 0x2d48, 0x2d49, 0x2d4a, 0x2d4b, 0x2d4c, 0x2d4d, 0x2d4e, 0x2d4f, 0x2d50, 0x2d51, 0x2d52, 0x2d53, 0x2d54, 0x2d55, 0x2d56, 0x2d57, 0x2d58, 0x2d59, 0x2d5a, 0x2d5b, 0x2d5c, 0x2d5d, 0x2d5e, 0x2d5f, 0x2d60, 0x2d61, 0x2d62, 0x2d63, 0x2d64, 0x2d65, 0x2d66, 0x2d67, 0x2d68, 0x2d69, 0x2d6a, 0x2d6b, 0x2d6c, 0x2d6d, 0x2d6e, 0x2d61, 0x2d70, 0x2d71, 0x2d72, 0x2d73, 0x2d74, 0x2d75, 0x2d76, 0x2d77, 0x2d78, 0x2d79, 0x2d7a, 0x2d7b, 0x2d7c, 0x2d7d, 0x2d7e, 0x2d7f, }; static const unsigned short gNormalizeTable2e80[] = { /* U+2e80 */ 0x2e80, 0x2e81, 0x2e82, 0x2e83, 0x2e84, 0x2e85, 0x2e86, 0x2e87, 0x2e88, 0x2e89, 0x2e8a, 0x2e8b, 0x2e8c, 0x2e8d, 0x2e8e, 0x2e8f, 0x2e90, 0x2e91, 0x2e92, 0x2e93, 0x2e94, 0x2e95, 0x2e96, 0x2e97, 0x2e98, 0x2e99, 0x2e9a, 0x2e9b, 0x2e9c, 0x2e9d, 0x2e9e, 0x6bcd, 0x2ea0, 0x2ea1, 0x2ea2, 0x2ea3, 0x2ea4, 0x2ea5, 0x2ea6, 0x2ea7, 0x2ea8, 0x2ea9, 0x2eaa, 0x2eab, 0x2eac, 0x2ead, 0x2eae, 0x2eaf, 0x2eb0, 0x2eb1, 0x2eb2, 0x2eb3, 0x2eb4, 0x2eb5, 0x2eb6, 0x2eb7, 0x2eb8, 0x2eb9, 0x2eba, 0x2ebb, 0x2ebc, 0x2ebd, 0x2ebe, 0x2ebf, }; static const unsigned short gNormalizeTable2ec0[] = { /* U+2ec0 */ 0x2ec0, 0x2ec1, 0x2ec2, 0x2ec3, 0x2ec4, 0x2ec5, 0x2ec6, 0x2ec7, 0x2ec8, 0x2ec9, 0x2eca, 0x2ecb, 0x2ecc, 0x2ecd, 0x2ece, 0x2ecf, 0x2ed0, 0x2ed1, 0x2ed2, 0x2ed3, 0x2ed4, 0x2ed5, 0x2ed6, 0x2ed7, 0x2ed8, 0x2ed9, 0x2eda, 0x2edb, 0x2edc, 0x2edd, 0x2ede, 0x2edf, 0x2ee0, 0x2ee1, 0x2ee2, 0x2ee3, 0x2ee4, 0x2ee5, 0x2ee6, 0x2ee7, 0x2ee8, 0x2ee9, 0x2eea, 0x2eeb, 0x2eec, 0x2eed, 0x2eee, 0x2eef, 0x2ef0, 0x2ef1, 0x2ef2, 0x9f9f, 0x2ef4, 0x2ef5, 0x2ef6, 0x2ef7, 0x2ef8, 0x2ef9, 0x2efa, 0x2efb, 0x2efc, 0x2efd, 0x2efe, 0x2eff, }; static const unsigned short gNormalizeTable2f00[] = { /* U+2f00 */ 0x4e00, 0x4e28, 0x4e36, 0x4e3f, 0x4e59, 0x4e85, 0x4e8c, 0x4ea0, 0x4eba, 0x513f, 0x5165, 0x516b, 0x5182, 0x5196, 0x51ab, 0x51e0, 0x51f5, 0x5200, 0x529b, 0x52f9, 0x5315, 0x531a, 0x5338, 0x5341, 0x535c, 0x5369, 0x5382, 0x53b6, 0x53c8, 0x53e3, 0x56d7, 0x571f, 0x58eb, 0x5902, 0x590a, 0x5915, 0x5927, 0x5973, 0x5b50, 0x5b80, 0x5bf8, 0x5c0f, 0x5c22, 0x5c38, 0x5c6e, 0x5c71, 0x5ddb, 0x5de5, 0x5df1, 0x5dfe, 0x5e72, 0x5e7a, 0x5e7f, 0x5ef4, 0x5efe, 0x5f0b, 0x5f13, 0x5f50, 0x5f61, 0x5f73, 0x5fc3, 0x6208, 0x6236, 0x624b, }; static const unsigned short gNormalizeTable2f40[] = { /* U+2f40 */ 0x652f, 0x6534, 0x6587, 0x6597, 0x65a4, 0x65b9, 0x65e0, 0x65e5, 0x66f0, 0x6708, 0x6728, 0x6b20, 0x6b62, 0x6b79, 0x6bb3, 0x6bcb, 0x6bd4, 0x6bdb, 0x6c0f, 0x6c14, 0x6c34, 0x706b, 0x722a, 0x7236, 0x723b, 0x723f, 0x7247, 0x7259, 0x725b, 0x72ac, 0x7384, 0x7389, 0x74dc, 0x74e6, 0x7518, 0x751f, 0x7528, 0x7530, 0x758b, 0x7592, 0x7676, 0x767d, 0x76ae, 0x76bf, 0x76ee, 0x77db, 0x77e2, 0x77f3, 0x793a, 0x79b8, 0x79be, 0x7a74, 0x7acb, 0x7af9, 0x7c73, 0x7cf8, 0x7f36, 0x7f51, 0x7f8a, 0x7fbd, 0x8001, 0x800c, 0x8012, 0x8033, }; static const unsigned short gNormalizeTable2f80[] = { /* U+2f80 */ 0x807f, 0x8089, 0x81e3, 0x81ea, 0x81f3, 0x81fc, 0x820c, 0x821b, 0x821f, 0x826e, 0x8272, 0x8278, 0x864d, 0x866b, 0x8840, 0x884c, 0x8863, 0x897e, 0x898b, 0x89d2, 0x8a00, 0x8c37, 0x8c46, 0x8c55, 0x8c78, 0x8c9d, 0x8d64, 0x8d70, 0x8db3, 0x8eab, 0x8eca, 0x8f9b, 0x8fb0, 0x8fb5, 0x9091, 0x9149, 0x91c6, 0x91cc, 0x91d1, 0x9577, 0x9580, 0x961c, 0x96b6, 0x96b9, 0x96e8, 0x9751, 0x975e, 0x9762, 0x9769, 0x97cb, 0x97ed, 0x97f3, 0x9801, 0x98a8, 0x98db, 0x98df, 0x9996, 0x9999, 0x99ac, 0x9aa8, 0x9ad8, 0x9adf, 0x9b25, 0x9b2f, }; static const unsigned short gNormalizeTable2fc0[] = { /* U+2fc0 */ 0x9b32, 0x9b3c, 0x9b5a, 0x9ce5, 0x9e75, 0x9e7f, 0x9ea5, 0x9ebb, 0x9ec3, 0x9ecd, 0x9ed1, 0x9ef9, 0x9efd, 0x9f0e, 0x9f13, 0x9f20, 0x9f3b, 0x9f4a, 0x9f52, 0x9f8d, 0x9f9c, 0x9fa0, 0x2fd6, 0x2fd7, 0x2fd8, 0x2fd9, 0x2fda, 0x2fdb, 0x2fdc, 0x2fdd, 0x2fde, 0x2fdf, 0x2fe0, 0x2fe1, 0x2fe2, 0x2fe3, 0x2fe4, 0x2fe5, 0x2fe6, 0x2fe7, 0x2fe8, 0x2fe9, 0x2fea, 0x2feb, 0x2fec, 0x2fed, 0x2fee, 0x2fef, 0x2ff0, 0x2ff1, 0x2ff2, 0x2ff3, 0x2ff4, 0x2ff5, 0x2ff6, 0x2ff7, 0x2ff8, 0x2ff9, 0x2ffa, 0x2ffb, 0x2ffc, 0x2ffd, 0x2ffe, 0x2fff, }; static const unsigned short gNormalizeTable3000[] = { /* U+3000 */ 0x0020, 0x3001, 0x3002, 0x3003, 0x3004, 0x3005, 0x3006, 0x3007, 0x3008, 0x3009, 0x300a, 0x300b, 0x300c, 0x300d, 0x300e, 0x300f, 0x3010, 0x3011, 0x3012, 0x3013, 0x3014, 0x3015, 0x3016, 0x3017, 0x3018, 0x3019, 0x301a, 0x301b, 0x301c, 0x301d, 0x301e, 0x301f, 0x3020, 0x3021, 0x3022, 0x3023, 0x3024, 0x3025, 0x3026, 0x3027, 0x3028, 0x3029, 0x302a, 0x302b, 0x302c, 0x302d, 0x302e, 0x302f, 0x3030, 0x3031, 0x3032, 0x3033, 0x3034, 0x3035, 0x3012, 0x3037, 0x5341, 0x5344, 0x5345, 0x303b, 0x303c, 0x303d, 0x303e, 0x303f, }; static const unsigned short gNormalizeTable3040[] = { /* U+3040 */ 0x3040, 0x3041, 0x3042, 0x3043, 0x3044, 0x3045, 0x3046, 0x3047, 0x3048, 0x3049, 0x304a, 0x304b, 0x304b, 0x304d, 0x304d, 0x304f, 0x304f, 0x3051, 0x3051, 0x3053, 0x3053, 0x3055, 0x3055, 0x3057, 0x3057, 0x3059, 0x3059, 0x305b, 0x305b, 0x305d, 0x305d, 0x305f, 0x305f, 0x3061, 0x3061, 0x3063, 0x3064, 0x3064, 0x3066, 0x3066, 0x3068, 0x3068, 0x306a, 0x306b, 0x306c, 0x306d, 0x306e, 0x306f, 0x306f, 0x306f, 0x3072, 0x3072, 0x3072, 0x3075, 0x3075, 0x3075, 0x3078, 0x3078, 0x3078, 0x307b, 0x307b, 0x307b, 0x307e, 0x307f, }; static const unsigned short gNormalizeTable3080[] = { /* U+3080 */ 0x3080, 0x3081, 0x3082, 0x3083, 0x3084, 0x3085, 0x3086, 0x3087, 0x3088, 0x3089, 0x308a, 0x308b, 0x308c, 0x308d, 0x308e, 0x308f, 0x3090, 0x3091, 0x3092, 0x3093, 0x3046, 0x3095, 0x3096, 0x3097, 0x3098, 0x3099, 0x309a, 0x0020, 0x0020, 0x309d, 0x309d, 0x3088, 0x30a0, 0x30a1, 0x30a2, 0x30a3, 0x30a4, 0x30a5, 0x30a6, 0x30a7, 0x30a8, 0x30a9, 0x30aa, 0x30ab, 0x30ab, 0x30ad, 0x30ad, 0x30af, 0x30af, 0x30b1, 0x30b1, 0x30b3, 0x30b3, 0x30b5, 0x30b5, 0x30b7, 0x30b7, 0x30b9, 0x30b9, 0x30bb, 0x30bb, 0x30bd, 0x30bd, 0x30bf, }; static const unsigned short gNormalizeTable30c0[] = { /* U+30c0 */ 0x30bf, 0x30c1, 0x30c1, 0x30c3, 0x30c4, 0x30c4, 0x30c6, 0x30c6, 0x30c8, 0x30c8, 0x30ca, 0x30cb, 0x30cc, 0x30cd, 0x30ce, 0x30cf, 0x30cf, 0x30cf, 0x30d2, 0x30d2, 0x30d2, 0x30d5, 0x30d5, 0x30d5, 0x30d8, 0x30d8, 0x30d8, 0x30db, 0x30db, 0x30db, 0x30de, 0x30df, 0x30e0, 0x30e1, 0x30e2, 0x30e3, 0x30e4, 0x30e5, 0x30e6, 0x30e7, 0x30e8, 0x30e9, 0x30ea, 0x30eb, 0x30ec, 0x30ed, 0x30ee, 0x30ef, 0x30f0, 0x30f1, 0x30f2, 0x30f3, 0x30a6, 0x30f5, 0x30f6, 0x30ef, 0x30f0, 0x30f1, 0x30f2, 0x30fb, 0x30fc, 0x30fd, 0x30fd, 0x30b3, }; static const unsigned short gNormalizeTable3100[] = { /* U+3100 */ 0x3100, 0x3101, 0x3102, 0x3103, 0x3104, 0x3105, 0x3106, 0x3107, 0x3108, 0x3109, 0x310a, 0x310b, 0x310c, 0x310d, 0x310e, 0x310f, 0x3110, 0x3111, 0x3112, 0x3113, 0x3114, 0x3115, 0x3116, 0x3117, 0x3118, 0x3119, 0x311a, 0x311b, 0x311c, 0x311d, 0x311e, 0x311f, 0x3120, 0x3121, 0x3122, 0x3123, 0x3124, 0x3125, 0x3126, 0x3127, 0x3128, 0x3129, 0x312a, 0x312b, 0x312c, 0x312d, 0x312e, 0x312f, 0x3130, 0x1100, 0x1101, 0x11aa, 0x1102, 0x11ac, 0x11ad, 0x1103, 0x1104, 0x1105, 0x11b0, 0x11b1, 0x11b2, 0x11b3, 0x11b4, 0x11b5, }; static const unsigned short gNormalizeTable3140[] = { /* U+3140 */ 0x111a, 0x1106, 0x1107, 0x1108, 0x1121, 0x1109, 0x110a, 0x110b, 0x110c, 0x110d, 0x110e, 0x110f, 0x1110, 0x1111, 0x1112, 0x1161, 0x1162, 0x1163, 0x1164, 0x1165, 0x1166, 0x1167, 0x1168, 0x1169, 0x116a, 0x116b, 0x116c, 0x116d, 0x116e, 0x116f, 0x1170, 0x1171, 0x1172, 0x1173, 0x1174, 0x1175, 0x0020, 0x1114, 0x1115, 0x11c7, 0x11c8, 0x11cc, 0x11ce, 0x11d3, 0x11d7, 0x11d9, 0x111c, 0x11dd, 0x11df, 0x111d, 0x111e, 0x1120, 0x1122, 0x1123, 0x1127, 0x1129, 0x112b, 0x112c, 0x112d, 0x112e, 0x112f, 0x1132, 0x1136, 0x1140, }; static const unsigned short gNormalizeTable3180[] = { /* U+3180 */ 0x1147, 0x114c, 0x11f1, 0x11f2, 0x1157, 0x1158, 0x1159, 0x1184, 0x1185, 0x1188, 0x1191, 0x1192, 0x1194, 0x119e, 0x11a1, 0x318f, 0x3190, 0x3191, 0x4e00, 0x4e8c, 0x4e09, 0x56db, 0x4e0a, 0x4e2d, 0x4e0b, 0x7532, 0x4e59, 0x4e19, 0x4e01, 0x5929, 0x5730, 0x4eba, 0x31a0, 0x31a1, 0x31a2, 0x31a3, 0x31a4, 0x31a5, 0x31a6, 0x31a7, 0x31a8, 0x31a9, 0x31aa, 0x31ab, 0x31ac, 0x31ad, 0x31ae, 0x31af, 0x31b0, 0x31b1, 0x31b2, 0x31b3, 0x31b4, 0x31b5, 0x31b6, 0x31b7, 0x31b8, 0x31b9, 0x31ba, 0x31bb, 0x31bc, 0x31bd, 0x31be, 0x31bf, }; static const unsigned short gNormalizeTable3200[] = { /* U+3200 */ 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x321f, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, 0x0028, }; static const unsigned short gNormalizeTable3240[] = { /* U+3240 */ 0x0028, 0x0028, 0x0028, 0x0028, 0x554f, 0x5e7c, 0x6587, 0x7b8f, 0x3248, 0x3249, 0x324a, 0x324b, 0x324c, 0x324d, 0x324e, 0x324f, 0x0070, 0x0032, 0x0032, 0x0032, 0x0032, 0x0032, 0x0032, 0x0032, 0x0032, 0x0032, 0x0033, 0x0033, 0x0033, 0x0033, 0x0033, 0x0033, 0x1100, 0x1102, 0x1103, 0x1105, 0x1106, 0x1107, 0x1109, 0x110b, 0x110c, 0x110e, 0x110f, 0x1110, 0x1111, 0x1112, 0x1100, 0x1102, 0x1103, 0x1105, 0x1106, 0x1107, 0x1109, 0x110b, 0x110c, 0x110e, 0x110f, 0x1110, 0x1111, 0x1112, 0x110e, 0x110c, 0x110b, 0x327f, }; static const unsigned short gNormalizeTable3280[] = { /* U+3280 */ 0x4e00, 0x4e8c, 0x4e09, 0x56db, 0x4e94, 0x516d, 0x4e03, 0x516b, 0x4e5d, 0x5341, 0x6708, 0x706b, 0x6c34, 0x6728, 0x91d1, 0x571f, 0x65e5, 0x682a, 0x6709, 0x793e, 0x540d, 0x7279, 0x8ca1, 0x795d, 0x52b4, 0x79d8, 0x7537, 0x5973, 0x9069, 0x512a, 0x5370, 0x6ce8, 0x9805, 0x4f11, 0x5199, 0x6b63, 0x4e0a, 0x4e2d, 0x4e0b, 0x5de6, 0x53f3, 0x533b, 0x5b97, 0x5b66, 0x76e3, 0x4f01, 0x8cc7, 0x5354, 0x591c, 0x0033, 0x0033, 0x0033, 0x0033, 0x0034, 0x0034, 0x0034, 0x0034, 0x0034, 0x0034, 0x0034, 0x0034, 0x0034, 0x0034, 0x0035, }; static const unsigned short gNormalizeTable32c0[] = { /* U+32c0 */ 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, 0x0039, 0x0031, 0x0031, 0x0031, 0x0068, 0x0065, 0x0065, 0x006c, 0x30a2, 0x30a4, 0x30a6, 0x30a8, 0x30aa, 0x30ab, 0x30ad, 0x30af, 0x30b1, 0x30b3, 0x30b5, 0x30b7, 0x30b9, 0x30bb, 0x30bd, 0x30bf, 0x30c1, 0x30c4, 0x30c6, 0x30c8, 0x30ca, 0x30cb, 0x30cc, 0x30cd, 0x30ce, 0x30cf, 0x30d2, 0x30d5, 0x30d8, 0x30db, 0x30de, 0x30df, 0x30e0, 0x30e1, 0x30e2, 0x30e4, 0x30e6, 0x30e8, 0x30e9, 0x30ea, 0x30eb, 0x30ec, 0x30ed, 0x30ef, 0x30f0, 0x30f1, 0x30f2, 0x32ff, }; static const unsigned short gNormalizeTable3300[] = { /* U+3300 */ 0x30a2, 0x30a2, 0x30a2, 0x30a2, 0x30a4, 0x30a4, 0x30a6, 0x30a8, 0x30a8, 0x30aa, 0x30aa, 0x30ab, 0x30ab, 0x30ab, 0x30ab, 0x30ab, 0x30ad, 0x30ad, 0x30ad, 0x30ad, 0x30ad, 0x30ad, 0x30ad, 0x30ad, 0x30af, 0x30af, 0x30af, 0x30af, 0x30b1, 0x30b3, 0x30b3, 0x30b5, 0x30b5, 0x30b7, 0x30bb, 0x30bb, 0x30bf, 0x30c6, 0x30c8, 0x30c8, 0x30ca, 0x30ce, 0x30cf, 0x30cf, 0x30cf, 0x30cf, 0x30d2, 0x30d2, 0x30d2, 0x30d2, 0x30d5, 0x30d5, 0x30d5, 0x30d5, 0x30d8, 0x30d8, 0x30d8, 0x30d8, 0x30d8, 0x30d8, 0x30d8, 0x30db, 0x30db, 0x30db, }; static const unsigned short gNormalizeTable3340[] = { /* U+3340 */ 0x30db, 0x30db, 0x30db, 0x30de, 0x30de, 0x30de, 0x30de, 0x30de, 0x30df, 0x30df, 0x30df, 0x30e1, 0x30e1, 0x30e1, 0x30e4, 0x30e4, 0x30e6, 0x30ea, 0x30ea, 0x30eb, 0x30eb, 0x30ec, 0x30ec, 0x30ef, 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, 0x0039, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0032, 0x0032, 0x0032, 0x0032, 0x0032, 0x0068, 0x0064, 0x0061, 0x0062, 0x006f, 0x0070, 0x0064, 0x0064, 0x0064, 0x0069, 0x5e73, 0x662d, 0x5927, 0x660e, 0x682a, }; static const unsigned short gNormalizeTable3380[] = { /* U+3380 */ 0x0070, 0x006e, 0x03bc, 0x006d, 0x006b, 0x006b, 0x006d, 0x0067, 0x0063, 0x006b, 0x0070, 0x006e, 0x03bc, 0x03bc, 0x006d, 0x006b, 0x0068, 0x006b, 0x006d, 0x0067, 0x0074, 0x03bc, 0x006d, 0x0064, 0x006b, 0x0066, 0x006e, 0x03bc, 0x006d, 0x0063, 0x006b, 0x006d, 0x0063, 0x006d, 0x006b, 0x006d, 0x0063, 0x006d, 0x006b, 0x006d, 0x006d, 0x0070, 0x006b, 0x006d, 0x0067, 0x0072, 0x0072, 0x0072, 0x0070, 0x006e, 0x03bc, 0x006d, 0x0070, 0x006e, 0x03bc, 0x006d, 0x006b, 0x006d, 0x0070, 0x006e, 0x03bc, 0x006d, 0x006b, 0x006d, }; static const unsigned short gNormalizeTable33c0[] = { /* U+33c0 */ 0x006b, 0x006d, 0x0061, 0x0062, 0x0063, 0x0063, 0x0063, 0x0063, 0x0064, 0x0067, 0x0068, 0x0068, 0x0069, 0x006b, 0x006b, 0x006b, 0x006c, 0x006c, 0x006c, 0x006c, 0x006d, 0x006d, 0x006d, 0x0070, 0x0070, 0x0070, 0x0070, 0x0073, 0x0073, 0x0077, 0x0076, 0x0061, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, 0x0039, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0031, 0x0032, 0x0032, 0x0032, 0x0032, 0x0032, 0x0032, 0x0032, 0x0032, 0x0032, 0x0032, 0x0033, 0x0033, 0x0067, }; static const unsigned short gNormalizeTablea640[] = { /* U+a640 */ 0xa641, 0xa641, 0xa643, 0xa643, 0xa645, 0xa645, 0xa647, 0xa647, 0xa649, 0xa649, 0xa64b, 0xa64b, 0xa64d, 0xa64d, 0xa64f, 0xa64f, 0xa651, 0xa651, 0xa653, 0xa653, 0xa655, 0xa655, 0xa657, 0xa657, 0xa659, 0xa659, 0xa65b, 0xa65b, 0xa65d, 0xa65d, 0xa65f, 0xa65f, 0xa660, 0xa661, 0xa663, 0xa663, 0xa665, 0xa665, 0xa667, 0xa667, 0xa669, 0xa669, 0xa66b, 0xa66b, 0xa66d, 0xa66d, 0xa66e, 0xa66f, 0xa670, 0xa671, 0xa672, 0xa673, 0xa674, 0xa675, 0xa676, 0xa677, 0xa678, 0xa679, 0xa67a, 0xa67b, 0xa67c, 0xa67d, 0xa67e, 0xa67f, }; static const unsigned short gNormalizeTablea680[] = { /* U+a680 */ 0xa681, 0xa681, 0xa683, 0xa683, 0xa685, 0xa685, 0xa687, 0xa687, 0xa689, 0xa689, 0xa68b, 0xa68b, 0xa68d, 0xa68d, 0xa68f, 0xa68f, 0xa691, 0xa691, 0xa693, 0xa693, 0xa695, 0xa695, 0xa697, 0xa697, 0xa698, 0xa699, 0xa69a, 0xa69b, 0xa69c, 0xa69d, 0xa69e, 0xa69f, 0xa6a0, 0xa6a1, 0xa6a2, 0xa6a3, 0xa6a4, 0xa6a5, 0xa6a6, 0xa6a7, 0xa6a8, 0xa6a9, 0xa6aa, 0xa6ab, 0xa6ac, 0xa6ad, 0xa6ae, 0xa6af, 0xa6b0, 0xa6b1, 0xa6b2, 0xa6b3, 0xa6b4, 0xa6b5, 0xa6b6, 0xa6b7, 0xa6b8, 0xa6b9, 0xa6ba, 0xa6bb, 0xa6bc, 0xa6bd, 0xa6be, 0xa6bf, }; static const unsigned short gNormalizeTablea700[] = { /* U+a700 */ 0xa700, 0xa701, 0xa702, 0xa703, 0xa704, 0xa705, 0xa706, 0xa707, 0xa708, 0xa709, 0xa70a, 0xa70b, 0xa70c, 0xa70d, 0xa70e, 0xa70f, 0xa710, 0xa711, 0xa712, 0xa713, 0xa714, 0xa715, 0xa716, 0xa717, 0xa718, 0xa719, 0xa71a, 0xa71b, 0xa71c, 0xa71d, 0xa71e, 0xa71f, 0xa720, 0xa721, 0xa723, 0xa723, 0xa725, 0xa725, 0xa727, 0xa727, 0xa729, 0xa729, 0xa72b, 0xa72b, 0xa72d, 0xa72d, 0xa72f, 0xa72f, 0xa730, 0xa731, 0xa733, 0xa733, 0xa735, 0xa735, 0xa737, 0xa737, 0xa739, 0xa739, 0xa73b, 0xa73b, 0xa73d, 0xa73d, 0xa73f, 0xa73f, }; static const unsigned short gNormalizeTablea740[] = { /* U+a740 */ 0xa741, 0xa741, 0xa743, 0xa743, 0xa745, 0xa745, 0xa747, 0xa747, 0xa749, 0xa749, 0xa74b, 0xa74b, 0xa74d, 0xa74d, 0xa74f, 0xa74f, 0xa751, 0xa751, 0xa753, 0xa753, 0xa755, 0xa755, 0xa757, 0xa757, 0xa759, 0xa759, 0xa75b, 0xa75b, 0xa75d, 0xa75d, 0xa75f, 0xa75f, 0xa761, 0xa761, 0xa763, 0xa763, 0xa765, 0xa765, 0xa767, 0xa767, 0xa769, 0xa769, 0xa76b, 0xa76b, 0xa76d, 0xa76d, 0xa76f, 0xa76f, 0xa76f, 0xa771, 0xa772, 0xa773, 0xa774, 0xa775, 0xa776, 0xa777, 0xa778, 0xa77a, 0xa77a, 0xa77c, 0xa77c, 0x1d79, 0xa77f, 0xa77f, }; static const unsigned short gNormalizeTablea780[] = { /* U+a780 */ 0xa781, 0xa781, 0xa783, 0xa783, 0xa785, 0xa785, 0xa787, 0xa787, 0xa788, 0xa789, 0xa78a, 0xa78c, 0xa78c, 0xa78d, 0xa78e, 0xa78f, 0xa790, 0xa791, 0xa792, 0xa793, 0xa794, 0xa795, 0xa796, 0xa797, 0xa798, 0xa799, 0xa79a, 0xa79b, 0xa79c, 0xa79d, 0xa79e, 0xa79f, 0xa7a0, 0xa7a1, 0xa7a2, 0xa7a3, 0xa7a4, 0xa7a5, 0xa7a6, 0xa7a7, 0xa7a8, 0xa7a9, 0xa7aa, 0xa7ab, 0xa7ac, 0xa7ad, 0xa7ae, 0xa7af, 0xa7b0, 0xa7b1, 0xa7b2, 0xa7b3, 0xa7b4, 0xa7b5, 0xa7b6, 0xa7b7, 0xa7b8, 0xa7b9, 0xa7ba, 0xa7bb, 0xa7bc, 0xa7bd, 0xa7be, 0xa7bf, }; static const unsigned short gNormalizeTablef900[] = { /* U+f900 */ 0x8c48, 0x66f4, 0x8eca, 0x8cc8, 0x6ed1, 0x4e32, 0x53e5, 0x9f9c, 0x9f9c, 0x5951, 0x91d1, 0x5587, 0x5948, 0x61f6, 0x7669, 0x7f85, 0x863f, 0x87ba, 0x88f8, 0x908f, 0x6a02, 0x6d1b, 0x70d9, 0x73de, 0x843d, 0x916a, 0x99f1, 0x4e82, 0x5375, 0x6b04, 0x721b, 0x862d, 0x9e1e, 0x5d50, 0x6feb, 0x85cd, 0x8964, 0x62c9, 0x81d8, 0x881f, 0x5eca, 0x6717, 0x6d6a, 0x72fc, 0x90ce, 0x4f86, 0x51b7, 0x52de, 0x64c4, 0x6ad3, 0x7210, 0x76e7, 0x8001, 0x8606, 0x865c, 0x8def, 0x9732, 0x9b6f, 0x9dfa, 0x788c, 0x797f, 0x7da0, 0x83c9, 0x9304, }; static const unsigned short gNormalizeTablef940[] = { /* U+f940 */ 0x9e7f, 0x8ad6, 0x58df, 0x5f04, 0x7c60, 0x807e, 0x7262, 0x78ca, 0x8cc2, 0x96f7, 0x58d8, 0x5c62, 0x6a13, 0x6dda, 0x6f0f, 0x7d2f, 0x7e37, 0x964b, 0x52d2, 0x808b, 0x51dc, 0x51cc, 0x7a1c, 0x7dbe, 0x83f1, 0x9675, 0x8b80, 0x62cf, 0x6a02, 0x8afe, 0x4e39, 0x5be7, 0x6012, 0x7387, 0x7570, 0x5317, 0x78fb, 0x4fbf, 0x5fa9, 0x4e0d, 0x6ccc, 0x6578, 0x7d22, 0x53c3, 0x585e, 0x7701, 0x8449, 0x8aaa, 0x6bba, 0x8fb0, 0x6c88, 0x62fe, 0x82e5, 0x63a0, 0x7565, 0x4eae, 0x5169, 0x51c9, 0x6881, 0x7ce7, 0x826f, 0x8ad2, 0x91cf, 0x52f5, }; static const unsigned short gNormalizeTablef980[] = { /* U+f980 */ 0x5442, 0x5973, 0x5eec, 0x65c5, 0x6ffe, 0x792a, 0x95ad, 0x9a6a, 0x9e97, 0x9ece, 0x529b, 0x66c6, 0x6b77, 0x8f62, 0x5e74, 0x6190, 0x6200, 0x649a, 0x6f23, 0x7149, 0x7489, 0x79ca, 0x7df4, 0x806f, 0x8f26, 0x84ee, 0x9023, 0x934a, 0x5217, 0x52a3, 0x54bd, 0x70c8, 0x88c2, 0x8aaa, 0x5ec9, 0x5ff5, 0x637b, 0x6bae, 0x7c3e, 0x7375, 0x4ee4, 0x56f9, 0x5be7, 0x5dba, 0x601c, 0x73b2, 0x7469, 0x7f9a, 0x8046, 0x9234, 0x96f6, 0x9748, 0x9818, 0x4f8b, 0x79ae, 0x91b4, 0x96b8, 0x60e1, 0x4e86, 0x50da, 0x5bee, 0x5c3f, 0x6599, 0x6a02, }; static const unsigned short gNormalizeTablef9c0[] = { /* U+f9c0 */ 0x71ce, 0x7642, 0x84fc, 0x907c, 0x9f8d, 0x6688, 0x962e, 0x5289, 0x677b, 0x67f3, 0x6d41, 0x6e9c, 0x7409, 0x7559, 0x786b, 0x7d10, 0x985e, 0x516d, 0x622e, 0x9678, 0x502b, 0x5d19, 0x6dea, 0x8f2a, 0x5f8b, 0x6144, 0x6817, 0x7387, 0x9686, 0x5229, 0x540f, 0x5c65, 0x6613, 0x674e, 0x68a8, 0x6ce5, 0x7406, 0x75e2, 0x7f79, 0x88cf, 0x88e1, 0x91cc, 0x96e2, 0x533f, 0x6eba, 0x541d, 0x71d0, 0x7498, 0x85fa, 0x96a3, 0x9c57, 0x9e9f, 0x6797, 0x6dcb, 0x81e8, 0x7acb, 0x7b20, 0x7c92, 0x72c0, 0x7099, 0x8b58, 0x4ec0, 0x8336, 0x523a, }; static const unsigned short gNormalizeTablefa00[] = { /* U+fa00 */ 0x5207, 0x5ea6, 0x62d3, 0x7cd6, 0x5b85, 0x6d1e, 0x66b4, 0x8f3b, 0x884c, 0x964d, 0x898b, 0x5ed3, 0x5140, 0x55c0, 0xfa0e, 0xfa0f, 0x585a, 0xfa11, 0x6674, 0xfa13, 0xfa14, 0x51de, 0x732a, 0x76ca, 0x793c, 0x795e, 0x7965, 0x798f, 0x9756, 0x7cbe, 0x7fbd, 0xfa1f, 0x8612, 0xfa21, 0x8af8, 0xfa23, 0xfa24, 0x9038, 0x90fd, 0xfa27, 0xfa28, 0xfa29, 0x98ef, 0x98fc, 0x9928, 0x9db4, 0xfa2e, 0xfa2f, 0x4fae, 0x50e7, 0x514d, 0x52c9, 0x52e4, 0x5351, 0x559d, 0x5606, 0x5668, 0x5840, 0x58a8, 0x5c64, 0x5c6e, 0x6094, 0x6168, 0x618e, }; static const unsigned short gNormalizeTablefa40[] = { /* U+fa40 */ 0x61f2, 0x654f, 0x65e2, 0x6691, 0x6885, 0x6d77, 0x6e1a, 0x6f22, 0x716e, 0x722b, 0x7422, 0x7891, 0x793e, 0x7949, 0x7948, 0x7950, 0x7956, 0x795d, 0x798d, 0x798e, 0x7a40, 0x7a81, 0x7bc0, 0x7df4, 0x7e09, 0x7e41, 0x7f72, 0x8005, 0x81ed, 0x8279, 0x8279, 0x8457, 0x8910, 0x8996, 0x8b01, 0x8b39, 0x8cd3, 0x8d08, 0x8fb6, 0x9038, 0x96e3, 0x97ff, 0x983b, 0x6075, 0xfa6c, 0x8218, 0xfa6e, 0xfa6f, 0x4e26, 0x51b5, 0x5168, 0x4f80, 0x5145, 0x5180, 0x52c7, 0x52fa, 0x559d, 0x5555, 0x5599, 0x55e2, 0x585a, 0x58b3, 0x5944, 0x5954, }; static const unsigned short gNormalizeTablefa80[] = { /* U+fa80 */ 0x5a62, 0x5b28, 0x5ed2, 0x5ed9, 0x5f69, 0x5fad, 0x60d8, 0x614e, 0x6108, 0x618e, 0x6160, 0x61f2, 0x6234, 0x63c4, 0x641c, 0x6452, 0x6556, 0x6674, 0x6717, 0x671b, 0x6756, 0x6b79, 0x6bba, 0x6d41, 0x6edb, 0x6ecb, 0x6f22, 0x701e, 0x716e, 0x77a7, 0x7235, 0x72af, 0x732a, 0x7471, 0x7506, 0x753b, 0x761d, 0x761f, 0x76ca, 0x76db, 0x76f4, 0x774a, 0x7740, 0x78cc, 0x7ab1, 0x7bc0, 0x7c7b, 0x7d5b, 0x7df4, 0x7f3e, 0x8005, 0x8352, 0x83ef, 0x8779, 0x8941, 0x8986, 0x8996, 0x8abf, 0x8af8, 0x8acb, 0x8b01, 0x8afe, 0x8aed, 0x8b39, }; static const unsigned short gNormalizeTablefac0[] = { /* U+fac0 */ 0x8b8a, 0x8d08, 0x8f38, 0x9072, 0x9199, 0x9276, 0x967c, 0x96e3, 0x9756, 0x97db, 0x97ff, 0x980b, 0x983b, 0x9b12, 0x9f9c, 0xfacf, 0xfad0, 0xfad1, 0x3b9d, 0x4018, 0x4039, 0xfad5, 0xfad6, 0xfad7, 0x9f43, 0x9f8e, 0xfada, 0xfadb, 0xfadc, 0xfadd, 0xfade, 0xfadf, 0xfae0, 0xfae1, 0xfae2, 0xfae3, 0xfae4, 0xfae5, 0xfae6, 0xfae7, 0xfae8, 0xfae9, 0xfaea, 0xfaeb, 0xfaec, 0xfaed, 0xfaee, 0xfaef, 0xfaf0, 0xfaf1, 0xfaf2, 0xfaf3, 0xfaf4, 0xfaf5, 0xfaf6, 0xfaf7, 0xfaf8, 0xfaf9, 0xfafa, 0xfafb, 0xfafc, 0xfafd, 0xfafe, 0xfaff, }; static const unsigned short gNormalizeTablefb00[] = { /* U+fb00 */ 0x0066, 0x0066, 0x0066, 0x0066, 0x0066, 0x0073, 0x0073, 0xfb07, 0xfb08, 0xfb09, 0xfb0a, 0xfb0b, 0xfb0c, 0xfb0d, 0xfb0e, 0xfb0f, 0xfb10, 0xfb11, 0xfb12, 0x0574, 0x0574, 0x0574, 0x057e, 0x0574, 0xfb18, 0xfb19, 0xfb1a, 0xfb1b, 0xfb1c, 0x05d9, 0xfb1e, 0x05f2, 0x05e2, 0x05d0, 0x05d3, 0x05d4, 0x05db, 0x05dc, 0x05dd, 0x05e8, 0x05ea, 0x002b, 0x05e9, 0x05e9, 0x05e9, 0x05e9, 0x05d0, 0x05d0, 0x05d0, 0x05d1, 0x05d2, 0x05d3, 0x05d4, 0x05d5, 0x05d6, 0xfb37, 0x05d8, 0x05d9, 0x05da, 0x05db, 0x05dc, 0xfb3d, 0x05de, 0xfb3f, }; static const unsigned short gNormalizeTablefb40[] = { /* U+fb40 */ 0x05e0, 0x05e1, 0xfb42, 0x05e3, 0x05e4, 0xfb45, 0x05e6, 0x05e7, 0x05e8, 0x05e9, 0x05ea, 0x05d5, 0x05d1, 0x05db, 0x05e4, 0x05d0, 0x0671, 0x0671, 0x067b, 0x067b, 0x067b, 0x067b, 0x067e, 0x067e, 0x067e, 0x067e, 0x0680, 0x0680, 0x0680, 0x0680, 0x067a, 0x067a, 0x067a, 0x067a, 0x067f, 0x067f, 0x067f, 0x067f, 0x0679, 0x0679, 0x0679, 0x0679, 0x06a4, 0x06a4, 0x06a4, 0x06a4, 0x06a6, 0x06a6, 0x06a6, 0x06a6, 0x0684, 0x0684, 0x0684, 0x0684, 0x0683, 0x0683, 0x0683, 0x0683, 0x0686, 0x0686, 0x0686, 0x0686, 0x0687, 0x0687, }; static const unsigned short gNormalizeTablefb80[] = { /* U+fb80 */ 0x0687, 0x0687, 0x068d, 0x068d, 0x068c, 0x068c, 0x068e, 0x068e, 0x0688, 0x0688, 0x0698, 0x0698, 0x0691, 0x0691, 0x06a9, 0x06a9, 0x06a9, 0x06a9, 0x06af, 0x06af, 0x06af, 0x06af, 0x06b3, 0x06b3, 0x06b3, 0x06b3, 0x06b1, 0x06b1, 0x06b1, 0x06b1, 0x06ba, 0x06ba, 0x06bb, 0x06bb, 0x06bb, 0x06bb, 0x06d5, 0x06d5, 0x06c1, 0x06c1, 0x06c1, 0x06c1, 0x06be, 0x06be, 0x06be, 0x06be, 0x06d2, 0x06d2, 0x06d2, 0x06d2, 0xfbb2, 0xfbb3, 0xfbb4, 0xfbb5, 0xfbb6, 0xfbb7, 0xfbb8, 0xfbb9, 0xfbba, 0xfbbb, 0xfbbc, 0xfbbd, 0xfbbe, 0xfbbf, }; static const unsigned short gNormalizeTablefbc0[] = { /* U+fbc0 */ 0xfbc0, 0xfbc1, 0xfbc2, 0xfbc3, 0xfbc4, 0xfbc5, 0xfbc6, 0xfbc7, 0xfbc8, 0xfbc9, 0xfbca, 0xfbcb, 0xfbcc, 0xfbcd, 0xfbce, 0xfbcf, 0xfbd0, 0xfbd1, 0xfbd2, 0x06ad, 0x06ad, 0x06ad, 0x06ad, 0x06c7, 0x06c7, 0x06c6, 0x06c6, 0x06c8, 0x06c8, 0x06c7, 0x06cb, 0x06cb, 0x06c5, 0x06c5, 0x06c9, 0x06c9, 0x06d0, 0x06d0, 0x06d0, 0x06d0, 0x0649, 0x0649, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x06cc, 0x06cc, 0x06cc, 0x06cc, }; static const unsigned short gNormalizeTablefc00[] = { /* U+fc00 */ 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x0628, 0x0628, 0x0628, 0x0628, 0x0628, 0x0628, 0x062a, 0x062a, 0x062a, 0x062a, 0x062a, 0x062a, 0x062b, 0x062b, 0x062b, 0x062b, 0x062c, 0x062c, 0x062d, 0x062d, 0x062e, 0x062e, 0x062e, 0x0633, 0x0633, 0x0633, 0x0633, 0x0635, 0x0635, 0x0636, 0x0636, 0x0636, 0x0636, 0x0637, 0x0637, 0x0638, 0x0639, 0x0639, 0x063a, 0x063a, 0x0641, 0x0641, 0x0641, 0x0641, 0x0641, 0x0641, 0x0642, 0x0642, 0x0642, 0x0642, 0x0643, 0x0643, 0x0643, 0x0643, 0x0643, 0x0643, 0x0643, 0x0643, 0x0644, }; static const unsigned short gNormalizeTablefc40[] = { /* U+fc40 */ 0x0644, 0x0644, 0x0644, 0x0644, 0x0644, 0x0645, 0x0645, 0x0645, 0x0645, 0x0645, 0x0645, 0x0646, 0x0646, 0x0646, 0x0646, 0x0646, 0x0646, 0x0647, 0x0647, 0x0647, 0x0647, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x0630, 0x0631, 0x0649, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x0628, 0x0628, 0x0628, 0x0628, 0x0628, 0x0628, 0x062a, 0x062a, 0x062a, 0x062a, 0x062a, 0x062a, 0x062b, 0x062b, 0x062b, 0x062b, 0x062b, 0x062b, 0x0641, 0x0641, 0x0642, 0x0642, }; static const unsigned short gNormalizeTablefc80[] = { /* U+fc80 */ 0x0643, 0x0643, 0x0643, 0x0643, 0x0643, 0x0644, 0x0644, 0x0644, 0x0645, 0x0645, 0x0646, 0x0646, 0x0646, 0x0646, 0x0646, 0x0646, 0x0649, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x0628, 0x0628, 0x0628, 0x0628, 0x0628, 0x062a, 0x062a, 0x062a, 0x062a, 0x062a, 0x062b, 0x062c, 0x062c, 0x062d, 0x062d, 0x062e, 0x062e, 0x0633, 0x0633, 0x0633, 0x0633, 0x0635, 0x0635, 0x0635, 0x0636, 0x0636, 0x0636, 0x0636, 0x0637, 0x0638, 0x0639, 0x0639, 0x063a, 0x063a, 0x0641, 0x0641, }; static const unsigned short gNormalizeTablefcc0[] = { /* U+fcc0 */ 0x0641, 0x0641, 0x0642, 0x0642, 0x0643, 0x0643, 0x0643, 0x0643, 0x0643, 0x0644, 0x0644, 0x0644, 0x0644, 0x0644, 0x0645, 0x0645, 0x0645, 0x0645, 0x0646, 0x0646, 0x0646, 0x0646, 0x0646, 0x0647, 0x0647, 0x0647, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x064a, 0x0628, 0x0628, 0x062a, 0x062a, 0x062b, 0x062b, 0x0633, 0x0633, 0x0634, 0x0634, 0x0643, 0x0643, 0x0644, 0x0646, 0x0646, 0x064a, 0x064a, 0x0640, 0x0640, 0x0640, 0x0637, 0x0637, 0x0639, 0x0639, 0x063a, 0x063a, 0x0633, 0x0633, 0x0634, 0x0634, 0x062d, }; static const unsigned short gNormalizeTablefd00[] = { /* U+fd00 */ 0x062d, 0x062c, 0x062c, 0x062e, 0x062e, 0x0635, 0x0635, 0x0636, 0x0636, 0x0634, 0x0634, 0x0634, 0x0634, 0x0634, 0x0633, 0x0635, 0x0636, 0x0637, 0x0637, 0x0639, 0x0639, 0x063a, 0x063a, 0x0633, 0x0633, 0x0634, 0x0634, 0x062d, 0x062d, 0x062c, 0x062c, 0x062e, 0x062e, 0x0635, 0x0635, 0x0636, 0x0636, 0x0634, 0x0634, 0x0634, 0x0634, 0x0634, 0x0633, 0x0635, 0x0636, 0x0634, 0x0634, 0x0634, 0x0634, 0x0633, 0x0634, 0x0637, 0x0633, 0x0633, 0x0633, 0x0634, 0x0634, 0x0634, 0x0637, 0x0638, 0x0627, 0x0627, 0xfd3e, 0xfd3f, }; static const unsigned short gNormalizeTablefd40[] = { /* U+fd40 */ 0xfd40, 0xfd41, 0xfd42, 0xfd43, 0xfd44, 0xfd45, 0xfd46, 0xfd47, 0xfd48, 0xfd49, 0xfd4a, 0xfd4b, 0xfd4c, 0xfd4d, 0xfd4e, 0xfd4f, 0x062a, 0x062a, 0x062a, 0x062a, 0x062a, 0x062a, 0x062a, 0x062a, 0x062c, 0x062c, 0x062d, 0x062d, 0x0633, 0x0633, 0x0633, 0x0633, 0x0633, 0x0633, 0x0633, 0x0633, 0x0635, 0x0635, 0x0635, 0x0634, 0x0634, 0x0634, 0x0634, 0x0634, 0x0634, 0x0634, 0x0636, 0x0636, 0x0636, 0x0637, 0x0637, 0x0637, 0x0637, 0x0639, 0x0639, 0x0639, 0x0639, 0x063a, 0x063a, 0x063a, 0x0641, 0x0641, 0x0642, 0x0642, }; static const unsigned short gNormalizeTablefd80[] = { /* U+fd80 */ 0x0644, 0x0644, 0x0644, 0x0644, 0x0644, 0x0644, 0x0644, 0x0644, 0x0644, 0x0645, 0x0645, 0x0645, 0x0645, 0x0645, 0x0645, 0x0645, 0xfd90, 0xfd91, 0x0645, 0x0647, 0x0647, 0x0646, 0x0646, 0x0646, 0x0646, 0x0646, 0x0646, 0x0646, 0x064a, 0x064a, 0x0628, 0x062a, 0x062a, 0x062a, 0x062a, 0x062a, 0x062a, 0x062c, 0x062c, 0x062c, 0x0633, 0x0635, 0x0634, 0x0636, 0x0644, 0x0644, 0x064a, 0x064a, 0x064a, 0x0645, 0x0642, 0x0646, 0x0642, 0x0644, 0x0639, 0x0643, 0x0646, 0x0645, 0x0644, 0x0643, 0x0644, 0x0646, 0x062c, 0x062d, }; static const unsigned short gNormalizeTablefdc0[] = { /* U+fdc0 */ 0x0645, 0x0641, 0x0628, 0x0643, 0x0639, 0x0635, 0x0633, 0x0646, 0xfdc8, 0xfdc9, 0xfdca, 0xfdcb, 0xfdcc, 0xfdcd, 0xfdce, 0xfdcf, 0xfdd0, 0xfdd1, 0xfdd2, 0xfdd3, 0xfdd4, 0xfdd5, 0xfdd6, 0xfdd7, 0xfdd8, 0xfdd9, 0xfdda, 0xfddb, 0xfddc, 0xfddd, 0xfdde, 0xfddf, 0xfde0, 0xfde1, 0xfde2, 0xfde3, 0xfde4, 0xfde5, 0xfde6, 0xfde7, 0xfde8, 0xfde9, 0xfdea, 0xfdeb, 0xfdec, 0xfded, 0xfdee, 0xfdef, 0x0635, 0x0642, 0x0627, 0x0627, 0x0645, 0x0635, 0x0631, 0x0639, 0x0648, 0x0635, 0x0635, 0x062c, 0x0631, 0xfdfd, 0xfdfe, 0xfdff, }; static const unsigned short gNormalizeTablefe00[] = { /* U+fe00 */ 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x002c, 0x3001, 0x3002, 0x003a, 0x003b, 0x0021, 0x003f, 0x3016, 0x3017, 0x002e, 0xfe1a, 0xfe1b, 0xfe1c, 0xfe1d, 0xfe1e, 0xfe1f, 0xfe20, 0xfe21, 0xfe22, 0xfe23, 0xfe24, 0xfe25, 0xfe26, 0xfe27, 0xfe28, 0xfe29, 0xfe2a, 0xfe2b, 0xfe2c, 0xfe2d, 0xfe2e, 0xfe2f, 0x002e, 0x2014, 0x2013, 0x005f, 0x005f, 0x0028, 0x0029, 0x007b, 0x007d, 0x3014, 0x3015, 0x3010, 0x3011, 0x300a, 0x300b, 0x3008, }; static const unsigned short gNormalizeTablefe40[] = { /* U+fe40 */ 0x3009, 0x300c, 0x300d, 0x300e, 0x300f, 0xfe45, 0xfe46, 0x005b, 0x005d, 0x0020, 0x0020, 0x0020, 0x0020, 0x005f, 0x005f, 0x005f, 0x002c, 0x3001, 0x002e, 0xfe53, 0x003b, 0x003a, 0x003f, 0x0021, 0x2014, 0x0028, 0x0029, 0x007b, 0x007d, 0x3014, 0x3015, 0x0023, 0x0026, 0x002a, 0x002b, 0x002d, 0x003c, 0x003e, 0x003d, 0xfe67, 0x005c, 0x0024, 0x0025, 0x0040, 0xfe6c, 0xfe6d, 0xfe6e, 0xfe6f, 0x0020, 0x0640, 0x0020, 0xfe73, 0x0020, 0xfe75, 0x0020, 0x0640, 0x0020, 0x0640, 0x0020, 0x0640, 0x0020, 0x0640, 0x0020, 0x0640, }; static const unsigned short gNormalizeTablefe80[] = { /* U+fe80 */ 0x0621, 0x0627, 0x0627, 0x0627, 0x0627, 0x0648, 0x0648, 0x0627, 0x0627, 0x064a, 0x064a, 0x064a, 0x064a, 0x0627, 0x0627, 0x0628, 0x0628, 0x0628, 0x0628, 0x0629, 0x0629, 0x062a, 0x062a, 0x062a, 0x062a, 0x062b, 0x062b, 0x062b, 0x062b, 0x062c, 0x062c, 0x062c, 0x062c, 0x062d, 0x062d, 0x062d, 0x062d, 0x062e, 0x062e, 0x062e, 0x062e, 0x062f, 0x062f, 0x0630, 0x0630, 0x0631, 0x0631, 0x0632, 0x0632, 0x0633, 0x0633, 0x0633, 0x0633, 0x0634, 0x0634, 0x0634, 0x0634, 0x0635, 0x0635, 0x0635, 0x0635, 0x0636, 0x0636, 0x0636, }; static const unsigned short gNormalizeTablefec0[] = { /* U+fec0 */ 0x0636, 0x0637, 0x0637, 0x0637, 0x0637, 0x0638, 0x0638, 0x0638, 0x0638, 0x0639, 0x0639, 0x0639, 0x0639, 0x063a, 0x063a, 0x063a, 0x063a, 0x0641, 0x0641, 0x0641, 0x0641, 0x0642, 0x0642, 0x0642, 0x0642, 0x0643, 0x0643, 0x0643, 0x0643, 0x0644, 0x0644, 0x0644, 0x0644, 0x0645, 0x0645, 0x0645, 0x0645, 0x0646, 0x0646, 0x0646, 0x0646, 0x0647, 0x0647, 0x0647, 0x0647, 0x0648, 0x0648, 0x0649, 0x0649, 0x064a, 0x064a, 0x064a, 0x064a, 0x0644, 0x0644, 0x0644, 0x0644, 0x0644, 0x0644, 0x0644, 0x0644, 0xfefd, 0xfefe, 0x0020, }; static const unsigned short gNormalizeTableff00[] = { /* U+ff00 */ 0xff00, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027, 0x0028, 0x0029, 0x002a, 0x002b, 0x002c, 0x002d, 0x002e, 0x002f, 0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037, 0x0038, 0x0039, 0x003a, 0x003b, 0x003c, 0x003d, 0x003e, 0x003f, 0x0040, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, 0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f, 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078, 0x0079, 0x007a, 0x005b, 0x005c, 0x005d, 0x005e, 0x005f, }; static const unsigned short gNormalizeTableff40[] = { /* U+ff40 */ 0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067, 0x0068, 0x0069, 0x006a, 0x006b, 0x006c, 0x006d, 0x006e, 0x006f, 0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077, 0x0078, 0x0079, 0x007a, 0x007b, 0x007c, 0x007d, 0x007e, 0x2985, 0x2986, 0x3002, 0x300c, 0x300d, 0x3001, 0x30fb, 0x30f2, 0x30a1, 0x30a3, 0x30a5, 0x30a7, 0x30a9, 0x30e3, 0x30e5, 0x30e7, 0x30c3, 0x30fc, 0x30a2, 0x30a4, 0x30a6, 0x30a8, 0x30aa, 0x30ab, 0x30ad, 0x30af, 0x30b1, 0x30b3, 0x30b5, 0x30b7, 0x30b9, 0x30bb, 0x30bd, }; static const unsigned short gNormalizeTableff80[] = { /* U+ff80 */ 0x30bf, 0x30c1, 0x30c4, 0x30c6, 0x30c8, 0x30ca, 0x30cb, 0x30cc, 0x30cd, 0x30ce, 0x30cf, 0x30d2, 0x30d5, 0x30d8, 0x30db, 0x30de, 0x30df, 0x30e0, 0x30e1, 0x30e2, 0x30e4, 0x30e6, 0x30e8, 0x30e9, 0x30ea, 0x30eb, 0x30ec, 0x30ed, 0x30ef, 0x30f3, 0x3099, 0x309a, 0x0020, 0x1100, 0x1101, 0x11aa, 0x1102, 0x11ac, 0x11ad, 0x1103, 0x1104, 0x1105, 0x11b0, 0x11b1, 0x11b2, 0x11b3, 0x11b4, 0x11b5, 0x111a, 0x1106, 0x1107, 0x1108, 0x1121, 0x1109, 0x110a, 0x110b, 0x110c, 0x110d, 0x110e, 0x110f, 0x1110, 0x1111, 0x1112, 0xffbf, }; static const unsigned short gNormalizeTableffc0[] = { /* U+ffc0 */ 0xffc0, 0xffc1, 0x1161, 0x1162, 0x1163, 0x1164, 0x1165, 0x1166, 0xffc8, 0xffc9, 0x1167, 0x1168, 0x1169, 0x116a, 0x116b, 0x116c, 0xffd0, 0xffd1, 0x116d, 0x116e, 0x116f, 0x1170, 0x1171, 0x1172, 0xffd8, 0xffd9, 0x1173, 0x1174, 0x1175, 0xffdd, 0xffde, 0xffdf, 0x00a2, 0x00a3, 0x00ac, 0x0020, 0x00a6, 0x00a5, 0x20a9, 0xffe7, 0x2502, 0x2190, 0x2191, 0x2192, 0x2193, 0x25a0, 0x25cb, 0xffef, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0x0020, 0xfff9, 0xfffa, 0xfffb, 0xfffc, 0xfffd, 0xfffe, 0xffff, }; static const unsigned short* gNormalizeTable[] = { 0, gNormalizeTable0040, gNormalizeTable0080, gNormalizeTable00c0, gNormalizeTable0100, gNormalizeTable0140, gNormalizeTable0180, gNormalizeTable01c0, gNormalizeTable0200, gNormalizeTable0240, gNormalizeTable0280, gNormalizeTable02c0, 0, gNormalizeTable0340, gNormalizeTable0380, gNormalizeTable03c0, gNormalizeTable0400, gNormalizeTable0440, gNormalizeTable0480, gNormalizeTable04c0, gNormalizeTable0500, gNormalizeTable0540, gNormalizeTable0580, 0, gNormalizeTable0600, gNormalizeTable0640, 0, gNormalizeTable06c0, 0, 0, 0, 0, 0, 0, 0, 0, gNormalizeTable0900, gNormalizeTable0940, 0, gNormalizeTable09c0, gNormalizeTable0a00, gNormalizeTable0a40, 0, 0, 0, gNormalizeTable0b40, gNormalizeTable0b80, gNormalizeTable0bc0, 0, gNormalizeTable0c40, 0, gNormalizeTable0cc0, 0, gNormalizeTable0d40, 0, gNormalizeTable0dc0, gNormalizeTable0e00, 0, gNormalizeTable0e80, gNormalizeTable0ec0, gNormalizeTable0f00, gNormalizeTable0f40, gNormalizeTable0f80, 0, gNormalizeTable1000, 0, gNormalizeTable1080, gNormalizeTable10c0, 0, gNormalizeTable1140, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, gNormalizeTable1780, 0, gNormalizeTable1800, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, gNormalizeTable1b00, gNormalizeTable1b40, 0, 0, 0, 0, 0, 0, gNormalizeTable1d00, gNormalizeTable1d40, gNormalizeTable1d80, 0, gNormalizeTable1e00, gNormalizeTable1e40, gNormalizeTable1e80, gNormalizeTable1ec0, gNormalizeTable1f00, gNormalizeTable1f40, gNormalizeTable1f80, gNormalizeTable1fc0, gNormalizeTable2000, gNormalizeTable2040, gNormalizeTable2080, 0, gNormalizeTable2100, gNormalizeTable2140, gNormalizeTable2180, gNormalizeTable21c0, gNormalizeTable2200, gNormalizeTable2240, gNormalizeTable2280, gNormalizeTable22c0, gNormalizeTable2300, 0, 0, 0, 0, gNormalizeTable2440, gNormalizeTable2480, gNormalizeTable24c0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, gNormalizeTable2a00, gNormalizeTable2a40, 0, gNormalizeTable2ac0, 0, 0, 0, 0, gNormalizeTable2c00, gNormalizeTable2c40, gNormalizeTable2c80, gNormalizeTable2cc0, 0, gNormalizeTable2d40, 0, 0, 0, 0, gNormalizeTable2e80, gNormalizeTable2ec0, gNormalizeTable2f00, gNormalizeTable2f40, gNormalizeTable2f80, gNormalizeTable2fc0, gNormalizeTable3000, gNormalizeTable3040, gNormalizeTable3080, gNormalizeTable30c0, gNormalizeTable3100, gNormalizeTable3140, gNormalizeTable3180, 0, gNormalizeTable3200, gNormalizeTable3240, gNormalizeTable3280, gNormalizeTable32c0, gNormalizeTable3300, gNormalizeTable3340, gNormalizeTable3380, gNormalizeTable33c0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, gNormalizeTablea640, gNormalizeTablea680, 0, gNormalizeTablea700, gNormalizeTablea740, gNormalizeTablea780, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, gNormalizeTablef900, gNormalizeTablef940, gNormalizeTablef980, gNormalizeTablef9c0, gNormalizeTablefa00, gNormalizeTablefa40, gNormalizeTablefa80, gNormalizeTablefac0, gNormalizeTablefb00, gNormalizeTablefb40, gNormalizeTablefb80, gNormalizeTablefbc0, gNormalizeTablefc00, gNormalizeTablefc40, gNormalizeTablefc80, gNormalizeTablefcc0, gNormalizeTablefd00, gNormalizeTablefd40, gNormalizeTablefd80, gNormalizeTablefdc0, gNormalizeTablefe00, gNormalizeTablefe40, gNormalizeTablefe80, gNormalizeTablefec0, gNormalizeTableff00, gNormalizeTableff40, gNormalizeTableff80, gNormalizeTableffc0, }; unsigned int normalize_character(const unsigned int c) { if (c >= 0x10000 || !gNormalizeTable[c >> 6]) return c; return gNormalizeTable[c >> 6][c & 0x3f]; } mediascanner2-0.115/src/mediascanner/mozilla/README.mozilla000066400000000000000000000003121436755250000234530ustar00rootroot00000000000000fts3_porter.c code is from SQLite3. This customized tokenizer "mozporter" by Mozilla supports CJK indexing using bi-gram. So you have to use bi-gram search string if you wanto to search CJK character. mediascanner2-0.115/src/mediascanner/mozilla/fts3_porter.c000066400000000000000000001207021436755250000235510ustar00rootroot00000000000000/* ** 2006 September 30 ** ** The author disclaims copyright to this source code. In place of ** a legal notice, here is a blessing: ** ** May you do good and not evil. ** May you find forgiveness for yourself and forgive others. ** May you share freely, never taking more than you give. ** ************************************************************************* ** Implementation of the full-text-search tokenizer that implements ** a Porter stemmer. ** */ /* * This file is based on the SQLite FTS3 Porter Stemmer implementation. * * This is an attempt to provide some level of full-text search to users of * Thunderbird who use languages that are not space/punctuation delimited. * This is accomplished by performing bi-gram indexing of characters fall * into the unicode space occupied by character sets used in such languages. * * Bi-gram indexing means that given the string "12345" we would index the * pairs "12", "23", "34", and "45" (with position information). We do this * because we are not sure where the word/semantic boundaries are in that * string. Then, when a user searches for "234" the FTS3 engine tokenizes the * search query into "23" and "34". Using special phrase-logic FTS3 requires * the matches to have the tokens "23" and "34" adjacent to each other and in * that order. In theory if the user searched for "2345" we we could just * search for "23 NEAR/2 34". Unfortunately, NEAR does not imply ordering, * so even though that would be more efficient, we would lose correctness * and cannot do it. * * The efficiency and usability of bi-gram search assumes that the character * space is large enough and actually observed bi-grams sufficiently * distributed throughout the potential space so that the search bi-grams * generated when the user issues a query find a 'reasonable' number of * documents for each bi-gram match. * * Mozilla contributors: * Makoto Kato * Andrew Sutherland */ /* ** The code in this file is only compiled if: ** ** * The FTS3 module is being built as an extension ** (in which case SQLITE_CORE is not defined), or ** ** * The FTS3 module is being built into the core of ** SQLite (in which case SQLITE_ENABLE_FTS3 is defined). */ #if !defined(SQLITE_CORE) || defined(SQLITE_ENABLE_FTS3) #define UNUSED __attribute__ ((unused)) #include #include #include #include #include #include "fts3_tokenizer.h" /* need some defined to compile without sqlite3 code */ #define sqlite3_malloc malloc #define sqlite3_free free #define sqlite3_realloc realloc static const unsigned char sqlite3Utf8Trans1[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x00, 0x00, }; typedef unsigned char u8; /** * SQLite helper macro from sqlite3.c (really utf.c) to encode a unicode * character into utf8. * * @param zOut A pointer to the current write position that is updated by * the routine. At entry it should point to one-past the last valid * encoded byte. The same holds true at exit. * @param c The character to encode; this should be an unsigned int. */ #define WRITE_UTF8(zOut, c) { \ if( c<0x0080 ){ \ *zOut++ = (u8)(c&0xff); \ } \ else if( c<0x0800 ){ \ *zOut++ = 0xC0 + (u8)((c>>6) & 0x1F); \ *zOut++ = 0x80 + (u8)(c & 0x3F); \ } \ else if( c<0x10000 ){ \ *zOut++ = 0xE0 + (u8)((c>>12) & 0x0F); \ *zOut++ = 0x80 + (u8)((c>>6) & 0x3F); \ *zOut++ = 0x80 + (u8)(c & 0x3F); \ }else{ \ *zOut++ = 0xf0 + (u8)((c>>18) & 0x07); \ *zOut++ = 0x80 + (u8)((c>>12) & 0x3F); \ *zOut++ = 0x80 + (u8)((c>>6) & 0x3F); \ *zOut++ = 0x80 + (u8)(c & 0x3F); \ } \ } /** * Fudge factor to avoid buffer overwrites when WRITE_UTF8 is involved. * * Our normalization table includes entries that may result in a larger * utf-8 encoding. Namely, 023a maps to 2c65. This is a growth from 2 bytes * as utf-8 encoded to 3 bytes. This is currently the only transition possible * because 1-byte encodings are known to stay 1-byte and our normalization * table is 16-bit and so can't generate a 4-byte encoded output. * * For simplicity, we just multiple by 2 which covers the current case and * potential growth for 2-byte to 4-byte growth. We can afford to do this * because we're not talking about a lot of memory here as a rule. */ #define MAX_UTF8_GROWTH_FACTOR 2 /** * Helper from sqlite3.c to read a single UTF8 character. * * The clever bit with multi-byte reading is that you keep going until you find * a byte whose top bits are not '10'. A single-byte UTF8 character will have * '00' or '01', and a multi-byte UTF8 character must start with '11'. * * In the event of illegal UTF-8 this macro may read an arbitrary number of * characters but will never read past zTerm. The resulting character value * of illegal UTF-8 can be anything, although efforts are made to return the * illegal character (0xfffd) for UTF-16 surrogates. * * @param zIn A pointer to the current position that is updated by the routine, * pointing at the start of the next character when the routine returns. * @param zTerm A pointer one past the end of the buffer. * @param c The 'unsigned int' to hold the resulting character value. Do not * use a short or a char. */ #define READ_UTF8(zIn, zTerm, c) { \ c = *(zIn++); \ if( c>=0xc0 ){ \ c = sqlite3Utf8Trans1[c-0xc0]; \ while( zIn!=zTerm && (*zIn & 0xc0)==0x80 ){ \ c = (c<<6) + (0x3f & *(zIn++)); \ } \ if( c<0x80 \ || (c&0xFFFFF800)==0xD800 \ || (c&0xFFFFFFFE)==0xFFFE ){ c = 0xFFFD; } \ } \ } /* end of compatible block to complie codes */ /* ** Class derived from sqlite3_tokenizer */ typedef struct porter_tokenizer { sqlite3_tokenizer base; /* Base class */ } porter_tokenizer; /* ** Class derived from sqlit3_tokenizer_cursor */ typedef struct porter_tokenizer_cursor { sqlite3_tokenizer_cursor base; const char *zInput; /* input we are tokenizing */ int nInput; /* size of the input */ int iOffset; /* current position in zInput */ int iToken; /* index of next token to be returned */ unsigned char *zToken; /* storage for current token */ int nAllocated; /* space allocated to zToken buffer */ /** * Store the offset of the second character in the bi-gram pair that we just * emitted so that we can consider it being the first character in a bi-gram * pair. * The value 0 indicates that there is no previous such character. This is * an acceptable sentinel value because the 0th offset can never be the * offset of the second in a bi-gram pair. * * For example, let us say we are tokenizing a string of 4 CJK characters * represented by the byte-string "11223344" where each repeated digit * indicates 2-bytes of storage used to encode the character in UTF-8. * (It actually takes 3, btw.) Then on the passes to emit each token, * the iOffset and iPrevGigramOffset values at entry will be: * * 1122: iOffset = 0, iPrevBigramOffset = 0 * 2233: iOffset = 4, iPrevBigramOffset = 2 * 3344: iOffset = 6, iPrevBigramOffset = 4 * (nothing will be emitted): iOffset = 8, iPrevBigramOffset = 6 */ int iPrevBigramOffset; /* previous result was bi-gram */ } porter_tokenizer_cursor; /* Forward declaration */ static const sqlite3_tokenizer_module porterTokenizerModule; /* from normalize.c */ extern unsigned int normalize_character(const unsigned int c); /* ** Create a new tokenizer instance. */ static int porterCreate( int UNUSED argc, const char UNUSED * const *argv, sqlite3_tokenizer **ppTokenizer ){ porter_tokenizer *t; t = (porter_tokenizer *) sqlite3_malloc(sizeof(*t)); if( t==NULL ) return SQLITE_NOMEM; memset(t, 0, sizeof(*t)); *ppTokenizer = &t->base; return SQLITE_OK; } /* ** Destroy a tokenizer */ static int porterDestroy(sqlite3_tokenizer *pTokenizer){ sqlite3_free(pTokenizer); return SQLITE_OK; } /* ** Prepare to begin tokenizing a particular string. The input ** string to be tokenized is zInput[0..nInput-1]. A cursor ** used to incrementally tokenize this string is returned in ** *ppCursor. */ static int porterOpen( sqlite3_tokenizer UNUSED *pTokenizer, /* The tokenizer */ const char *zInput, int nInput, /* String to be tokenized */ sqlite3_tokenizer_cursor **ppCursor /* OUT: Tokenization cursor */ ){ porter_tokenizer_cursor *c; c = (porter_tokenizer_cursor *) sqlite3_malloc(sizeof(*c)); if( c==NULL ) return SQLITE_NOMEM; c->zInput = zInput; if( zInput==0 ){ c->nInput = 0; }else if( nInput<0 ){ c->nInput = (int)strlen(zInput); }else{ c->nInput = nInput; } c->iOffset = 0; /* start tokenizing at the beginning */ c->iToken = 0; c->zToken = NULL; /* no space allocated, yet. */ c->nAllocated = 0; c->iPrevBigramOffset = 0; *ppCursor = &c->base; return SQLITE_OK; } /* ** Close a tokenization cursor previously opened by a call to ** porterOpen() above. */ static int porterClose(sqlite3_tokenizer_cursor *pCursor){ porter_tokenizer_cursor *c = (porter_tokenizer_cursor *) pCursor; sqlite3_free(c->zToken); sqlite3_free(c); return SQLITE_OK; } /* ** Vowel or consonant */ static const char cType[] = { 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 2, 1 }; /* ** isConsonant() and isVowel() determine if their first character in ** the string they point to is a consonant or a vowel, according ** to Porter ruls. ** ** A consonate is any letter other than 'a', 'e', 'i', 'o', or 'u'. ** 'Y' is a consonant unless it follows another consonant, ** in which case it is a vowel. ** ** In these routine, the letters are in reverse order. So the 'y' rule ** is that 'y' is a consonant unless it is followed by another ** consonent. */ static int isVowel(const char*); static int isConsonant(const char *z){ int j; char x = *z; if( x==0 ) return 0; assert( x>='a' && x<='z' ); j = cType[x-'a']; if( j<2 ) return j; return z[1]==0 || isVowel(z + 1); } static int isVowel(const char *z){ int j; char x = *z; if( x==0 ) return 0; assert( x>='a' && x<='z' ); j = cType[x-'a']; if( j<2 ) return 1-j; return isConsonant(z + 1); } /* ** Let any sequence of one or more vowels be represented by V and let ** C be sequence of one or more consonants. Then every word can be ** represented as: ** ** [C] (VC){m} [V] ** ** In prose: A word is an optional consonant followed by zero or ** vowel-consonant pairs followed by an optional vowel. "m" is the ** number of vowel consonant pairs. This routine computes the value ** of m for the first i bytes of a word. ** ** Return true if the m-value for z is 1 or more. In other words, ** return true if z contains at least one vowel that is followed ** by a consonant. ** ** In this routine z[] is in reverse order. So we are really looking ** for an instance of of a consonant followed by a vowel. */ static int m_gt_0(const char *z){ while( isVowel(z) ){ z++; } if( *z==0 ) return 0; while( isConsonant(z) ){ z++; } return *z!=0; } /* Like mgt0 above except we are looking for a value of m which is ** exactly 1 */ static int m_eq_1(const char *z){ while( isVowel(z) ){ z++; } if( *z==0 ) return 0; while( isConsonant(z) ){ z++; } if( *z==0 ) return 0; while( isVowel(z) ){ z++; } if( *z==0 ) return 1; while( isConsonant(z) ){ z++; } return *z==0; } /* Like mgt0 above except we are looking for a value of m>1 instead ** or m>0 */ static int m_gt_1(const char *z){ while( isVowel(z) ){ z++; } if( *z==0 ) return 0; while( isConsonant(z) ){ z++; } if( *z==0 ) return 0; while( isVowel(z) ){ z++; } if( *z==0 ) return 0; while( isConsonant(z) ){ z++; } return *z!=0; } /* ** Return TRUE if there is a vowel anywhere within z[0..n-1] */ static int hasVowel(const char *z){ while( isConsonant(z) ){ z++; } return *z!=0; } /* ** Return TRUE if the word ends in a double consonant. ** ** The text is reversed here. So we are really looking at ** the first two characters of z[]. */ static int doubleConsonant(const char *z){ return isConsonant(z) && z[0]==z[1] && isConsonant(z+1); } /* ** Return TRUE if the word ends with three letters which ** are consonant-vowel-consonent and where the final consonant ** is not 'w', 'x', or 'y'. ** ** The word is reversed here. So we are really checking the ** first three letters and the first one cannot be in [wxy]. */ static int star_oh(const char *z){ return z[0]!=0 && isConsonant(z) && z[0]!='w' && z[0]!='x' && z[0]!='y' && z[1]!=0 && isVowel(z+1) && z[2]!=0 && isConsonant(z+2); } /* ** If the word ends with zFrom and xCond() is true for the stem ** of the word that preceeds the zFrom ending, then change the ** ending to zTo. ** ** The input word *pz and zFrom are both in reverse order. zTo ** is in normal order. ** ** Return TRUE if zFrom matches. Return FALSE if zFrom does not ** match. Not that TRUE is returned even if xCond() fails and ** no substitution occurs. */ static int stem( char **pz, /* The word being stemmed (Reversed) */ const char *zFrom, /* If the ending matches this... (Reversed) */ const char *zTo, /* ... change the ending to this (not reversed) */ int (*xCond)(const char*) /* Condition that must be true */ ){ char *z = *pz; while( *zFrom && *zFrom==*z ){ z++; zFrom++; } if( *zFrom!=0 ) return 0; if( xCond && !xCond(z) ) return 1; while( *zTo ){ *(--z) = *(zTo++); } *pz = z; return 1; } /** * Voiced sound mark is only on Japanese. It is like accent. It combines with * previous character. Example, "ă‚”" (Katakana) with "゛" (voiced sound mark) is * "ă‚¶". Although full-width character mapping has combined character like "ă‚¶", * there is no combined character on half-width Katanaka character mapping. */ static int isVoicedSoundMark(const unsigned int c) { if (c == 0xff9e || c == 0xff9f || c == 0x3099 || c == 0x309a) return 1; return 0; } /** * How many unicode characters to take from the front and back of a term in * |copy_stemmer|. */ #define COPY_STEMMER_COPY_HALF_LEN 10 /** * Normalizing but non-stemming term copying. * * The original function would take 10 bytes from the front and 10 bytes from * the back if there were no digits in the string and it was more than 20 * bytes long. If there were digits involved that would decrease to 3 bytes * from the front and 3 from the back. This would potentially corrupt utf-8 * encoded characters, which is fine from the perspective of the FTS3 logic. * * In our revised form we now operate on a unicode character basis rather than * a byte basis. Additionally we use the same length limit even if there are * digits involved because it's not clear digit token-space reduction is saving * us from anything and could be hurting. Specifically, if no one is ever * going to search on things with digits, then we should just remove them. * Right now, the space reduction is going to increase false positives when * people do search on them and increase the number of collisions sufficiently * to make it really expensive. The caveat is there will be some increase in * index size which could be meaningful if people are receiving lots of emails * full of distinct numbers. * * In order to do the copy-from-the-front and copy-from-the-back trick, once * we reach N characters in, we set zFrontEnd to the current value of zOut * (which represents the termination of the first part of the result string) * and set zBackStart to the value of zOutStart. We then advanced zBackStart * along a character at a time as we write more characters. Once we have * traversed the entire string, if zBackStart > zFrontEnd, then we know * the string should be shrunk using the characters in the two ranges. * * (It would be faster to scan from the back with specialized logic but that * particular logic seems easy to screw up and we don't have unit tests in here * to the extent required.) * * @param zIn Input string to normalize and potentially shrink. * @param nBytesIn The number of bytes in zIn, distinct from the number of * unicode characters encoded in zIn. * @param zOut The string to write our output into. This must have at least * nBytesIn * MAX_UTF8_GROWTH_FACTOR in order to compensate for * normalization that results in a larger utf-8 encoding. * @param pnBytesOut Integer to write the number of bytes in zOut into. */ static void copy_stemmer(const unsigned char *zIn, const int nBytesIn, unsigned char *zOut, int *pnBytesOut){ const unsigned char *zInTerm = zIn + nBytesIn; unsigned char *zOutStart = zOut; unsigned int c; unsigned int charCount = 0; unsigned char *zFrontEnd = NULL, *zBackStart = NULL; unsigned int trashC; /* copy normalized character */ while (zIn < zInTerm) { READ_UTF8(zIn, zInTerm, c); c = normalize_character(c); /* ignore voiced/semi-voiced sound mark */ if (!isVoicedSoundMark(c)) { /* advance one non-voiced sound mark character. */ if (zBackStart) READ_UTF8(zBackStart, zOut, trashC); WRITE_UTF8(zOut, c); charCount++; if (charCount == COPY_STEMMER_COPY_HALF_LEN) { zFrontEnd = zOut; zBackStart = zOutStart; } } } /* if we need to shrink the string, transplant the back bytes */ if (zBackStart > zFrontEnd) { /* this handles when both are null too */ size_t backBytes = zOut - zBackStart; memmove(zFrontEnd, zBackStart, backBytes); zOut = zFrontEnd + backBytes; } *zOut = 0; *pnBytesOut = zOut - zOutStart; } /* ** Stem the input word zIn[0..nIn-1]. Store the output in zOut. ** zOut is at least big enough to hold nIn bytes. Write the actual ** size of the output word (exclusive of the '\0' terminator) into *pnOut. ** ** Any upper-case characters in the US-ASCII character set ([A-Z]) ** are converted to lower case. Upper-case UTF characters are ** unchanged. ** ** Words that are longer than about 20 bytes are stemmed by retaining ** a few bytes from the beginning and the end of the word. If the ** word contains digits, 3 bytes are taken from the beginning and ** 3 bytes from the end. For long words without digits, 10 bytes ** are taken from each end. US-ASCII case folding still applies. ** ** If the input word contains not digits but does characters not ** in [a-zA-Z] then no stemming is attempted and this routine just ** copies the input into the input into the output with US-ASCII ** case folding. ** ** Stemming never increases the length of the word. So there is ** no chance of overflowing the zOut buffer. */ static void porter_stemmer( const unsigned char *zIn, unsigned int nIn, unsigned char *zOut, int *pnOut ){ unsigned int i, j, c; char zReverse[28]; char *z, *z2; const unsigned char *zTerm = zIn + nIn; const unsigned char *zTmp = zIn; if( nIn<3 || nIn>=sizeof(zReverse)-7 ){ /* The word is too big or too small for the porter stemmer. ** Fallback to the copy stemmer */ copy_stemmer(zIn, nIn, zOut, pnOut); return; } for (j = sizeof(zReverse) - 6; zTmp < zTerm; j--) { READ_UTF8(zTmp, zTerm, c); c = normalize_character(c); if( c>='a' && c<='z' ){ zReverse[j] = c; }else{ /* The use of a character not in [a-zA-Z] means that we fallback ** to the copy stemmer */ copy_stemmer(zIn, nIn, zOut, pnOut); return; } } memset(&zReverse[sizeof(zReverse)-5], 0, 5); z = &zReverse[j+1]; /* Step 1a */ if( z[0]=='s' ){ if( !stem(&z, "sess", "ss", 0) && !stem(&z, "sei", "i", 0) && !stem(&z, "ss", "ss", 0) ){ z++; } } /* Step 1b */ z2 = z; if( stem(&z, "dee", "ee", m_gt_0) ){ /* Do nothing. The work was all in the test */ }else if( (stem(&z, "gni", "", hasVowel) || stem(&z, "de", "", hasVowel)) && z!=z2 ){ if( stem(&z, "ta", "ate", 0) || stem(&z, "lb", "ble", 0) || stem(&z, "zi", "ize", 0) ){ /* Do nothing. The work was all in the test */ }else if( doubleConsonant(z) && (*z!='l' && *z!='s' && *z!='z') ){ z++; }else if( m_eq_1(z) && star_oh(z) ){ *(--z) = 'e'; } } /* Step 1c */ if( z[0]=='y' && hasVowel(z+1) ){ z[0] = 'i'; } /* Step 2 */ switch( z[1] ){ case 'a': (void) (stem(&z, "lanoita", "ate", m_gt_0) || stem(&z, "lanoit", "tion", m_gt_0)); break; case 'c': (void) (stem(&z, "icne", "ence", m_gt_0) || stem(&z, "icna", "ance", m_gt_0)); break; case 'e': (void) (stem(&z, "rezi", "ize", m_gt_0)); break; case 'g': (void) (stem(&z, "igol", "log", m_gt_0)); break; case 'l': (void) (stem(&z, "ilb", "ble", m_gt_0) || stem(&z, "illa", "al", m_gt_0) || stem(&z, "iltne", "ent", m_gt_0) || stem(&z, "ile", "e", m_gt_0) || stem(&z, "ilsuo", "ous", m_gt_0)); break; case 'o': (void) (stem(&z, "noitazi", "ize", m_gt_0) || stem(&z, "noita", "ate", m_gt_0) || stem(&z, "rota", "ate", m_gt_0)); break; case 's': (void) (stem(&z, "msila", "al", m_gt_0) || stem(&z, "ssenevi", "ive", m_gt_0) || stem(&z, "ssenluf", "ful", m_gt_0) || stem(&z, "ssensuo", "ous", m_gt_0)); break; case 't': (void) (stem(&z, "itila", "al", m_gt_0) || stem(&z, "itivi", "ive", m_gt_0) || stem(&z, "itilib", "ble", m_gt_0)); break; } /* Step 3 */ switch( z[0] ){ case 'e': (void) (stem(&z, "etaci", "ic", m_gt_0) || stem(&z, "evita", "", m_gt_0) || stem(&z, "ezila", "al", m_gt_0)); break; case 'i': (void) (stem(&z, "itici", "ic", m_gt_0)); break; case 'l': (void) (stem(&z, "laci", "ic", m_gt_0) || stem(&z, "luf", "", m_gt_0)); break; case 's': (void) (stem(&z, "ssen", "", m_gt_0)); break; } /* Step 4 */ switch( z[1] ){ case 'a': if( z[0]=='l' && m_gt_1(z+2) ){ z += 2; } break; case 'c': if( z[0]=='e' && z[2]=='n' && (z[3]=='a' || z[3]=='e') && m_gt_1(z+4) ){ z += 4; } break; case 'e': if( z[0]=='r' && m_gt_1(z+2) ){ z += 2; } break; case 'i': if( z[0]=='c' && m_gt_1(z+2) ){ z += 2; } break; case 'l': if( z[0]=='e' && z[2]=='b' && (z[3]=='a' || z[3]=='i') && m_gt_1(z+4) ){ z += 4; } break; case 'n': if( z[0]=='t' ){ if( z[2]=='a' ){ if( m_gt_1(z+3) ){ z += 3; } }else if( z[2]=='e' ){ (void) (stem(&z, "tneme", "", m_gt_1) || stem(&z, "tnem", "", m_gt_1) || stem(&z, "tne", "", m_gt_1)); } } break; case 'o': if( z[0]=='u' ){ if( m_gt_1(z+2) ){ z += 2; } }else if( z[3]=='s' || z[3]=='t' ){ (void) (stem(&z, "noi", "", m_gt_1)); } break; case 's': if( z[0]=='m' && z[2]=='i' && m_gt_1(z+3) ){ z += 3; } break; case 't': (void) (stem(&z, "eta", "", m_gt_1) || stem(&z, "iti", "", m_gt_1)); break; case 'u': if( z[0]=='s' && z[2]=='o' && m_gt_1(z+3) ){ z += 3; } break; case 'v': case 'z': if( z[0]=='e' && z[2]=='i' && m_gt_1(z+3) ){ z += 3; } break; } /* Step 5a */ if( z[0]=='e' ){ if( m_gt_1(z+1) ){ z++; }else if( m_eq_1(z+1) && !star_oh(z+1) ){ z++; } } /* Step 5b */ if( m_gt_1(z) && z[0]=='l' && z[1]=='l' ){ z++; } /* z[] is now the stemmed word in reverse order. Flip it back ** around into forward order and return. */ *pnOut = i = strlen(z); zOut[i] = 0; while( *z ){ zOut[--i] = *(z++); } } /** * Indicate whether characters in the 0x30 - 0x7f region can be part of a token. * Letters and numbers can; punctuation (and 'del') can't. */ static const char porterIdChar[] = { /* x0 x1 x2 x3 x4 x5 x6 x7 x8 x9 xA xB xC xD xE xF */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, /* 3x */ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 4x */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, /* 5x */ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 6x */ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, /* 7x */ }; /** * Test whether a character is a (non-ascii) space character or not. isDelim * uses the existing porter stemmer logic for anything in the ASCII (< 0x80) * space which covers 0x20. * * 0x2000-0x206F is the general punctuation table. 0x2000 - 0x200b are spaces. * The spaces 0x2000 - 0x200a are all defined as roughly equivalent to a * standard 0x20 space. 0x200b is a "zero width space" (ZWSP) and not like an * 0x20 space. 0x202f is a narrow no-break space and roughly equivalent to an * 0x20 space. 0x205f is a "medium mathematical space" and defined as roughly * equivalent to an 0x20 space. */ #define IS_UNI_SPACE(x) (((x)>=0x2000&&(x)<=0x200a) || (x)==0x202f || (x)==0x205f) /** * What we are checking for: * - 0x3001: Ideographic comma (-> 0x2c ',') * - 0x3002: Ideographic full stop (-> 0x2e '.') * - 0xff0c: fullwidth comma (~ wide 0x2c ',') * - 0xff0e: fullwidth full stop (~ wide 0x2e '.') * - 0xff61: halfwidth ideographic full stop (~ narrow 0x3002) * - 0xff64: halfwidth ideographic comma (~ narrow 0x3001) * * It is possible we should be treating other things as delimiters! */ #define IS_JA_DELIM(x) (((x)==0x3001)||((x)==0xFF64)||((x)==0xFF0E)||((x)==0x3002)||((x)==0xFF61)||((x)==0xFF0C)) /** * The previous character was a delimeter (which includes the start of the * string). */ #define BIGRAM_RESET 0 /** * The previous character was a CJK character and we have only seen one of them. * If we had seen more than one in a row it would be the BIGRAM_USE state. */ #define BIGRAM_UNKNOWN 1 /** * We have seen two or more CJK characters in a row. */ #define BIGRAM_USE 2 /** * The previous character was ASCII or something in the unicode general scripts * area that we do not believe is a delimeter. We call it 'alpha' as in * alphabetic/alphanumeric and something that should be tokenized based on * delimiters rather than on a bi-gram basis. */ #define BIGRAM_ALPHA 3 static int isDelim( const unsigned char *zCur, /* IN: current pointer of token */ const unsigned char *zTerm, /* IN: one character beyond end of token */ int *len, /* OUT: analyzed bytes in this token */ int *state /* IN/OUT: analyze state */ ){ const unsigned char *zIn = zCur; unsigned int c; int delim; /* get the unicode character to analyze */ READ_UTF8(zIn, zTerm, c); c = normalize_character(c); *len = zIn - zCur; /* ASCII character range has rule */ if( c < 0x80 ){ // This is original porter stemmer isDelim logic. // 0x0 - 0x1f are all control characters, 0x20 is space, 0x21-0x2f are // punctuation. delim = (c < 0x30 || !porterIdChar[c - 0x30]); // cases: "&a", "&." if (*state == BIGRAM_USE || *state == BIGRAM_UNKNOWN ){ /* previous maybe CJK and current is ascii */ *state = BIGRAM_ALPHA; /*ascii*/ delim = 1; /* must break */ } else if (delim == 1) { // cases: "a.", ".." /* this is delimiter character */ *state = BIGRAM_RESET; /*reset*/ } else { // cases: "aa", ".a" *state = BIGRAM_ALPHA; /*ascii*/ } return delim; } // (at this point we must be a non-ASCII character) /* voiced/semi-voiced sound mark is ignore */ if (isVoicedSoundMark(c) && *state != BIGRAM_ALPHA) { /* ignore this because it is combined with previous char */ return 0; } /* this isn't CJK range, so return as no delim */ // Anything less than 0x2000 (except to U+0E00-U+0EFF and U+1780-U+17FF) // is the general scripts area and should not be bi-gram indexed. // 0xa000 - 0a4cf is the Yi area. It is apparently a phonetic language whose // usage does not appear to have simple delimeter rules, so we're leaving it // as bigram processed. This is a guess, if you know better, let us know. // (We previously bailed on this range too.) // Addition, U+0E00-U+0E7F is Thai, U+0E80-U+0EFF is Laos, // and U+1780-U+17FF is Khmer. It is no easy way to break each word. // So these should use bi-gram too. // cases: "aa", ".a", "&a" if (c < 0xe00 || (c >= 0xf00 && c < 0x1780) || (c >= 0x1800 && c < 0x2000)) { *state = BIGRAM_ALPHA; /* not really ASCII but same idea; tokenize it */ return 0; } // (at this point we must be a bi-grammable char or delimiter) /* this is space character or delim character */ // cases: "a.", "..", "&." if( IS_UNI_SPACE(c) || IS_JA_DELIM(c) ){ *state = BIGRAM_RESET; /* reset */ return 1; /* it actually is a delimiter; report as such */ } // (at this point we must be a bi-grammable char) // cases: "a&" if( *state==BIGRAM_ALPHA ){ /* Previous is ascii and current maybe CJK */ *state = BIGRAM_UNKNOWN; /* mark as unknown */ return 1; /* break to emit the ASCII token*/ } /* We have no rule for CJK!. use bi-gram */ // cases: "&&" if( *state==BIGRAM_UNKNOWN || *state==BIGRAM_USE ){ /* previous state is unknown. mark as bi-gram */ *state = BIGRAM_USE; return 1; /* break to emit the digram */ } // cases: ".&" (*state == BIGRAM_RESET) *state = BIGRAM_UNKNOWN; /* mark as unknown */ return 0; /* no need to break; nothing to emit */ } static inline int tokenizer_hides_wildcards(void) { static int first_run = 1; static int hides_wildcard; if (first_run) { int version = sqlite3_libversion_number(); hides_wildcard = version > 3008004 && version < 3008007; first_run = 0; } return hides_wildcard; } /** * Generate a new token. There are basically three types of token we can * generate: * - A porter stemmed token. This is a word entirely comprised of ASCII * characters. We run the porter stemmer algorithm against the word. * Because we have no way to know what is and is not an English word * (the only language for which the porter stemmer was designed), this * could theoretically map multiple words that are not variations of the * same word down to the same root, resulting in potentially unexpected * result inclusions in the search results. We accept this result because * there's not a lot we can do about it and false positives are much * better than false negatives. * - A copied token; case/accent-folded but not stemmed. We call the porter * stemmer for all non-CJK cases and it diverts to the copy stemmer if it * sees any non-ASCII characters (after folding) or if the string is too * long. The copy stemmer will shrink the string if it is deemed too long. * - A bi-gram token; two CJK-ish characters. For query reasons we generate a * series of overlapping bi-grams. (We can't require the user to start their * search based on the arbitrary context of the indexed documents.) * * It may be useful to think of this function as operating at the points between * characters. While we are considering the 'current' character (the one after * the 'point'), we are also interested in the 'previous' character (the one * preceding the point). * At any 'point', there are a number of possible situations which I will * illustrate with pairs of characters. 'a' means alphanumeric ASCII or a * non-ASCII character that is not bi-grammable or a delimeter, '.' * means a delimiter (space or punctuation), '&' means a bi-grammable * character. * - aa: We are in the midst of a token. State remains BIGRAM_ALPHA. * - a.: We will generate a porter stemmed or copied token. State was * BIGRAM_ALPHA, gets set to BIGRAM_RESET. * - a&: We will generate a porter stemmed or copied token; we will set our * state to BIGRAM_UNKNOWN to indicate we have seen one bigram character * but that it is not yet time to emit a bigram. * - .a: We are starting a token. State was BIGRAM_RESET, gets set to * BIGRAM_ALPHA. * - ..: We skip/eat the delimeters. State stays BIGRAM_RESET. * - .&: State set to BIGRAM_UNKNOWN to indicate we have seen one bigram char. * - &a: If the state was BIGRAM_USE, we generate a bi-gram token. If the state * was BIGRAM_UNKNOWN we had only seen one CJK character and so don't do * anything. State is set to BIGRAM_ALPHA. * - &.: Same as the "&a" case, but state is set to BIGRAM_RESET. * - &&: We will generate a bi-gram token. State was either BIGRAM_UNKNOWN or * BIGRAM_USE, gets set to BIGRAM_USE. */ static int porterNext( sqlite3_tokenizer_cursor *pCursor, /* Cursor returned by porterOpen */ const char **pzToken, /* OUT: *pzToken is the token text */ int *pnBytes, /* OUT: Number of bytes in token */ int *piStartOffset, /* OUT: Starting offset of token */ int *piEndOffset, /* OUT: Ending offset of token */ int *piPosition /* OUT: Position integer of token */ ){ porter_tokenizer_cursor *c = (porter_tokenizer_cursor *) pCursor; const unsigned char *z = (unsigned char *) c->zInput; int len = 0; int state; while( c->iOffset < c->nInput ){ int iStartOffset, numChars; /* * This loop basically has two modes of operation: * - general processing (iPrevBigramOffset == 0 here) * - CJK processing (iPrevBigramOffset != 0 here) * * In an general processing pass we skip over all the delimiters, leaving us * at a character that promises to produce a token. This could be a CJK * token (state == BIGRAM_USE) or an ALPHA token (state == BIGRAM_ALPHA). * If it was a CJK token, we transition into CJK state for the next loop. * If it was an alpha token, our current offset is pointing at a delimiter * (which could be a CJK character), so it is good that our next pass * through the function and loop will skip over any delimiters. If the * delimiter we hit was a CJK character, the next time through we will * not treat it as a delimiter though; the entry state for that scan is * BIGRAM_RESET so the transition is not treated as a delimiter! * * The CJK pass always starts with the second character in a bi-gram emitted * as a token in the previous step. No delimiter skipping is required * because we know that first character might produce a token for us. It * only 'might' produce a token because the previous pass performed no * lookahead and cannot be sure it is followed by another CJK character. * This is why */ // If we have a previous bigram offset if (c->iPrevBigramOffset == 0) { /* Scan past delimiter characters */ state = BIGRAM_RESET; /* reset */ while (c->iOffset < c->nInput && isDelim(z + c->iOffset, z + c->nInput, &len, &state)) { c->iOffset += len; } } else { /* for bigram indexing, use previous offset */ c->iOffset = c->iPrevBigramOffset; } /* Count non-delimiter characters. */ iStartOffset = c->iOffset; numChars = 0; // Start from a reset state. This means the first character we see // (which will not be a delimiter) determines which of ALPHA or CJK modes // we are operating in. (It won't be a delimiter because in a 'general' // pass as defined above, we will have eaten all the delimiters, and in // a CJK pass we are guaranteed that the first character is CJK.) state = BIGRAM_RESET; /* state is reset */ // Advance until it is time to emit a token. // For ALPHA characters, this means advancing until we encounter a delimiter // or a CJK character. iOffset will be pointing at the delimiter or CJK // character, aka one beyond the last ALPHA character. // For CJK characters this means advancing until we encounter an ALPHA // character, a delimiter, or we have seen two consecutive CJK // characters. iOffset points at the ALPHA/delimiter in the first 2 cases // and the second of two CJK characters in the last case. // Because of the way this loop is structured, iOffset is only updated // when we don't terminate. However, if we terminate, len still contains // the number of bytes in the character found at iOffset. (This is useful // in the CJK case.) while (c->iOffset < c->nInput && !isDelim(z + c->iOffset, z + c->nInput, &len, &state)) { c->iOffset += len; numChars++; } if (state == BIGRAM_USE) { /* Split word by bigram */ // Right now iOffset is pointing at the second character in a pair. // Save this offset so next-time through we start with that as the // first character. c->iPrevBigramOffset = c->iOffset; // And now advance so that iOffset is pointing at the character after // the second character in the bi-gram pair. Also count the char. c->iOffset += len; numChars++; } else { /* Reset bigram offset */ c->iPrevBigramOffset = 0; } /* We emit a token if: * - there are two ideograms together, * - there are three chars or more, * - we think this is a query and wildcard magic is desired. * We think is a wildcard query when we have at least one * character, our current offset is one shy of nInput and the * character at iOffset is '*'. * * Sqlite 3.8.5 ... 3.8.6 changed how the tokenizer is called such * that we can't detect wildcards. In that case, we accept any * short word at the end of the input. */ // It is possible we have no token to emit here if iPrevBigramOffset was not // 0 on entry and there was no second CJK character. iPrevBigramOffset // will now be 0 if that is the case (and c->iOffset == iStartOffset). if (// allow two-character words only if in bigram (numChars == 2 && state == BIGRAM_USE) || // otherwise, drop two-letter words (considered stop-words) (numChars >=3) || // final word wildcard case: (numChars >= 1 && (tokenizer_hides_wildcards() ? (c->iOffset == c->nInput) : (c->iOffset == c->nInput - 1 && z[c->iOffset] == '*')))) { /* figure out the number of bytes to copy/stem */ int n = c->iOffset - iStartOffset; /* make sure there is enough buffer space */ if (n * MAX_UTF8_GROWTH_FACTOR > c->nAllocated) { c->nAllocated = n * MAX_UTF8_GROWTH_FACTOR + 20; c->zToken = sqlite3_realloc(c->zToken, c->nAllocated); if (c->zToken == NULL) return SQLITE_NOMEM; } if (state == BIGRAM_USE) { /* This is by bigram. So it is unnecessary to convert word */ copy_stemmer(&z[iStartOffset], n, c->zToken, pnBytes); } else { porter_stemmer(&z[iStartOffset], n, c->zToken, pnBytes); } *pzToken = (const char*)c->zToken; *piStartOffset = iStartOffset; *piEndOffset = c->iOffset; *piPosition = c->iToken++; return SQLITE_OK; } } return SQLITE_DONE; } /* ** The set of routines that implement the porter-stemmer tokenizer */ static const sqlite3_tokenizer_module porterTokenizerModule = { 0, porterCreate, porterDestroy, porterOpen, porterClose, porterNext, }; /* ** Allocate a new porter tokenizer. Return a pointer to the new ** tokenizer in *ppModule */ void sqlite3Fts3PorterTokenizerModule( sqlite3_tokenizer_module const**ppModule ){ *ppModule = &porterTokenizerModule; } #endif /* !defined(SQLITE_CORE) || defined(SQLITE_ENABLE_FTS3) */ mediascanner2-0.115/src/mediascanner/mozilla/fts3_tokenizer.h000066400000000000000000000136171436755250000242630ustar00rootroot00000000000000/* ** 2006 July 10 ** ** The author disclaims copyright to this source code. ** ************************************************************************* ** Defines the interface to tokenizers used by fulltext-search. There ** are three basic components: ** ** sqlite3_tokenizer_module is a singleton defining the tokenizer ** interface functions. This is essentially the class structure for ** tokenizers. ** ** sqlite3_tokenizer is used to define a particular tokenizer, perhaps ** including customization information defined at creation time. ** ** sqlite3_tokenizer_cursor is generated by a tokenizer to generate ** tokens from a particular input. */ #ifndef _FTS3_TOKENIZER_H_ #define _FTS3_TOKENIZER_H_ /* TODO(shess) Only used for SQLITE_OK and SQLITE_DONE at this time. ** If tokenizers are to be allowed to call sqlite3_*() functions, then ** we will need a way to register the API consistently. */ #include "sqlite3.h" /* ** Structures used by the tokenizer interface. When a new tokenizer ** implementation is registered, the caller provides a pointer to ** an sqlite3_tokenizer_module containing pointers to the callback ** functions that make up an implementation. ** ** When an fts3 table is created, it passes any arguments passed to ** the tokenizer clause of the CREATE VIRTUAL TABLE statement to the ** sqlite3_tokenizer_module.xCreate() function of the requested tokenizer ** implementation. The xCreate() function in turn returns an ** sqlite3_tokenizer structure representing the specific tokenizer to ** be used for the fts3 table (customized by the tokenizer clause arguments). ** ** To tokenize an input buffer, the sqlite3_tokenizer_module.xOpen() ** method is called. It returns an sqlite3_tokenizer_cursor object ** that may be used to tokenize a specific input buffer based on ** the tokenization rules supplied by a specific sqlite3_tokenizer ** object. */ typedef struct sqlite3_tokenizer_module sqlite3_tokenizer_module; typedef struct sqlite3_tokenizer sqlite3_tokenizer; typedef struct sqlite3_tokenizer_cursor sqlite3_tokenizer_cursor; struct sqlite3_tokenizer_module { /* ** Structure version. Should always be set to 0. */ int iVersion; /* ** Create a new tokenizer. The values in the argv[] array are the ** arguments passed to the "tokenizer" clause of the CREATE VIRTUAL ** TABLE statement that created the fts3 table. For example, if ** the following SQL is executed: ** ** CREATE .. USING fts3( ... , tokenizer arg1 arg2) ** ** then argc is set to 2, and the argv[] array contains pointers ** to the strings "arg1" and "arg2". ** ** This method should return either SQLITE_OK (0), or an SQLite error ** code. If SQLITE_OK is returned, then *ppTokenizer should be set ** to point at the newly created tokenizer structure. The generic ** sqlite3_tokenizer.pModule variable should not be initialised by ** this callback. The caller will do so. */ int (*xCreate)( int argc, /* Size of argv array */ const char *const*argv, /* Tokenizer argument strings */ sqlite3_tokenizer **ppTokenizer /* OUT: Created tokenizer */ ); /* ** Destroy an existing tokenizer. The fts3 module calls this method ** exactly once for each successful call to xCreate(). */ int (*xDestroy)(sqlite3_tokenizer *pTokenizer); /* ** Create a tokenizer cursor to tokenize an input buffer. The caller ** is responsible for ensuring that the input buffer remains valid ** until the cursor is closed (using the xClose() method). */ int (*xOpen)( sqlite3_tokenizer *pTokenizer, /* Tokenizer object */ const char *pInput, int nBytes, /* Input buffer */ sqlite3_tokenizer_cursor **ppCursor /* OUT: Created tokenizer cursor */ ); /* ** Destroy an existing tokenizer cursor. The fts3 module calls this ** method exactly once for each successful call to xOpen(). */ int (*xClose)(sqlite3_tokenizer_cursor *pCursor); /* ** Retrieve the next token from the tokenizer cursor pCursor. This ** method should either return SQLITE_OK and set the values of the ** "OUT" variables identified below, or SQLITE_DONE to indicate that ** the end of the buffer has been reached, or an SQLite error code. ** ** *ppToken should be set to point at a buffer containing the ** normalized version of the token (i.e. after any case-folding and/or ** stemming has been performed). *pnBytes should be set to the length ** of this buffer in bytes. The input text that generated the token is ** identified by the byte offsets returned in *piStartOffset and ** *piEndOffset. *piStartOffset should be set to the index of the first ** byte of the token in the input buffer. *piEndOffset should be set ** to the index of the first byte just past the end of the token in ** the input buffer. ** ** The buffer *ppToken is set to point at is managed by the tokenizer ** implementation. It is only required to be valid until the next call ** to xNext() or xClose(). */ /* TODO(shess) current implementation requires pInput to be ** nul-terminated. This should either be fixed, or pInput/nBytes ** should be converted to zInput. */ int (*xNext)( sqlite3_tokenizer_cursor *pCursor, /* Tokenizer cursor */ const char **ppToken, int *pnBytes, /* OUT: Normalized text for token */ int *piStartOffset, /* OUT: Byte offset of token in input buffer */ int *piEndOffset, /* OUT: Byte offset of end of token in input buffer */ int *piPosition /* OUT: Number of tokens returned before this one */ ); }; struct sqlite3_tokenizer { const sqlite3_tokenizer_module *pModule; /* The module for this tokenizer */ /* Tokenizer implementations will typically add additional fields */ }; struct sqlite3_tokenizer_cursor { sqlite3_tokenizer *pTokenizer; /* Tokenizer for this cursor. */ /* Tokenizer implementations will typically add additional fields */ }; #endif /* _FTS3_TOKENIZER_H_ */ mediascanner2-0.115/src/mediascanner/scannercore.hh000066400000000000000000000017401436755250000223070ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef SCANNERCORE_H #define SCANNERCORE_H namespace mediascanner { enum MediaType { UnknownMedia, AudioMedia, VideoMedia, ImageMedia, AllMedia = 255, }; enum class MediaOrder { Default, Rank, Title, Date, Modified, }; } #endif mediascanner2-0.115/src/mediascanner/utils.cc000066400000000000000000000106471436755250000211410ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include"internal/utils.hh" #include #include #include #include #include #include namespace mediascanner { std::string sqlQuote(const std::string &input) { std::vector out; out.reserve(input.size() + 2); const char quote = '\''; out.push_back(quote); for(size_t i=0; i std::string filenameToTitle(const std::string &filename) { auto fname_start = filename.rfind('/'); auto suffix_dot = filename.rfind('.'); std::string result; if(fname_start == std::string::npos) { if(suffix_dot == std::string::npos) { result = filename; } else { result = filename.substr(0, suffix_dot); } } else { if(suffix_dot == std::string::npos) { result = filename.substr(fname_start+1, filename.size()); } else { result = filename.substr(fname_start+1, suffix_dot-fname_start-1); } } for(size_t i=0; imessage; g_error_free(error); throw std::runtime_error(msg); } std::string uri(uristr); g_free(uristr); return uri; } using namespace std; static bool dir_exists(const string &path) { struct stat statbuf; if(stat(path.c_str(), &statbuf) < 0) { if(errno != ENOENT) { printf("Error while trying to determine state of dir %s: %s\n", path.c_str(), strerror(errno)); } return false; } return S_ISDIR(statbuf.st_mode) ; } static bool file_exists(const string &path) { struct stat statbuf; if(stat(path.c_str(), &statbuf) < 0) { if(errno != ENOENT) { printf("Error while trying to determine state of file %s: %s\n", path.c_str(), strerror(errno)); } return false; } return S_ISREG(statbuf.st_mode) ; } bool is_rootlike(const string &path) { string s1 = path + "/usr"; string s2 = path + "/var"; string s3 = path + "/bin"; string s4 = path + "/Program Files"; return (dir_exists(s1) && dir_exists(s2) && dir_exists(s3)) || dir_exists(s4); } bool is_optical_disc(const string &path) { string dvd1 = path + "/AUDIO_TS"; string dvd2 = path + "/VIDEO_TS"; string bluray = path + "/BDMV"; return (dir_exists(dvd1) && dir_exists(dvd2)) || dir_exists(bluray); } bool has_scanblock(const std::string &path) { return file_exists(path + "/.nomedia"); } static string uri_escape(const string &unescaped) { char *result = g_uri_escape_string(unescaped.c_str(), NULL, FALSE); string escaped(result); g_free(result); return escaped; } string make_album_art_uri(const string &artist, const string &album) { string result = "image://albumart/artist="; result += uri_escape(artist); result += "&album="; result += uri_escape(album); return result; } std::string make_thumbnail_uri(const std::string &uri) { return string("image://thumbnailer/") + uri; } } mediascanner2-0.115/src/ms-dbus/000077500000000000000000000000001436755250000164035ustar00rootroot00000000000000mediascanner2-0.115/src/ms-dbus/CMakeLists.txt000066400000000000000000000022721436755250000211460ustar00rootroot00000000000000include_directories(..) add_definitions(${MEDIASCANNER_CFLAGS} ${DBUSCPP_CFLAGS} ${APPARMOR_CFLAGS}) add_library(ms-dbus STATIC dbus-codec.cc service-skeleton.cc service-stub.cc ) # gcc 6 is miscompiling something in dbus-cpp, and disabling # optimisation makes the problem go away. # https://bugs.launchpad.net/ubuntu/+source/mediascanner2/+bug/1621002 set_source_files_properties(dbus-codec.cc PROPERTIES COMPILE_FLAGS "-O0") target_link_libraries(ms-dbus mediascanner Threads::Threads ${DBUSCPP_LDFLAGS} ${APPARMOR_LDFLAGS}) # Compile with -fPIC, since this code is linked into the QML plugin. set_target_properties(ms-dbus PROPERTIES COMPILE_FLAGS "-fPIC ") add_executable(mediascanner-dbus main.cc ) target_link_libraries(mediascanner-dbus ms-dbus) set_target_properties(mediascanner-dbus PROPERTIES OUTPUT_NAME "mediascanner-dbus-2.0") install( TARGETS mediascanner-dbus RUNTIME DESTINATION ${CMAKE_INSTALL_LIBEXECDIR}/mediascanner-2.0 ) configure_file( com.canonical.MediaScanner2.service.in com.canonical.MediaScanner2.service) install( FILES ${CMAKE_CURRENT_BINARY_DIR}/com.canonical.MediaScanner2.service DESTINATION ${CMAKE_INSTALL_DATADIR}/dbus-1/services ) mediascanner2-0.115/src/ms-dbus/com.canonical.MediaScanner2.service.in000066400000000000000000000001751436755250000255130ustar00rootroot00000000000000[D-BUS Service] Name=com.canonical.MediaScanner2 Exec=@CMAKE_INSTALL_FULL_LIBEXECDIR@/mediascanner-2.0/mediascanner-dbus-2.0 mediascanner2-0.115/src/ms-dbus/dbus-codec.cc000066400000000000000000000155201436755250000207250ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "dbus-codec.hh" #include #include #include #include #include #include #include using core::dbus::Message; using core::dbus::Codec; using core::dbus::types::Variant; using mediascanner::MediaFile; using mediascanner::MediaFileBuilder; using mediascanner::MediaOrder; using mediascanner::MediaType; using mediascanner::Album; using mediascanner::Filter; using std::string; void Codec::encode_argument(Message::Writer &out, const MediaFile &file) { auto w = out.open_structure(); core::dbus::encode_argument(w, file.getFileName()); core::dbus::encode_argument(w, file.getContentType()); core::dbus::encode_argument(w, file.getETag()); core::dbus::encode_argument(w, file.getTitle()); core::dbus::encode_argument(w, file.getAuthor()); core::dbus::encode_argument(w, file.getAlbum()); core::dbus::encode_argument(w, file.getAlbumArtist()); core::dbus::encode_argument(w, file.getDate()); core::dbus::encode_argument(w, file.getGenre()); core::dbus::encode_argument(w, (int32_t)file.getDiscNumber()); core::dbus::encode_argument(w, (int32_t)file.getTrackNumber()); core::dbus::encode_argument(w, (int32_t)file.getDuration()); core::dbus::encode_argument(w, (int32_t)file.getWidth()); core::dbus::encode_argument(w, (int32_t)file.getHeight()); core::dbus::encode_argument(w, file.getLatitude()); core::dbus::encode_argument(w, file.getLongitude()); core::dbus::encode_argument(w, file.getHasThumbnail()); core::dbus::encode_argument(w, file.getModificationTime()); core::dbus::encode_argument(w, (int32_t)file.getType()); out.close_structure(std::move(w)); } void Codec::decode_argument(Message::Reader &in, MediaFile &file) { auto r = in.pop_structure(); string filename, content_type, etag, title, author; string album, album_artist, date, genre; int32_t disc_number, track_number, duration, width, height, type; double latitude, longitude; bool has_thumbnail; uint64_t mtime; r >> filename >> content_type >> etag >> title >> author >> album >> album_artist >> date >> genre >> disc_number >> track_number >> duration >> width >> height >> latitude >> longitude >> has_thumbnail >> mtime >> type; file = MediaFileBuilder(filename) .setContentType(content_type) .setETag(etag) .setTitle(title) .setAuthor(author) .setAlbum(album) .setAlbumArtist(album_artist) .setDate(date) .setGenre(genre) .setDiscNumber(disc_number) .setTrackNumber(track_number) .setDuration(duration) .setWidth(width) .setHeight(height) .setLatitude(latitude) .setLongitude(longitude) .setHasThumbnail(has_thumbnail) .setModificationTime(mtime) .setType((MediaType)type); } void Codec::encode_argument(Message::Writer &out, const Album &album) { auto w = out.open_structure(); core::dbus::encode_argument(w, album.getTitle()); core::dbus::encode_argument(w, album.getArtist()); core::dbus::encode_argument(w, album.getDate()); core::dbus::encode_argument(w, album.getGenre()); core::dbus::encode_argument(w, album.getArtFile()); core::dbus::encode_argument(w, album.getHasThumbnail()); core::dbus::encode_argument(w, album.getArtistCount()); out.close_structure(std::move(w)); } void Codec::decode_argument(Message::Reader &in, Album &album) { auto r = in.pop_structure(); string title, artist, date, genre, art_file; bool has_thumbnail; int artist_count; r >> title >> artist >> date >> genre >> art_file >> has_thumbnail >> artist_count; album = Album(title, artist, date, genre, art_file, has_thumbnail, artist_count); } void Codec::encode_argument(Message::Writer &out, const Filter &filter) { auto w = out.open_array(core::dbus::types::Signature("{sv}")); if (filter.hasArtist()) { w.close_dict_entry( w.open_dict_entry() << string("artist") << Variant::encode(filter.getArtist())); } if (filter.hasAlbum()) { w.close_dict_entry( w.open_dict_entry() << string("album") << Variant::encode(filter.getAlbum())); } if (filter.hasAlbumArtist()) { w.close_dict_entry( w.open_dict_entry() << string("album_artist") << Variant::encode(filter.getAlbumArtist())); } if (filter.hasGenre()) { w.close_dict_entry( w.open_dict_entry() << string("genre") << Variant::encode(filter.getGenre())); } w.close_dict_entry( w.open_dict_entry() << string("offset") << Variant::encode((int32_t)filter.getOffset())); w.close_dict_entry( w.open_dict_entry() << string("limit") << Variant::encode((int32_t)filter.getLimit())); w.close_dict_entry( w.open_dict_entry() << string("order") << Variant::encode(static_cast(filter.getOrder()))); w.close_dict_entry( w.open_dict_entry() << string("reverse") << Variant::encode(filter.getReverse())); out.close_array(std::move(w)); } void Codec::decode_argument(Message::Reader &in, Filter &filter) { auto r = in.pop_array(); filter.clear(); while (r.type() != ArgumentType::invalid) { string key; Variant value; r.pop_dict_entry() >> key >> value; if (key == "artist") { filter.setArtist(value.as()); } else if (key == "album") { filter.setAlbum(value.as()); } else if (key == "album_artist") { filter.setAlbumArtist(value.as()); } else if (key == "genre") { filter.setGenre(value.as()); } else if (key == "offset") { filter.setOffset(value.as()); } else if (key == "limit") { filter.setLimit(value.as()); } else if (key == "order") { filter.setOrder(static_cast(value.as())); } else if (key == "reverse") { filter.setReverse(value.as()); } } } mediascanner2-0.115/src/ms-dbus/dbus-codec.hh000066400000000000000000000056301436755250000207400ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef MEDIASCANNER_DBUS_CODEC_HH #define MEDIASCANNER_DBUS_CODEC_HH #include #include #include namespace mediascanner { class MediaFile; class Album; class Filter; } namespace core { namespace dbus { template <> struct Codec { static void encode_argument(Message::Writer &out, const mediascanner::MediaFile &file); static void decode_argument(Message::Reader &in, mediascanner::MediaFile &file); }; template <> struct Codec { static void encode_argument(Message::Writer &out, const mediascanner::Album &album); static void decode_argument(Message::Reader &in, mediascanner::Album &album); }; template <> struct Codec { static void encode_argument(Message::Writer &out, const mediascanner::Filter &filter); static void decode_argument(Message::Reader &in, mediascanner::Filter &filter); }; namespace helper { template<> struct TypeMapper { constexpr static ArgumentType type_value() { return ArgumentType::structure; } constexpr static bool is_basic_type() { return false; } constexpr static bool requires_signature() { return true; } static const std::string &signature() { static const std::string s = "(sssssssssiiiiiddbti)"; return s; } }; template<> struct TypeMapper { constexpr static ArgumentType type_value() { return ArgumentType::structure; } constexpr static bool is_basic_type() { return false; } constexpr static bool requires_signature() { return true; } static const std::string &signature() { static const std::string s = "(sssssbi)"; return s; } }; template<> struct TypeMapper { constexpr static ArgumentType type_value() { return ArgumentType::array; } constexpr static bool is_basic_type() { return false; } constexpr static bool requires_signature() { return true; } static const std::string &signature() { static const std::string s = "a{sv}"; return s; } }; } } } #endif mediascanner2-0.115/src/ms-dbus/dbus-interface.hh000066400000000000000000000132731436755250000216250ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef MEDIASCANNER_DBUS_INTERFACE_HH #define MEDIASCANNER_DBUS_INTERFACE_HH #include #include #include #include namespace mediascanner { namespace dbus { struct MediaStoreInterface { inline static const std::string& name() { static std::string s = "com.canonical.MediaScanner2"; return s; } inline static const std::chrono::milliseconds default_timeout() { return std::chrono::seconds{10}; } struct Errors { struct Error { inline static const std::string& name() { static std::string s = "com.canonical.MediaScanner2.Error"; return s; } }; struct Unauthorized { inline static const std::string& name() { static std::string s = "com.canonical.MediaScanner2.Error.Unauthorized"; return s; } }; }; struct Lookup { typedef MediaStoreInterface Interface; inline static const std::string& name() { static std::string s = "Lookup"; return s; } inline static const std::chrono::milliseconds default_timeout() { return Interface::default_timeout(); } }; struct Query { typedef MediaStoreInterface Interface; inline static const std::string& name() { static std::string s = "Query"; return s; } inline static const std::chrono::milliseconds default_timeout() { return Interface::default_timeout(); } }; struct QueryAlbums { typedef MediaStoreInterface Interface; inline static const std::string& name() { static std::string s = "QueryAlbums"; return s; } inline static const std::chrono::milliseconds default_timeout() { return std::chrono::seconds{1}; } }; struct QueryArtists { typedef MediaStoreInterface Interface; inline static const std::string& name() { static std::string s = "QueryArtists"; return s; } inline static const std::chrono::milliseconds default_timeout() { return std::chrono::seconds{1}; } }; struct GetAlbumSongs { typedef MediaStoreInterface Interface; inline static const std::string& name() { static std::string s = "GetAlbumSongs"; return s; } inline static const std::chrono::milliseconds default_timeout() { return Interface::default_timeout(); } }; struct GetETag { typedef MediaStoreInterface Interface; inline static const std::string& name() { static std::string s = "GetETag"; return s; } inline static const std::chrono::milliseconds default_timeout() { return Interface::default_timeout(); } }; struct ListSongs { typedef MediaStoreInterface Interface; inline static const std::string& name() { static std::string s = "ListSongs"; return s; } inline static const std::chrono::milliseconds default_timeout() { return Interface::default_timeout(); } }; struct ListAlbums { typedef MediaStoreInterface Interface; inline static const std::string& name() { static std::string s = "ListAlbums"; return s; } inline static const std::chrono::milliseconds default_timeout() { return Interface::default_timeout(); } }; struct ListArtists { typedef MediaStoreInterface Interface; inline static const std::string& name() { static std::string s = "ListArtists"; return s; } inline static const std::chrono::milliseconds default_timeout() { return Interface::default_timeout(); } }; struct ListAlbumArtists { typedef MediaStoreInterface Interface; inline static const std::string& name() { static std::string s = "ListAlbumArtists"; return s; } inline static const std::chrono::milliseconds default_timeout() { return Interface::default_timeout(); } }; struct ListGenres { typedef MediaStoreInterface Interface; inline static const std::string& name() { static std::string s = "ListGenres"; return s; } inline static const std::chrono::milliseconds default_timeout() { return Interface::default_timeout(); } }; struct HasMedia { typedef MediaStoreInterface Interface; inline static const std::string& name() { static std::string s = "HasMedia"; return s; } inline static const std::chrono::milliseconds default_timeout() { return Interface::default_timeout(); } }; }; } } #endif mediascanner2-0.115/src/ms-dbus/main.cc000066400000000000000000000023131436755250000176350ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include #include #include #include #include "service-skeleton.hh" using namespace mediascanner; int main(int , char **) { auto bus = std::make_shared(core::dbus::WellKnownBus::session); bus->install_executor(core::dbus::asio::make_executor(bus)); auto store = std::make_shared(MS_READ_ONLY); dbus::ServiceSkeleton service(bus, store); service.run(); return 0; } mediascanner2-0.115/src/ms-dbus/service-skeleton.cc000066400000000000000000000347241436755250000222060ustar00rootroot00000000000000#include "service-skeleton.hh" #include #include #include #include #include #include #include #include #include #include #include "dbus-interface.hh" #include "dbus-codec.hh" using core::dbus::Message; using core::dbus::types::ObjectPath; using core::dbus::types::Variant; namespace mediascanner { namespace dbus { struct BusDaemon { static const std::string &name() { static std::string s = "org.freedesktop.DBus"; return s; } struct GetConnectionCredentials { typedef BusDaemon Interface; static const std::string &name() { static std::string s = "GetConnectionCredentials"; return s; } static const std::chrono::milliseconds default_timeout() { return std::chrono::seconds{1}; } }; }; struct ServiceSkeleton::Private { ServiceSkeleton *impl; std::shared_ptr store; core::dbus::Object::Ptr object; Private(ServiceSkeleton *impl, std::shared_ptr store) : impl(impl), store(store), object(impl->access_service()->add_object_for_path( core::dbus::traits::Service::object_path())) { object->install_method_handler( std::bind( &Private::handle_lookup, this, std::placeholders::_1)); object->install_method_handler( std::bind( &Private::handle_query, this, std::placeholders::_1)); object->install_method_handler( std::bind( &Private::handle_query_albums, this, std::placeholders::_1)); object->install_method_handler( std::bind( &Private::handle_query_artists, this, std::placeholders::_1)); object->install_method_handler( std::bind( &Private::handle_get_album_songs, this, std::placeholders::_1)); object->install_method_handler( std::bind( &Private::handle_get_etag, this, std::placeholders::_1)); object->install_method_handler( std::bind( &Private::handle_list_songs, this, std::placeholders::_1)); object->install_method_handler( std::bind( &Private::handle_list_albums, this, std::placeholders::_1)); object->install_method_handler( std::bind( &Private::handle_list_artists, this, std::placeholders::_1)); object->install_method_handler( std::bind( &Private::handle_list_album_artists, this, std::placeholders::_1)); object->install_method_handler( std::bind( &Private::handle_list_genres, this, std::placeholders::_1)); object->install_method_handler( std::bind( &Private::handle_has_media, this, std::placeholders::_1)); } std::string get_client_apparmor_context(const Message::Ptr &message) { if (!aa_is_enabled()) { return "unconfined"; } auto service = core::dbus::Service::use_service( impl->access_bus(), "org.freedesktop.DBus"); auto obj = service->object_for_path( ObjectPath("/org/freedesktop/DBus")); core::dbus::Result> result; try { result = obj->invoke_method_synchronously>(message->sender()); } catch (const std::exception &e) { fprintf(stderr, "Error getting connection credentials: %s\n", e.what()); return std::string(); } if (result.is_error()) { fprintf(stderr, "Error getting connection credentials: %s\n", result.error().print().c_str()); return std::string(); } const auto& creds = result.value(); auto it = creds.find("LinuxSecurityLabel"); if (it == creds.end()) { fprintf(stderr, "Connection credentials don't include security label"); return std::string(); } std::vector label; try { label = it->second.as>(); } catch (const std::exception &e) { fprintf(stderr, "Could not convert security label to byte array"); return std::string(); } return std::string(aa_splitcon(reinterpret_cast(&label[0]), nullptr)); } bool does_client_have_access(const std::string &context, MediaType type) { if (context.empty()) { // Deny access if we don't have a context return false; } if (context == "unconfined") { // Unconfined return true; } auto pos = context.find_first_of('_'); if (pos == std::string::npos) { fprintf(stderr, "Badly formed AppArmor context: %s\n", context.c_str()); return false; } const std::string pkgname = context.substr(0, pos); // TODO: when the trust store lands, check it to see if this // app can access the index. if (type == AudioMedia && pkgname == "com.ubuntu.music") { return true; } return false; } bool check_access(const Message::Ptr &message, MediaType type) { const std::string context = get_client_apparmor_context(message); bool have_access = does_client_have_access(context, type); if (!have_access) { auto reply = Message::make_error( message, MediaStoreInterface::Errors::Unauthorized::name(), "Unauthorized"); impl->access_bus()->send(reply); } return have_access; } void handle_lookup(const Message::Ptr &message) { if (!check_access(message, AllMedia)) return; std::string filename; message->reader() >> filename; Message::Ptr reply; try { MediaFile file = store->lookup(filename); reply = Message::make_method_return(message); reply->writer() << file; } catch (const std::exception &e) { reply = Message::make_error( message, MediaStoreInterface::Errors::Error::name(), e.what()); } impl->access_bus()->send(reply); } void handle_query(const Message::Ptr &message) { std::string query; int32_t type; Filter filter; message->reader() >> query >> type >> filter; if (!check_access(message, (MediaType)type)) return; Message::Ptr reply; try { auto results = store->query(query, (MediaType)type, filter); reply = Message::make_method_return(message); reply->writer() << results; } catch (const std::exception &e) { reply = Message::make_error( message, MediaStoreInterface::Errors::Error::name(), e.what()); } impl->access_bus()->send(reply); } void handle_query_albums(const Message::Ptr &message) { if (!check_access(message, AudioMedia)) return; std::string query; Filter filter; message->reader() >> query >> filter; Message::Ptr reply; try { auto albums = store->queryAlbums(query, filter); reply = Message::make_method_return(message); reply->writer() << albums; } catch (const std::exception &e) { reply = Message::make_error( message, MediaStoreInterface::Errors::Error::name(), e.what()); } impl->access_bus()->send(reply); } void handle_query_artists(const Message::Ptr &message) { if (!check_access(message, AudioMedia)) return; std::string query; Filter filter; message->reader() >> query >> filter; Message::Ptr reply; try { auto artists = store->queryArtists(query, filter); reply = Message::make_method_return(message); reply->writer() << artists; } catch (const std::exception &e) { reply = Message::make_error( message, MediaStoreInterface::Errors::Error::name(), e.what()); } impl->access_bus()->send(reply); } void handle_get_album_songs(const Message::Ptr &message) { if (!check_access(message, AudioMedia)) return; Album album("", ""); message->reader() >> album; Message::Ptr reply; try { auto results = store->getAlbumSongs(album); reply = Message::make_method_return(message); reply->writer() << results; } catch (const std::exception &e) { reply = Message::make_error( message, MediaStoreInterface::Errors::Error::name(), e.what()); } impl->access_bus()->send(reply); } void handle_get_etag(const Message::Ptr &message) { if (!check_access(message, AllMedia)) return; std::string filename; message->reader() >> filename; Message::Ptr reply; try { std::string etag = store->getETag(filename); reply = Message::make_method_return(message); reply->writer() << etag; } catch (const std::exception &e) { reply = Message::make_error( message, MediaStoreInterface::Errors::Error::name(), e.what()); } impl->access_bus()->send(reply); } void handle_list_songs(const Message::Ptr &message) { if (!check_access(message, AudioMedia)) return; Filter filter; message->reader() >> filter; Message::Ptr reply; try { auto results = store->listSongs(filter); reply = Message::make_method_return(message); reply->writer() << results; } catch (const std::exception &e) { reply = Message::make_error( message, MediaStoreInterface::Errors::Error::name(), e.what()); } impl->access_bus()->send(reply); } void handle_list_albums(const Message::Ptr &message) { if (!check_access(message, AudioMedia)) return; Filter filter; message->reader() >> filter; Message::Ptr reply; try { auto albums = store->listAlbums(filter); reply = Message::make_method_return(message); reply->writer() << albums; } catch (const std::exception &e) { reply = Message::make_error( message, MediaStoreInterface::Errors::Error::name(), e.what()); } impl->access_bus()->send(reply); } void handle_list_artists(const Message::Ptr &message) { if (!check_access(message, AudioMedia)) return; Filter filter; message->reader() >> filter; Message::Ptr reply; try { auto artists = store->listArtists(filter); reply = Message::make_method_return(message); reply->writer() << artists; } catch (const std::exception &e) { reply = Message::make_error( message, MediaStoreInterface::Errors::Error::name(), e.what()); } impl->access_bus()->send(reply); } void handle_list_album_artists(const Message::Ptr &message) { if (!check_access(message, AudioMedia)) return; Filter filter; message->reader() >> filter; Message::Ptr reply; try { auto artists = store->listAlbumArtists(filter); reply = Message::make_method_return(message); reply->writer() << artists; } catch (const std::exception &e) { reply = Message::make_error( message, MediaStoreInterface::Errors::Error::name(), e.what()); } impl->access_bus()->send(reply); } void handle_list_genres(const Message::Ptr &message) { if (!check_access(message, AudioMedia)) return; Filter filter; message->reader() >> filter; Message::Ptr reply; try { auto genres = store->listGenres(filter); reply = Message::make_method_return(message); reply->writer() << genres; } catch (const std::exception &e) { reply = Message::make_error( message, MediaStoreInterface::Errors::Error::name(), e.what()); } impl->access_bus()->send(reply); } void handle_has_media(const Message::Ptr &message) { int32_t type; message->reader() >> type; if (!check_access(message, static_cast(type))) return; Message::Ptr reply; try { bool result = store->hasMedia(static_cast(type)); reply = Message::make_method_return(message); reply->writer() << result; } catch (const std::exception &e) { reply = Message::make_error( message, MediaStoreInterface::Errors::Error::name(), e.what()); } impl->access_bus()->send(reply); } }; ServiceSkeleton::ServiceSkeleton(core::dbus::Bus::Ptr bus, std::shared_ptr store) : core::dbus::Skeleton(bus), p(new Private(this, store)) { } ServiceSkeleton::~ServiceSkeleton() { } void ServiceSkeleton::run() { access_bus()->run(); } void ServiceSkeleton::stop() { access_bus()->stop(); } } } mediascanner2-0.115/src/ms-dbus/service-skeleton.hh000066400000000000000000000023761436755250000222160ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef MEDIASCANNER_DBUS_SERVICE_SKELETON_HH #define MEDIASCANNER_DBUS_SERVICE_SKELETON_HH #include #include #include #include "service.hh" namespace mediascanner { class MediaStore; namespace dbus { class ServiceSkeleton : public core::dbus::Skeleton { public: ServiceSkeleton(core::dbus::Bus::Ptr bus, std::shared_ptr store); ~ServiceSkeleton(); void run(); void stop(); private: struct Private; std::unique_ptr p; }; } } #endif mediascanner2-0.115/src/ms-dbus/service-stub.cc000066400000000000000000000120251436755250000213250ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "service-stub.hh" #include #include #include #include #include "dbus-interface.hh" #include "dbus-codec.hh" using std::string; namespace mediascanner { namespace dbus { struct ServiceStub::Private { core::dbus::Object::Ptr object; }; ServiceStub::ServiceStub(core::dbus::Bus::Ptr bus) : core::dbus::Stub(bus), p(new Private{access_service()->object_for_path( core::dbus::types::ObjectPath(core::dbus::traits::Service::object_path()))}) { } ServiceStub::~ServiceStub() { } MediaFile ServiceStub::lookup(const string &filename) const { auto result = p->object->invoke_method_synchronously(filename); if (result.is_error()) throw std::runtime_error(result.error().print()); return result.value(); } std::vector ServiceStub::query(const string &q, MediaType type, const Filter &filter) const { auto result = p->object->invoke_method_synchronously>(q, (int32_t)type, filter); if (result.is_error()) throw std::runtime_error(result.error().print()); return result.value(); } std::vector ServiceStub::queryAlbums(const string &core_term, const Filter &filter) const { auto result = p->object->invoke_method_synchronously>(core_term, filter); if (result.is_error()) throw std::runtime_error(result.error().print()); return result.value(); } std::vector ServiceStub::queryArtists(const string &q, const Filter &filter) const { auto result = p->object->invoke_method_synchronously>(q, filter); if (result.is_error()) throw std::runtime_error(result.error().print()); return result.value(); } std::vector ServiceStub::getAlbumSongs(const Album& album) const { auto result = p->object->invoke_method_synchronously>(album); if (result.is_error()) throw std::runtime_error(result.error().print()); return result.value(); } string ServiceStub::getETag(const string &filename) const { auto result = p->object->invoke_method_synchronously(filename); if (result.is_error()) throw std::runtime_error(result.error().print()); return result.value(); } std::vector ServiceStub::listSongs(const Filter &filter) const { auto result = p->object->invoke_method_synchronously>(filter); if (result.is_error()) throw std::runtime_error(result.error().print()); return result.value(); } std::vector ServiceStub::listAlbums(const Filter &filter) const { auto result = p->object->invoke_method_synchronously>(filter); if (result.is_error()) throw std::runtime_error(result.error().print()); return result.value(); } std::vector ServiceStub::listArtists(const Filter &filter) const { auto result = p->object->invoke_method_synchronously>(filter); if (result.is_error()) throw std::runtime_error(result.error().print()); return result.value(); } std::vector ServiceStub::listAlbumArtists(const Filter &filter) const { auto result = p->object->invoke_method_synchronously>(filter); if (result.is_error()) throw std::runtime_error(result.error().print()); return result.value(); } std::vector ServiceStub::listGenres(const Filter &filter) const { auto result = p->object->invoke_method_synchronously>(filter); if (result.is_error()) throw std::runtime_error(result.error().print()); return result.value(); } bool ServiceStub::hasMedia(MediaType type) const { auto result = p->object->invoke_method_synchronously(static_cast(type)); if (result.is_error()) throw std::runtime_error(result.error().print()); return result.value(); } } } mediascanner2-0.115/src/ms-dbus/service-stub.hh000066400000000000000000000045321436755250000213430ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef MEDIASCANNER_DBUS_SERVICE_STUB_HH #define MEDIASCANNER_DBUS_SERVICE_STUB_HH #include #include #include #include #include #include #include "service.hh" namespace mediascanner { class Album; class Filter; class MediaFile; namespace dbus { class ServiceStub : public core::dbus::Stub, public virtual MediaStoreBase { public: explicit ServiceStub(core::dbus::Bus::Ptr bus); virtual ~ServiceStub(); virtual MediaFile lookup(const std::string &filename) const override; virtual std::vector query(const std::string &q, MediaType type, const Filter &filter) const override; virtual std::vector queryAlbums(const std::string &core_term, const Filter &filter) const override; virtual std::vector queryArtists(const std::string &q, const Filter &filter) const override; virtual std::vector getAlbumSongs(const Album& album) const override; virtual std::string getETag(const std::string &filename) const override; virtual std::vector listSongs(const Filter &filter) const override; virtual std::vector listAlbums(const Filter &filter) const override; virtual std::vector listArtists(const Filter &filter) const override; virtual std::vector listAlbumArtists(const Filter &filter) const override; virtual std::vector listGenres(const Filter &filter) const override; virtual bool hasMedia(MediaType type) const override; private: struct Private; std::unique_ptr p; }; } } #endif mediascanner2-0.115/src/ms-dbus/service.hh000066400000000000000000000031351436755250000203660ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef MEDIASCANNER_DBUS_SERVICE_HH #define MEDIASCANNER_DBUS_SERVICE_HH #include namespace mediascanner { namespace dbus { class MediaStoreService { public: MediaStoreService() {} MediaStoreService(const MediaStoreService&) = delete; virtual ~MediaStoreService() = default; MediaStoreService& operator=(const MediaStoreService&) = delete; bool operator==(const MediaStoreService&) const = delete; }; } } namespace core { namespace dbus { namespace traits { template<> struct Service { inline static const std::string& interface_name() { static const std::string iface("com.canonical.MediaScanner2"); return iface; } inline static const std::string& object_path() { static const std::string path("/com/canonical/MediaScanner2"); return path; } }; } } } #endif mediascanner2-0.115/src/qml/000077500000000000000000000000001436755250000156225ustar00rootroot00000000000000mediascanner2-0.115/src/qml/MediaScanner.0.1/000077500000000000000000000000001436755250000204505ustar00rootroot00000000000000mediascanner2-0.115/src/qml/MediaScanner.0.1/AlbumModelBase.cpp000066400000000000000000000044541436755250000237770ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "AlbumModelBase.h" using namespace mediascanner::qml; AlbumModelBase::AlbumModelBase(QObject *parent) : StreamingModel(parent) { roles[Roles::RoleTitle] = "title"; roles[Roles::RoleArtist] = "artist"; roles[Roles::RoleDate] = "date"; roles[Roles::RoleGenre] = "genre"; roles[Roles::RoleArt] = "art"; } int AlbumModelBase::rowCount(const QModelIndex &) const { return results.size(); } QVariant AlbumModelBase::data(const QModelIndex &index, int role) const { if (index.row() < 0 || index.row() >= (ptrdiff_t)results.size()) { return QVariant(); } const mediascanner::Album &album = results[index.row()]; switch (role) { case RoleTitle: return QString::fromStdString(album.getTitle()); case RoleArtist: if (album.getArtistCount() > 1) { return QStringLiteral("Various"); // This gets translated in the client apps } return QString::fromStdString(album.getArtist()); case RoleDate: return QString::fromStdString(album.getDate()); case RoleGenre: return QString::fromStdString(album.getGenre()); case RoleArt: return QString::fromStdString(album.getArtUri()); default: return QVariant(); } } QHash AlbumModelBase::roleNames() const { return roles; } void AlbumModelBase::appendRows(std::unique_ptr &&row_data) { AlbumRowData *data = static_cast(row_data.get()); std::move(data->rows.begin(), data->rows.end(), std::back_inserter(results)); } void AlbumModelBase::clearBacking() { results.clear(); } mediascanner2-0.115/src/qml/MediaScanner.0.1/AlbumModelBase.h000066400000000000000000000036001436755250000234340ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef MEDIASCANNER_QML_ALBUMMODELBASE_H #define MEDIASCANNER_QML_ALBUMMODELBASE_H #include #include #include #include "StreamingModel.h" namespace mediascanner { namespace qml { class AlbumModelBase : public StreamingModel { Q_OBJECT Q_ENUMS(Roles) public: enum Roles { RoleTitle, RoleArtist, RoleDate, RoleGenre, RoleArt, }; explicit AlbumModelBase(QObject *parent = 0); int rowCount(const QModelIndex &parent=QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; void appendRows(std::unique_ptr &&row_data) override; void clearBacking() override; class AlbumRowData : public RowData { public: AlbumRowData(std::vector &&rows) : rows(std::move(rows)) {} ~AlbumRowData() {} size_t size() const override { return rows.size(); } std::vector rows; }; protected: QHash roleNames() const override; private: QHash roles; std::vector results; }; } } #endif mediascanner2-0.115/src/qml/MediaScanner.0.1/AlbumsModel.cpp000066400000000000000000000061741436755250000233700ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "AlbumsModel.h" #include using namespace mediascanner::qml; AlbumsModel::AlbumsModel(QObject *parent) : AlbumModelBase(parent) { } QVariant AlbumsModel::getArtist() { if (!filter.hasArtist()) return QVariant(); return QString::fromStdString(filter.getArtist()); } void AlbumsModel::setArtist(const QVariant artist) { if (artist.isNull()) { if (filter.hasArtist()) { filter.unsetArtist(); invalidate(); } } else { const std::string std_artist = artist.value().toStdString(); if (!filter.hasArtist() || filter.getArtist() != std_artist) { filter.setArtist(std_artist); invalidate(); } } } QVariant AlbumsModel::getAlbumArtist() { if (!filter.hasAlbumArtist()) return QVariant(); return QString::fromStdString(filter.getAlbumArtist()); } void AlbumsModel::setAlbumArtist(const QVariant album_artist) { if (album_artist.isNull()) { if (filter.hasAlbumArtist()) { filter.unsetAlbumArtist(); invalidate(); } } else { const std::string std_album_artist = album_artist.value().toStdString(); if (!filter.hasAlbumArtist() || filter.getAlbumArtist() != std_album_artist) { filter.setAlbumArtist(std_album_artist); invalidate(); } } } QVariant AlbumsModel::getGenre() { if (!filter.hasGenre()) return QVariant(); return QString::fromStdString(filter.getGenre()); } void AlbumsModel::setGenre(const QVariant genre) { if (genre.isNull()) { if (filter.hasGenre()) { filter.unsetGenre(); invalidate(); } } else { const std::string std_genre = genre.value().toStdString(); if (!filter.hasGenre() || filter.getGenre() != std_genre) { filter.setGenre(std_genre); invalidate(); } } } int AlbumsModel::getLimit() { return -1; } void AlbumsModel::setLimit(int) { qWarning() << "Setting limit on AlbumsModel is deprecated"; } std::unique_ptr AlbumsModel::retrieveRows(std::shared_ptr store, int limit, int offset) const { auto limit_filter = filter; limit_filter.setLimit(limit); limit_filter.setOffset(offset); return std::unique_ptr( new AlbumRowData(store->listAlbums(limit_filter))); } mediascanner2-0.115/src/qml/MediaScanner.0.1/AlbumsModel.h000066400000000000000000000033711436755250000230310ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef MEDIASCANNER_QML_ALBUMSMODEL_H #define MEDIASCANNER_QML_ALBUMSMODEL_H #include #include #include "MediaStoreWrapper.h" #include "AlbumModelBase.h" namespace mediascanner { namespace qml { class AlbumsModel : public AlbumModelBase { Q_OBJECT Q_PROPERTY(QVariant artist READ getArtist WRITE setArtist) Q_PROPERTY(QVariant albumArtist READ getAlbumArtist WRITE setAlbumArtist) Q_PROPERTY(QVariant genre READ getGenre WRITE setGenre) Q_PROPERTY(int limit READ getLimit WRITE setLimit) public: explicit AlbumsModel(QObject *parent=0); std::unique_ptr retrieveRows(std::shared_ptr store, int limit, int offset) const override; protected: QVariant getArtist(); void setArtist(const QVariant artist); QVariant getAlbumArtist(); void setAlbumArtist(const QVariant album_artist); QVariant getGenre(); void setGenre(const QVariant genre); int getLimit(); void setLimit(int limit); private: Filter filter; }; } } #endif mediascanner2-0.115/src/qml/MediaScanner.0.1/ArtistsModel.cpp000066400000000000000000000067021436755250000235730ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "ArtistsModel.h" #include using namespace mediascanner::qml; ArtistsModel::ArtistsModel(QObject *parent) : StreamingModel(parent), album_artists(false) { roles[Roles::RoleArtist] = "artist"; } int ArtistsModel::rowCount(const QModelIndex &) const { return results.size(); } QVariant ArtistsModel::data(const QModelIndex &index, int role) const { if (index.row() < 0 || index.row() >= (ptrdiff_t)results.size()) { return QVariant(); } switch (role) { case RoleArtist: return QString::fromStdString(results[index.row()]); default: return QVariant(); } } QHash ArtistsModel::roleNames() const { return roles; } bool ArtistsModel::getAlbumArtists() { return album_artists; } void ArtistsModel::setAlbumArtists(bool album_artists) { if (this->album_artists != album_artists) { this->album_artists = album_artists; invalidate(); } } QVariant ArtistsModel::getGenre() { if (!filter.hasGenre()) return QVariant(); return QString::fromStdString(filter.getGenre()); } void ArtistsModel::setGenre(const QVariant genre) { if (genre.isNull()) { if (filter.hasGenre()) { filter.unsetGenre(); invalidate(); } } else { const std::string std_genre = genre.value().toStdString(); if (!filter.hasGenre() || filter.getGenre() != std_genre) { filter.setGenre(std_genre); invalidate(); } } } int ArtistsModel::getLimit() { return -1; } void ArtistsModel::setLimit(int) { qWarning() << "Setting limit on ArtistsModel is deprecated"; } namespace { class ArtistRowData : public StreamingModel::RowData { public: ArtistRowData(std::vector &&rows) : rows(std::move(rows)) {} ~ArtistRowData() {} size_t size() const override { return rows.size(); } std::vector rows; }; } std::unique_ptr ArtistsModel::retrieveRows(std::shared_ptr store, int limit, int offset) const { auto limit_filter = filter; limit_filter.setLimit(limit); limit_filter.setOffset(offset); std::vector artists; if (album_artists) { artists = store->listAlbumArtists(limit_filter); } else { artists = store->listArtists(limit_filter); } return std::unique_ptr( new ArtistRowData(std::move(artists))); } void ArtistsModel::appendRows(std::unique_ptr &&row_data) { ArtistRowData *data = static_cast(row_data.get()); std::move(data->rows.begin(), data->rows.end(), std::back_inserter(results)); } void ArtistsModel::clearBacking() { results.clear(); } mediascanner2-0.115/src/qml/MediaScanner.0.1/ArtistsModel.h000066400000000000000000000041101436755250000232270ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef MEDIASCANNER_QML_ARTISTSMODEL_H #define MEDIASCANNER_QML_ARTISTSMODEL_H #include #include #include #include #include "StreamingModel.h" namespace mediascanner { namespace qml { class ArtistsModel : public StreamingModel { Q_OBJECT Q_ENUMS(Roles) Q_PROPERTY(bool albumArtists READ getAlbumArtists WRITE setAlbumArtists) Q_PROPERTY(QVariant genre READ getGenre WRITE setGenre) Q_PROPERTY(int limit READ getLimit WRITE setLimit) public: enum Roles { RoleArtist, }; explicit ArtistsModel(QObject *parent = 0); int rowCount(const QModelIndex &parent=QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; std::unique_ptr retrieveRows(std::shared_ptr store, int limit, int offset) const override; void appendRows(std::unique_ptr &&row_data) override; void clearBacking() override; protected: QHash roleNames() const override; bool getAlbumArtists(); void setAlbumArtists(bool album_artists); QVariant getGenre(); void setGenre(QVariant genre); int getLimit(); void setLimit(int limit); private: QHash roles; std::vector results; Filter filter; bool album_artists; }; } } #endif mediascanner2-0.115/src/qml/MediaScanner.0.1/CMakeLists.txt000066400000000000000000000034171436755250000232150ustar00rootroot00000000000000include_directories(${CMAKE_SOURCE_DIR}/src) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(QML_PLUGIN_DIR "${CMAKE_INSTALL_LIBDIR}/qt5/qml/MediaScanner.0.1") add_library(mediascanner-qml SHARED plugin.cpp MediaFileWrapper.cpp MediaFileWrapper.h MediaStoreWrapper.cpp MediaStoreWrapper.h StreamingModel.cpp StreamingModel.h MediaFileModelBase.cpp MediaFileModelBase.h AlbumModelBase.cpp AlbumModelBase.h AlbumsModel.cpp AlbumsModel.h ArtistsModel.cpp ArtistsModel.h GenresModel.cpp GenresModel.h SongsModel.cpp SongsModel.h SongsSearchModel.cpp SongsSearchModel.h ) target_compile_definitions(mediascanner-qml PRIVATE -DQT_NO_KEYWORDS) target_link_libraries(mediascanner-qml Qt5::Core Qt5::Gui Qt5::Qml Qt5::Concurrent Qt5::DBus PkgConfig::DBUSCPP mediascanner ms-dbus) install( TARGETS mediascanner-qml LIBRARY DESTINATION ${QML_PLUGIN_DIR} ) file(GLOB QMLFILES qmldir ) add_custom_target(mediascanner-qmlfiles ALL COMMAND cp ${QMLFILES} ${CMAKE_CURRENT_BINARY_DIR} DEPENDS ${QMLFILES} ) install( FILES ${QMLFILES} DESTINATION ${QML_PLUGIN_DIR} ) if(NOT CMAKE_CROSSCOMPILING) find_program(qmlplugindump_exe qmlplugindump) if(NOT qmlplugindump_exe) msg(FATAL_ERROR "Could not locate qmlplugindump.") endif() # qmlplugindump doesn't run reliably in the CI environment (it seems # to be instantiating the types, which fails when there is no media # database). So add a add_custom_target(update-qmltypes COMMAND QML2_IMPORT_PATH=${CMAKE_BINARY_DIR}/src/qml ${qmlplugindump_exe} -notrelocatable MediaScanner 0.1 > ${CMAKE_CURRENT_SOURCE_DIR}/plugin.qmltypes DEPENDS mediascanner-qml mediascanner-qmlfiles ) endif() install( FILES plugin.qmltypes DESTINATION ${QML_PLUGIN_DIR} ) mediascanner2-0.115/src/qml/MediaScanner.0.1/GenresModel.cpp000066400000000000000000000046541436755250000233710ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "GenresModel.h" #include using namespace mediascanner::qml; GenresModel::GenresModel(QObject *parent) : StreamingModel(parent) { roles[Roles::RoleGenre] = "genre"; } int GenresModel::rowCount(const QModelIndex &) const { return results.size(); } QVariant GenresModel::data(const QModelIndex &index, int role) const { if (index.row() < 0 || index.row() >= (ptrdiff_t)results.size()) { return QVariant(); } switch (role) { case RoleGenre: return QString::fromStdString(results[index.row()]); default: return QVariant(); } } QHash GenresModel::roleNames() const { return roles; } int GenresModel::getLimit() { return -1; } void GenresModel::setLimit(int) { qWarning() << "Setting limit on GenresModel is deprecated"; } namespace { class GenreRowData : public StreamingModel::RowData { public: GenreRowData(std::vector &&rows) : rows(std::move(rows)) {} ~GenreRowData() {} size_t size() const override { return rows.size(); } std::vector rows; }; } std::unique_ptr GenresModel::retrieveRows(std::shared_ptr store, int limit, int offset) const { auto limit_filter = filter; limit_filter.setLimit(limit); limit_filter.setOffset(offset); return std::unique_ptr( new GenreRowData(store->listGenres(limit_filter))); } void GenresModel::appendRows(std::unique_ptr &&row_data) { GenreRowData *data = static_cast(row_data.get()); std::move(data->rows.begin(), data->rows.end(), std::back_inserter(results)); } void GenresModel::clearBacking() { results.clear(); } mediascanner2-0.115/src/qml/MediaScanner.0.1/GenresModel.h000066400000000000000000000034751436755250000230360ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef MEDIASCANNER_QML_GENRESMODEL_H #define MEDIASCANNER_QML_GENRESMODEL_H #include #include #include #include #include #include "StreamingModel.h" namespace mediascanner { namespace qml { class GenresModel : public StreamingModel { Q_OBJECT Q_PROPERTY(int limit READ getLimit WRITE setLimit) Q_ENUMS(Roles) public: enum Roles { RoleGenre, }; explicit GenresModel(QObject *parent = 0); int rowCount(const QModelIndex &parent=QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; std::unique_ptr retrieveRows(std::shared_ptr store, int limit, int offset) const override; void appendRows(std::unique_ptr &&row_data) override; void clearBacking() override; protected: QHash roleNames() const override; int getLimit(); void setLimit(int limit); private: QHash roles; std::vector results; mediascanner::Filter filter; }; } } #endif mediascanner2-0.115/src/qml/MediaScanner.0.1/MediaFileModelBase.cpp000066400000000000000000000074651436755250000245630ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "MediaFileModelBase.h" #include "MediaFileWrapper.h" using namespace mediascanner::qml; MediaFileModelBase::MediaFileModelBase(QObject *parent) : StreamingModel(parent) { roles[Roles::RoleModelData] = "modelData"; roles[Roles::RoleFilename] = "filename"; roles[Roles::RoleUri] = "uri"; roles[Roles::RoleContentType] = "contentType"; roles[Roles::RoleETag] = "eTag"; roles[Roles::RoleTitle] = "title"; roles[Roles::RoleAuthor] = "author"; roles[Roles::RoleAlbum] = "album"; roles[Roles::RoleAlbumArtist] = "albumArtist"; roles[Roles::RoleDate] = "date"; roles[Roles::RoleGenre] = "genre"; roles[Roles::RoleDiscNumber] = "discNumber"; roles[Roles::RoleTrackNumber] = "trackNumber"; roles[Roles::RoleDuration] = "duration"; roles[Roles::RoleWidth] = "width"; roles[Roles::RoleHeight] = "height"; roles[Roles::RoleLatitude] = "latitude"; roles[Roles::RoleLongitude] = "longitude"; roles[Roles::RoleArt] = "art"; } int MediaFileModelBase::rowCount(const QModelIndex &) const { return results.size(); } QVariant MediaFileModelBase::data(const QModelIndex &index, int role) const { if (index.row() < 0 || index.row() >= (ptrdiff_t)results.size()) { return QVariant(); } const mediascanner::MediaFile &media = results[index.row()]; switch (role) { case RoleModelData: return QVariant::fromValue(new MediaFileWrapper(media)); case RoleFilename: return QString::fromStdString(media.getFileName()); case RoleUri: return QString::fromStdString(media.getUri()); case RoleContentType: return QString::fromStdString(media.getContentType()); case RoleETag: return QString::fromStdString(media.getETag()); case RoleTitle: return QString::fromStdString(media.getTitle()); case RoleAuthor: return QString::fromStdString(media.getAuthor()); case RoleAlbum: return QString::fromStdString(media.getAlbum()); case RoleAlbumArtist: return QString::fromStdString(media.getAlbumArtist()); case RoleDate: return QString::fromStdString(media.getDate()); case RoleGenre: return QString::fromStdString(media.getGenre()); case RoleDiscNumber: return media.getDiscNumber(); case RoleTrackNumber: return media.getTrackNumber(); case RoleDuration: return media.getDuration(); case RoleWidth: return media.getWidth(); case RoleHeight: return media.getHeight(); case RoleLatitude: return media.getLatitude(); case RoleLongitude: return media.getLongitude(); case RoleArt: return QString::fromStdString(media.getArtUri()); default: return QVariant(); } } QHash MediaFileModelBase::roleNames() const { return roles; } void MediaFileModelBase::appendRows(std::unique_ptr &&row_data) { MediaFileRowData *data = static_cast(row_data.get()); std::move(data->rows.begin(), data->rows.end(), std::back_inserter(results)); } void MediaFileModelBase::clearBacking() { results.clear(); } mediascanner2-0.115/src/qml/MediaScanner.0.1/MediaFileModelBase.h000066400000000000000000000044531436755250000242220ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef MEDIASCANNER_QML_MEDIAFILEMODELBASE_H #define MEDIASCANNER_QML_MEDIAFILEMODELBASE_H #include #include #include #include "StreamingModel.h" namespace mediascanner { namespace qml { class MediaFileModelBase : public StreamingModel { Q_OBJECT Q_ENUMS(Roles) public: enum Roles { RoleModelData, RoleFilename, RoleUri, RoleContentType, RoleETag, RoleTitle, RoleAuthor, RoleAlbum, RoleAlbumArtist, RoleDate, RoleGenre, RoleDiscNumber, RoleTrackNumber, RoleDuration, RoleWidth, RoleHeight, RoleLatitude, RoleLongitude, RoleArt, }; explicit MediaFileModelBase(QObject *parent = 0); int rowCount(const QModelIndex &parent=QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; void appendRows(std::unique_ptr &&row_data) override; void clearBacking() override; class MediaFileRowData : public RowData { public: MediaFileRowData(std::vector &&rows) : rows(std::move(rows)) {} ~MediaFileRowData() {} size_t size() const override { return rows.size(); } std::vector rows; }; protected: QHash roleNames() const override; void updateResults(const std::vector &results); private: QHash roles; std::vector results; }; } } #endif mediascanner2-0.115/src/qml/MediaScanner.0.1/MediaFileWrapper.cpp000066400000000000000000000052661436755250000243450ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "MediaFileWrapper.h" using namespace mediascanner::qml; MediaFileWrapper::MediaFileWrapper(const mediascanner::MediaFile &media, QObject *parent) : QObject(parent), media(media) { } QString MediaFileWrapper::filename() const { return QString::fromStdString(media.getFileName()); } QString MediaFileWrapper::uri() const { return QString::fromStdString(media.getUri()); } QString MediaFileWrapper::contentType() const { return QString::fromStdString(media.getContentType()); } QString MediaFileWrapper::eTag() const { return QString::fromStdString(media.getETag()); } QString MediaFileWrapper::title() const { return QString::fromStdString(media.getTitle()); } QString MediaFileWrapper::author() const { return QString::fromStdString(media.getAuthor()); } QString MediaFileWrapper::album() const { return QString::fromStdString(media.getAlbum()); } QString MediaFileWrapper::albumArtist() const { return QString::fromStdString(media.getAlbumArtist()); } QString MediaFileWrapper::date() const { return QString::fromStdString(media.getDate()); } QString MediaFileWrapper::genre() const { return QString::fromStdString(media.getGenre()); } int MediaFileWrapper::discNumber() const { return media.getDiscNumber(); } int MediaFileWrapper::trackNumber() const { return media.getTrackNumber(); } int MediaFileWrapper::duration() const { return media.getDuration(); } int MediaFileWrapper::width() const { return media.getWidth(); } int MediaFileWrapper::height() const { return media.getHeight(); } double MediaFileWrapper::latitude() const { return media.getLatitude(); } double MediaFileWrapper::longitude() const { return media.getLongitude(); } bool MediaFileWrapper::hasThumbnail() const { return media.getHasThumbnail(); } uint64_t MediaFileWrapper::modificationTime() const { return media.getModificationTime(); } QString MediaFileWrapper::art() const { return QString::fromStdString(media.getArtUri()); } mediascanner2-0.115/src/qml/MediaScanner.0.1/MediaFileWrapper.h000066400000000000000000000053361436755250000240100ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef MEDIASCANNER_QML_MEDIAFILEWRAPPER_H #define MEDIASCANNER_QML_MEDIAFILEWRAPPER_H #include #include #include namespace mediascanner { namespace qml { class MediaFileWrapper : public QObject { Q_OBJECT Q_PROPERTY(QString filename READ filename CONSTANT) Q_PROPERTY(QString uri READ uri CONSTANT) Q_PROPERTY(QString contentType READ contentType CONSTANT) Q_PROPERTY(QString eTag READ eTag CONSTANT) Q_PROPERTY(QString title READ title CONSTANT) Q_PROPERTY(QString author READ author CONSTANT) Q_PROPERTY(QString album READ album CONSTANT) Q_PROPERTY(QString albumArtist READ albumArtist CONSTANT) Q_PROPERTY(QString date READ date CONSTANT) Q_PROPERTY(QString genre READ genre CONSTANT) Q_PROPERTY(int discNumber READ discNumber CONSTANT) Q_PROPERTY(int trackNumber READ trackNumber CONSTANT) Q_PROPERTY(int duration READ duration CONSTANT) Q_PROPERTY(int width READ width CONSTANT) Q_PROPERTY(int height READ height CONSTANT) Q_PROPERTY(double latitude READ latitude CONSTANT) Q_PROPERTY(double longitude READ longitude CONSTANT) Q_PROPERTY(bool hasThumbnail READ hasThumbnail CONSTANT) Q_PROPERTY(uint64_t modificationTime READ modificationTime CONSTANT) Q_PROPERTY(QString art READ art CONSTANT) public: MediaFileWrapper(const mediascanner::MediaFile &media, QObject *parent=0); QString filename() const; QString uri() const; QString contentType() const; QString eTag() const; QString title() const; QString author() const; QString album() const; QString albumArtist() const; QString date() const; QString genre() const; int discNumber() const; int trackNumber() const; int duration() const; int width() const; int height() const; double latitude() const; double longitude() const; bool hasThumbnail() const; uint64_t modificationTime() const; QString art() const; private: const mediascanner::MediaFile media; }; } } #endif mediascanner2-0.115/src/qml/MediaScanner.0.1/MediaStoreWrapper.cpp000066400000000000000000000065751436755250000245660ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "MediaStoreWrapper.h" #include #include #include #include #include #include #include #include #include #include using namespace mediascanner::qml; static core::dbus::Bus::Ptr the_session_bus() { static core::dbus::Bus::Ptr bus = std::make_shared( core::dbus::WellKnownBus::session); static core::dbus::Executor::Ptr executor = core::dbus::asio::make_executor(bus); static std::once_flag once; std::call_once(once, []() {bus->install_executor(executor);}); return bus; } MediaStoreWrapper::MediaStoreWrapper(QObject *parent) : QObject(parent) { const char *use_dbus = getenv("MEDIASCANNER_USE_DBUS"); try { if (use_dbus != nullptr && !strcmp(use_dbus, "1")) { store.reset(new mediascanner::dbus::ServiceStub(the_session_bus())); } else { store.reset(new mediascanner::MediaStore(MS_READ_ONLY)); } } catch (const std::exception &e) { qWarning() << "Could not initialise media store:" << e.what(); } QDBusConnection::sessionBus().connect( "com.canonical.MediaScanner2.Daemon", "/com/canonical/unity/scopes", "com.canonical.unity.scopes", "InvalidateResults", QStringList{"mediascanner-music"}, "s", this, SLOT(resultsInvalidated())); } QList MediaStoreWrapper::query(const QString &q, MediaType type) { if (!store) { qWarning() << "query() called on invalid MediaStore"; return QList(); } QList result; try { for (const auto &media : store->query(q.toStdString(), static_cast(type), mediascanner::Filter())) { auto wrapper = new MediaFileWrapper(media); QQmlEngine::setObjectOwnership(wrapper, QQmlEngine::JavaScriptOwnership); result.append(wrapper); } } catch (const std::exception &e) { qWarning() << "Failed to retrieve query results:" << e.what(); } return result; } MediaFileWrapper *MediaStoreWrapper::lookup(const QString &filename) { if (!store) { qWarning() << "lookup() called on invalid MediaStore"; return nullptr; } MediaFileWrapper *wrapper; try { wrapper = new MediaFileWrapper(store->lookup(filename.toStdString())); } catch (std::exception &e) { return nullptr; } QQmlEngine::setObjectOwnership(wrapper, QQmlEngine::JavaScriptOwnership); return wrapper; } void MediaStoreWrapper::resultsInvalidated() { Q_EMIT updated(); } mediascanner2-0.115/src/qml/MediaScanner.0.1/MediaStoreWrapper.h000066400000000000000000000031741436755250000242230ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef MEDIASCANNER_QML_MEDIASTOREWRAPPER_H #define MEDIASCANNER_QML_MEDIASTOREWRAPPER_H #include #include #include #include #include #include "MediaFileWrapper.h" namespace mediascanner { namespace qml { class MediaStoreWrapper : public QObject { Q_OBJECT Q_ENUMS(MediaType) public: enum MediaType { AudioMedia = mediascanner::AudioMedia, VideoMedia = mediascanner::VideoMedia, ImageMedia = mediascanner::ImageMedia, AllMedia = mediascanner::AllMedia, }; MediaStoreWrapper(QObject *parent=0); Q_INVOKABLE QList query(const QString &q, MediaType type); Q_INVOKABLE mediascanner::qml::MediaFileWrapper *lookup(const QString &filename); std::shared_ptr store; Q_SIGNALS: void updated(); private Q_SLOTS: void resultsInvalidated(); }; } } #endif mediascanner2-0.115/src/qml/MediaScanner.0.1/SongsModel.cpp000066400000000000000000000072541436755250000232360ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "SongsModel.h" #include using namespace mediascanner::qml; SongsModel::SongsModel(QObject *parent) : MediaFileModelBase(parent) { } QVariant SongsModel::getArtist() { if (!filter.hasArtist()) return QVariant(); return QString::fromStdString(filter.getArtist()); } void SongsModel::setArtist(const QVariant artist) { if (artist.isNull()) { if (filter.hasArtist()) { filter.unsetArtist(); invalidate(); } } else { const std::string std_artist = artist.value().toStdString(); if (!filter.hasArtist() || filter.getArtist() != std_artist) { filter.setArtist(std_artist); invalidate(); } } } QVariant SongsModel::getAlbum() { if (!filter.hasAlbum()) return QVariant(); return QString::fromStdString(filter.getAlbum()); } void SongsModel::setAlbum(const QVariant album) { if (album.isNull()) { if (filter.hasAlbum()) { filter.unsetAlbum(); invalidate(); } } else { const std::string std_album = album.value().toStdString(); if (!filter.hasAlbum() || filter.getAlbum() != std_album) { filter.setAlbum(std_album); invalidate(); } } } QVariant SongsModel::getAlbumArtist() { if (!filter.hasAlbumArtist()) return QVariant(); return QString::fromStdString(filter.getAlbumArtist()); } void SongsModel::setAlbumArtist(const QVariant album_artist) { if (album_artist.isNull()) { if (filter.hasAlbumArtist()) { filter.unsetAlbumArtist(); invalidate(); } } else { const std::string std_album_artist = album_artist.value().toStdString(); if (!filter.hasAlbumArtist() || filter.getAlbumArtist() != std_album_artist) { filter.setAlbumArtist(std_album_artist); invalidate(); } } } QVariant SongsModel::getGenre() { if (!filter.hasGenre()) return QVariant(); return QString::fromStdString(filter.getGenre()); } void SongsModel::setGenre(const QVariant genre) { if (genre.isNull()) { if (filter.hasGenre()) { filter.unsetGenre(); invalidate(); } } else { const std::string std_genre = genre.value().toStdString(); if (!filter.hasGenre() || filter.getGenre() != std_genre) { filter.setGenre(std_genre); invalidate(); } } } int SongsModel::getLimit() { return -1; } void SongsModel::setLimit(int) { qWarning() << "Setting limit on SongsModel is deprecated"; } std::unique_ptr SongsModel::retrieveRows(std::shared_ptr store, int limit, int offset) const { auto limit_filter = filter; limit_filter.setLimit(limit); limit_filter.setOffset(offset); return std::unique_ptr( new MediaFileRowData(store->listSongs(limit_filter))); } mediascanner2-0.115/src/qml/MediaScanner.0.1/SongsModel.h000066400000000000000000000035731436755250000227030ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef MEDIASCANNER_QML_SONGSMODEL_H #define MEDIASCANNER_QML_SONGSMODEL_H #include #include #include "MediaStoreWrapper.h" #include "MediaFileModelBase.h" namespace mediascanner { namespace qml { class SongsModel : public MediaFileModelBase { Q_OBJECT Q_PROPERTY(QVariant artist READ getArtist WRITE setArtist) Q_PROPERTY(QVariant album READ getAlbum WRITE setAlbum) Q_PROPERTY(QVariant albumArtist READ getAlbumArtist WRITE setAlbumArtist) Q_PROPERTY(QVariant genre READ getGenre WRITE setGenre) Q_PROPERTY(int limit READ getLimit WRITE setLimit) public: explicit SongsModel(QObject *parent=0); std::unique_ptr retrieveRows(std::shared_ptr store, int limit, int offset) const override; protected: QVariant getArtist(); void setArtist(const QVariant artist); QVariant getAlbum(); void setAlbum(const QVariant album); QVariant getAlbumArtist(); void setAlbumArtist(const QVariant album_artist); QVariant getGenre(); void setGenre(const QVariant genre); int getLimit(); void setLimit(int limit); private: Filter filter; }; } } #endif mediascanner2-0.115/src/qml/MediaScanner.0.1/SongsSearchModel.cpp000066400000000000000000000031111436755250000243500ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include "SongsSearchModel.h" #include #include using namespace mediascanner::qml; SongsSearchModel::SongsSearchModel(QObject *parent) : MediaFileModelBase(parent), query("") { } QString SongsSearchModel::getQuery() { return query; } void SongsSearchModel::setQuery(const QString query) { if (this->query != query) { this->query = query; invalidate(); } } std::unique_ptr SongsSearchModel::retrieveRows(std::shared_ptr store, int limit, int offset) const { std::vector songs; mediascanner::Filter limit_filter; limit_filter.setLimit(limit); limit_filter.setOffset(offset); return std::unique_ptr( new MediaFileRowData(store->query(query.toStdString(), mediascanner::AudioMedia, limit_filter))); } mediascanner2-0.115/src/qml/MediaScanner.0.1/SongsSearchModel.h000066400000000000000000000025401436755250000240220ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef MEDIASCANNER_QML_SONGSSEARCHMODEL_H #define MEDIASCANNER_QML_SONGSSEARCHMODEL_H #include #include "MediaStoreWrapper.h" #include "MediaFileModelBase.h" namespace mediascanner { namespace qml { class SongsSearchModel : public MediaFileModelBase { Q_OBJECT Q_PROPERTY(QString query READ getQuery WRITE setQuery) public: explicit SongsSearchModel(QObject *parent=0); std::unique_ptr retrieveRows(std::shared_ptr store, int limit, int offset) const override; protected: QString getQuery(); void setQuery(const QString query); private: QString query; }; } } #endif mediascanner2-0.115/src/qml/MediaScanner.0.1/StreamingModel.cpp000066400000000000000000000117711436755250000240750ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "StreamingModel.h" #include #include #include #include #include #include using namespace mediascanner::qml; namespace { const int BATCH_SIZE = 200; class AdditionEvent : public QEvent { private: std::unique_ptr rows; bool error = false; int generation; public: AdditionEvent(int generation) : QEvent(AdditionEvent::additionEventType()), generation(generation) { } void setRows(std::unique_ptr &&r) { rows = std::move(r); } void setError(bool e) { error = e; } std::unique_ptr& getRows() { return rows; } bool getError() const { return error; } int getGeneration() const { return generation; } static QEvent::Type additionEventType() { static QEvent::Type type = static_cast(QEvent::registerEventType()); return type; } }; void runQuery(int generation, StreamingModel *model, std::shared_ptr store) { if (!store) { return; } int offset = 0; int cursize; do { if(model->shouldWorkerStop()) { return; } QScopedPointer e(new AdditionEvent(generation)); try { e->setRows(model->retrieveRows(store, BATCH_SIZE, offset)); } catch (const std::exception &exc) { qWarning() << "Failed to retrieve rows:" << exc.what(); e->setError(true); return; } cursize = e->getRows()->size(); if (model->shouldWorkerStop()) { return; } QCoreApplication::instance()->postEvent(model, e.take()); offset += cursize; } while(cursize >= BATCH_SIZE); } } StreamingModel::StreamingModel(QObject *parent) : QAbstractListModel(parent), generation(0), status(Ready) { } StreamingModel::~StreamingModel() { setWorkerStop(true); try { query_future.waitForFinished(); } catch(...) { qWarning() << "Unknown error when shutting down worker thread.\n"; } } void StreamingModel::updateModel() { if (store.isNull() || !store->store) { query_future = QFuture(); setStatus(Ready); return; } setStatus(Loading); setWorkerStop(false); query_future = QtConcurrent::run(runQuery, ++generation, this, store->store); } QVariant StreamingModel::get(int row, int role) const { return data(index(row, 0), role); } bool StreamingModel::event(QEvent *e) { if (e->type() != AdditionEvent::additionEventType()) { return QObject::event(e); } AdditionEvent *ae = dynamic_cast(e); assert(ae); // Old results may be in Qt's event queue and get delivered // after we have moved to a new query. Ignore them. if(ae->getGeneration() != generation) { return true; } if (ae->getError()) { setStatus(Error); return true; } auto &newrows = ae->getRows(); bool lastBatch = newrows->size() < BATCH_SIZE; beginInsertRows(QModelIndex(), rowCount(), rowCount()+newrows->size()-1); appendRows(std::move(newrows)); endInsertRows(); Q_EMIT countChanged(); if (lastBatch) { setStatus(Ready); } return true; } MediaStoreWrapper *StreamingModel::getStore() const { return store.data(); } void StreamingModel::setStore(MediaStoreWrapper *store) { if (this->store != store) { if (this->store) { disconnect(this->store, &MediaStoreWrapper::updated, this, &StreamingModel::invalidate); } this->store = store; if (store) { connect(this->store, &MediaStoreWrapper::updated, this, &StreamingModel::invalidate); } invalidate(); } } StreamingModel::ModelStatus StreamingModel::getStatus() const { return status; } void StreamingModel::setStatus(StreamingModel::ModelStatus status) { this->status = status; Q_EMIT statusChanged(); if (status == Ready) { Q_EMIT filled(); } } void StreamingModel::invalidate() { setWorkerStop(true); query_future.waitForFinished(); beginResetModel(); clearBacking(); endResetModel(); Q_EMIT countChanged(); updateModel(); } mediascanner2-0.115/src/qml/MediaScanner.0.1/StreamingModel.h000066400000000000000000000053631436755250000235420ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef MEDIASCANNER_QML_STREAMINGMODEL_H #define MEDIASCANNER_QML_STREAMINGMODEL_H #include #include #include #include #include #include "MediaStoreWrapper.h" namespace mediascanner { namespace qml { class StreamingModel : public QAbstractListModel { Q_OBJECT Q_ENUMS(ModelStatus) Q_PROPERTY(mediascanner::qml::MediaStoreWrapper* store READ getStore WRITE setStore) Q_PROPERTY(int count READ rowCount NOTIFY countChanged) Q_PROPERTY(int rowCount READ rowCount NOTIFY countChanged) Q_PROPERTY(ModelStatus status READ getStatus NOTIFY statusChanged) public: enum ModelStatus { Ready, Loading, Error }; explicit StreamingModel(QObject *parent=nullptr); virtual ~StreamingModel(); Q_INVOKABLE QVariant get(int row, int role) const; bool event(QEvent *e) override; bool shouldWorkerStop() const noexcept { return stopflag.load(std::memory_order_consume); } // Subclasses should implement the following, along with rowCount and data class RowData { public: virtual ~RowData() {} virtual size_t size() const = 0; }; virtual std::unique_ptr retrieveRows(std::shared_ptr store, int limit, int offset) const = 0; virtual void appendRows(std::unique_ptr &&row_data) = 0; virtual void clearBacking() = 0; protected: MediaStoreWrapper *getStore() const; void setStore(MediaStoreWrapper *store); ModelStatus getStatus() const; void setStatus(ModelStatus status); private: void updateModel(); void setWorkerStop(bool new_stop_status) noexcept { stopflag.store(new_stop_status, std::memory_order_release); } QPointer store; QFuture query_future; int generation; std::atomic stopflag; ModelStatus status; Q_SIGNALS: void countChanged(); void statusChanged(); // This next signal is here for backwards compatibility void filled(); public Q_SLOTS: void invalidate(); }; } } #endif mediascanner2-0.115/src/qml/MediaScanner.0.1/plugin.cpp000066400000000000000000000037141436755250000224570ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * James Henstridge * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #include #include #include "MediaFileWrapper.h" #include "MediaStoreWrapper.h" #include "AlbumsModel.h" #include "ArtistsModel.h" #include "GenresModel.h" #include "SongsModel.h" #include "SongsSearchModel.h" class Q_DECL_EXPORT MediaScannerPlugin : public QQmlExtensionPlugin { Q_OBJECT Q_PLUGIN_METADATA(IID "MediaScaner") public: virtual ~MediaScannerPlugin() {} void initializeEngine(QQmlEngine*, const char* uri){ Q_ASSERT(uri == QLatin1String("MediaScaner")); qmlRegisterModule(uri, 0, 1); } void registerTypes(const char *uri) { using namespace mediascanner::qml; Q_ASSERT(uri == QLatin1String("MediaScaner")); qmlRegisterType(uri, 0, 1, "MediaStore"); qmlRegisterUncreatableType(uri, 0, 1, "MediaFile", "Use a MediaStore to retrieve MediaFiles"); qmlRegisterType(uri, 0, 1, "AlbumsModel"); qmlRegisterType(uri, 0, 1, "ArtistsModel"); qmlRegisterType(uri, 0, 1, "GenresModel"); qmlRegisterType(uri, 0, 1, "SongsModel"); qmlRegisterType(uri, 0, 1, "SongsSearchModel"); } }; #include "plugin.moc" mediascanner2-0.115/src/qml/MediaScanner.0.1/plugin.qmltypes000066400000000000000000000151621436755250000235530ustar00rootroot00000000000000import QtQuick.tooling 1.1 // This file describes the plugin-supplied types contained in the library. // It is used for QML tooling purposes only. // // This file was auto-generated by: // 'qmlplugindump -notrelocatable MediaScanner 0.1' Module { Component { name: "mediascanner::qml::AlbumModelBase" prototype: "mediascanner::qml::StreamingModel" Enum { name: "Roles" values: { "RoleTitle": 0, "RoleArtist": 1, "RoleDate": 2, "RoleGenre": 3, "RoleArt": 4 } } } Component { name: "mediascanner::qml::AlbumsModel" prototype: "mediascanner::qml::AlbumModelBase" exports: ["MediaScanner/AlbumsModel 0.1"] exportMetaObjectRevisions: [0] Property { name: "artist"; type: "QVariant" } Property { name: "albumArtist"; type: "QVariant" } Property { name: "genre"; type: "QVariant" } Property { name: "limit"; type: "int" } } Component { name: "mediascanner::qml::ArtistsModel" prototype: "mediascanner::qml::StreamingModel" exports: ["MediaScanner/ArtistsModel 0.1"] exportMetaObjectRevisions: [0] Enum { name: "Roles" values: { "RoleArtist": 0 } } Property { name: "albumArtists"; type: "bool" } Property { name: "genre"; type: "QVariant" } Property { name: "limit"; type: "int" } } Component { name: "mediascanner::qml::GenresModel" prototype: "mediascanner::qml::StreamingModel" exports: ["MediaScanner/GenresModel 0.1"] exportMetaObjectRevisions: [0] Enum { name: "Roles" values: { "RoleGenre": 0 } } Property { name: "limit"; type: "int" } } Component { name: "mediascanner::qml::MediaFileModelBase" prototype: "mediascanner::qml::StreamingModel" Enum { name: "Roles" values: { "RoleModelData": 0, "RoleFilename": 1, "RoleUri": 2, "RoleContentType": 3, "RoleETag": 4, "RoleTitle": 5, "RoleAuthor": 6, "RoleAlbum": 7, "RoleAlbumArtist": 8, "RoleDate": 9, "RoleGenre": 10, "RoleDiscNumber": 11, "RoleTrackNumber": 12, "RoleDuration": 13, "RoleWidth": 14, "RoleHeight": 15, "RoleLatitude": 16, "RoleLongitude": 17, "RoleArt": 18 } } } Component { name: "mediascanner::qml::MediaFileWrapper" prototype: "QObject" exports: ["MediaScanner/MediaFile 0.1"] isCreatable: false exportMetaObjectRevisions: [0] Property { name: "filename"; type: "string"; isReadonly: true } Property { name: "uri"; type: "string"; isReadonly: true } Property { name: "contentType"; type: "string"; isReadonly: true } Property { name: "eTag"; type: "string"; isReadonly: true } Property { name: "title"; type: "string"; isReadonly: true } Property { name: "author"; type: "string"; isReadonly: true } Property { name: "album"; type: "string"; isReadonly: true } Property { name: "albumArtist"; type: "string"; isReadonly: true } Property { name: "date"; type: "string"; isReadonly: true } Property { name: "genre"; type: "string"; isReadonly: true } Property { name: "discNumber"; type: "int"; isReadonly: true } Property { name: "trackNumber"; type: "int"; isReadonly: true } Property { name: "duration"; type: "int"; isReadonly: true } Property { name: "width"; type: "int"; isReadonly: true } Property { name: "height"; type: "int"; isReadonly: true } Property { name: "latitude"; type: "double"; isReadonly: true } Property { name: "longitude"; type: "double"; isReadonly: true } Property { name: "hasThumbnail"; type: "bool"; isReadonly: true } Property { name: "art"; type: "string"; isReadonly: true } } Component { name: "mediascanner::qml::MediaStoreWrapper" prototype: "QObject" exports: ["MediaScanner/MediaStore 0.1"] exportMetaObjectRevisions: [0] Enum { name: "MediaType" values: { "AudioMedia": 1, "VideoMedia": 2, "ImageMedia": 3, "AllMedia": 255 } } Signal { name: "updated" } Method { name: "query" type: "QList" Parameter { name: "q"; type: "string" } Parameter { name: "type"; type: "MediaType" } } Method { name: "lookup" type: "mediascanner::qml::MediaFileWrapper*" Parameter { name: "filename"; type: "string" } } } Component { name: "mediascanner::qml::SongsModel" prototype: "mediascanner::qml::MediaFileModelBase" exports: ["MediaScanner/SongsModel 0.1"] exportMetaObjectRevisions: [0] Property { name: "artist"; type: "QVariant" } Property { name: "album"; type: "QVariant" } Property { name: "albumArtist"; type: "QVariant" } Property { name: "genre"; type: "QVariant" } Property { name: "limit"; type: "int" } } Component { name: "mediascanner::qml::SongsSearchModel" prototype: "mediascanner::qml::MediaFileModelBase" exports: ["MediaScanner/SongsSearchModel 0.1"] exportMetaObjectRevisions: [0] Property { name: "query"; type: "string" } } Component { name: "mediascanner::qml::StreamingModel" prototype: "QAbstractListModel" Enum { name: "ModelStatus" values: { "Ready": 0, "Loading": 1, "Error": 2 } } Property { name: "store"; type: "mediascanner::qml::MediaStoreWrapper"; isPointer: true } Property { name: "count"; type: "int"; isReadonly: true } Property { name: "rowCount"; type: "int"; isReadonly: true } Property { name: "status"; type: "ModelStatus"; isReadonly: true } Signal { name: "filled" } Method { name: "invalidate" } Method { name: "get" type: "QVariant" Parameter { name: "row"; type: "int" } Parameter { name: "role"; type: "int" } } } } mediascanner2-0.115/src/qml/MediaScanner.0.1/qmldir000066400000000000000000000001051436755250000216570ustar00rootroot00000000000000module MediaScanner plugin mediascanner-qml typeinfo plugin.qmltypes mediascanner2-0.115/src/utils/000077500000000000000000000000001436755250000161715ustar00rootroot00000000000000mediascanner2-0.115/src/utils/CMakeLists.txt000066400000000000000000000004631436755250000207340ustar00rootroot00000000000000add_definitions(${MEDIASCANNER_DEPS_CFLAGS}) include_directories(..) add_executable(watcher watchertest.cc) add_executable(query query.cc) target_link_libraries(query mediascanner ${MEDIASCANNER_DEPS_LDFLAGS}) add_executable(mountwatcher mountwatcher.cc) target_link_libraries(mountwatcher scannerstuff) mediascanner2-0.115/src/utils/mountwatcher.cc000066400000000000000000000023661436755250000212270ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include "../daemon/MountWatcher.hh" void mount_event(const mediascanner::MountWatcher::Info &info) { printf("Filesystem %s\n", info.is_mounted ? "mounted" : "unmounted"); printf(" Device: %s\n", info.device.c_str()); printf(" UUID: %s\n", info.uuid.c_str()); printf(" Mount Point: %s\n", info.mount_point.c_str()); printf("\n"); } int main(int, char **) { mediascanner::MountWatcher watcher(mount_event); GMainLoop *main = g_main_loop_new(nullptr, false); g_main_loop_run(main); } mediascanner2-0.115/src/utils/query.cc000066400000000000000000000033611436755250000176500ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "mediascanner/Filter.hh" #include "mediascanner/MediaFile.hh" #include "mediascanner/MediaStore.hh" #include #include #include #include using namespace std; using namespace mediascanner; void queryDb(const string &core_term) { MediaStore store(MS_READ_ONLY); vector results; results = store.query(core_term, AudioMedia, Filter()); if(results.empty()) { printf("No audio matches.\n"); } else { printf("Audio matches:\n"); } for(const auto &i : results) { printf("Filename: %s\n", i.getFileName().c_str()); } results = store.query(core_term, VideoMedia, Filter()); if(results.empty()) { printf("No video matches.\n"); } else { printf("Video matches:\n"); } for(const auto &i : results) { printf("Filename: %s\n", i.getFileName().c_str()); } } int main(int argc, char **argv) { if(argc < 2) { printf("%s \n", argv[0]); return 1; } string core_term(argv[1]); queryDb(core_term); } mediascanner2-0.115/src/utils/watchertest.cc000066400000000000000000000042271436755250000210420ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include using namespace std; #define BUFSIZE 4096 int main(int /*argc*/, char **/*argv*/) { string dbdir = "/home/"; dbdir += getlogin(); dbdir += "/.cache/mediascanner-test"; int ifd; int wd; char buf[BUFSIZE]; ifd = inotify_init(); if(ifd == -1) { printf("Inotify init failed.\n"); return 1; } wd = inotify_add_watch(ifd, dbdir.c_str(), IN_CREATE | IN_DELETE | IN_MODIFY); if(wd == -1) { printf("Could not create watch for data dir.\n"); return 1; } printf("This program simulates invalidation of query results as media files come and go.\n\n"); while(true) { ssize_t num_read; num_read = read(ifd, buf, BUFSIZE); if(num_read == 0) { printf("Inotify returned 0.\n"); return 1; } if(num_read == -1) { printf("Read error.\n"); return 1; } for(char *p = buf; p < buf + num_read;) { struct inotify_event *event = (struct inotify_event *) p; if(event->mask & IN_CREATE) { printf("Invalidation: create\n"); } else if(event->mask & IN_DELETE) { printf("Invalidation: delete\n"); } else { printf("Invalidation: modify %s\n", event->name); } p += sizeof(struct inotify_event) + event->len; } } return 0; } mediascanner2-0.115/test/000077500000000000000000000000001436755250000152215ustar00rootroot00000000000000mediascanner2-0.115/test/CMakeLists.txt000066400000000000000000000074001436755250000177620ustar00rootroot00000000000000find_package(GMock REQUIRED) set(TEST_LIBS ${GTEST_BOTH_LIBRARIES} ) add_definitions(${MEDIASCANNER_DEPS_CFLAGS} ${GST_CFLAGS} ${DBUSCPP_CFLAGS} -DQT_NO_KEYWORDS) include_directories(../src) configure_file(test_config.h.in test_config.h) include_directories(${CMAKE_CURRENT_BINARY_DIR}) configure_file( services/com.canonical.MediaScanner2.service.in services/com.canonical.MediaScanner2.service) configure_file( services/com.canonical.MediaScanner2.Extractor.service.in services/com.canonical.MediaScanner2.Extractor.service) add_executable(basic basic.cc) target_link_libraries(basic mediascanner scannerstuff ${TEST_LIBS}) add_test(basic basic) # The gvfs modules interfere with the private D-Bus test fixtures set_tests_properties(basic PROPERTIES ENVIRONMENT "GIO_MODULE_DIR=${CMAKE_CURRENT_BINARY_DIR}/modules") add_executable(test_mediastore test_mediastore.cc ../src/mediascanner/utils.cc) target_link_libraries(test_mediastore mediascanner ${TEST_LIBS}) add_test(test_mediastore test_mediastore) add_executable(test_extractorbackend test_extractorbackend.cc) target_link_libraries(test_extractorbackend extractor-backend ${TEST_LIBS}) add_test(test_extractorbackend test_extractorbackend) add_executable(test_metadataextractor test_metadataextractor.cc) target_link_libraries(test_metadataextractor extractor-client ${TEST_LIBS}) add_test(test_metadataextractor test_metadataextractor) # The gvfs modules interfere with the private D-Bus test fixtures set_tests_properties(test_metadataextractor PROPERTIES ENVIRONMENT "GIO_MODULE_DIR=${CMAKE_CURRENT_BINARY_DIR}/modules") add_executable(test_subtreewatcher test_subtreewatcher.cc) target_link_libraries(test_subtreewatcher mediascanner scannerstuff ${TEST_LIBS}) add_test(test_subtreewatcher test_subtreewatcher) # The gvfs modules interfere with the private D-Bus test fixtures set_tests_properties(test_subtreewatcher PROPERTIES ENVIRONMENT "GIO_MODULE_DIR=${CMAKE_CURRENT_BINARY_DIR}/modules") add_executable(test_volumemanager test_volumemanager.cc) target_link_libraries(test_volumemanager mediascanner scannerstuff ${TEST_LIBS}) add_test(test_volumemanager test_volumemanager) # The gvfs modules interfere with the private D-Bus test fixtures set_tests_properties(test_volumemanager PROPERTIES ENVIRONMENT "GIO_MODULE_DIR=${CMAKE_CURRENT_BINARY_DIR}/modules") add_executable(test_sqliteutils test_sqliteutils.cc) target_link_libraries(test_sqliteutils ${TEST_LIBS} ${MEDIASCANNER_DEPS_LDFLAGS}) add_test(test_sqliteutils test_sqliteutils) add_executable(test_mfbuilder test_mfbuilder.cc) target_link_libraries(test_mfbuilder ${TEST_LIBS} mediascanner) add_test(test_mfbuilder test_mfbuilder) add_executable(test_dbus test_dbus.cc) target_link_libraries(test_dbus ${TEST_LIBS} mediascanner ms-dbus) add_test(test_dbus test_dbus) add_executable(test_qml test_qml.cc) target_link_libraries(test_qml mediascanner Qt5::Test Qt5::QuickTest) add_test(test_qml test_qml -import ${CMAKE_BINARY_DIR}/src/qml) set_tests_properties(test_qml PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=minimal;QT_SELECT=qt5" TIMEOUT 600) add_test(test_qml_dbus test_qml -import ${CMAKE_BINARY_DIR}/src/qml) set_tests_properties(test_qml_dbus PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=minimal;MEDIASCANNER_USE_DBUS=1;QT_SELECT=qt5" TIMEOUT 600) add_test(test_qml_nodb qmltestrunner -import ${CMAKE_BINARY_DIR}/src/qml -input ${CMAKE_CURRENT_SOURCE_DIR}/test_qml_nodb.qml) set_tests_properties(test_qml_nodb PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=minimal;QT_SELECT=qt5;MEDIASCANNER_CACHEDIR=${CMAKE_CURRENT_BINARY_DIR}/qml-nodb-cachedir;DBUS_SESSION_BUS_ADDRESS=" TIMEOUT 600) add_executable(test_util test_util.cc ../src/mediascanner/utils.cc) target_link_libraries(test_util ${TEST_LIBS} ${GLIB_LDFLAGS}) add_test(test_util test_util) mediascanner2-0.115/test/basic.cc000066400000000000000000000273411436755250000166200ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include "test_config.h" #include #include #include #include #include #include #include using namespace std; using namespace mediascanner; class ScanTest : public ::testing::Test { protected: ScanTest() { } virtual ~ScanTest() { } virtual void SetUp() override{ test_dbus_.reset(g_test_dbus_new(G_TEST_DBUS_NONE)); g_test_dbus_add_service_dir(test_dbus_.get(), TEST_DIR "/services"); } virtual void TearDown() override { session_bus_.reset(); test_dbus_.reset(); } GDBusConnection *session_bus() { if (!bus_started_) { g_test_dbus_up(test_dbus_.get()); GError *error = nullptr; char *address = g_dbus_address_get_for_bus_sync(G_BUS_TYPE_SESSION, nullptr, &error); if (!address) { std::string errortxt(error->message); g_error_free(error); throw std::runtime_error( std::string("Failed to determine session bus address: ") + errortxt); } session_bus_.reset(g_dbus_connection_new_for_address_sync( address, static_cast(G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION), nullptr, nullptr, &error)); g_free(address); if (!session_bus_) { std::string errortxt(error->message); g_error_free(error); throw std::runtime_error( std::string("Failed to connect to session bus: ") + errortxt); } bus_started_ = true; } return session_bus_.get(); } private: unique_ptr test_dbus_ {nullptr, g_object_unref}; unique_ptr session_bus_ {nullptr, g_object_unref}; bool bus_started_ = false; }; TEST_F(ScanTest, init) { MediaStore store(":memory:", MS_READ_WRITE); MetadataExtractor extractor(session_bus()); InvalidationSender invalidator; SubtreeWatcher watcher(store, extractor, invalidator); } void clear_dir(const string &subdir) { string cmd = "rm -rf " + subdir; ASSERT_EQ(system(cmd.c_str()), 0); // Because I like to live dangerously, that's why. } void copy_file(const string &src, const string &dst) { FILE* f = fopen(src.c_str(), "r"); ASSERT_TRUE(f); fseek(f, 0, SEEK_END); size_t size = ftell(f); char* buf = new char[size]; fseek(f, 0, SEEK_SET); ASSERT_EQ(fread(buf, 1, size, f), size); fclose(f); f = fopen(dst.c_str(), "w"); ASSERT_TRUE(f); ASSERT_EQ(fwrite(buf, 1, size, f), size); fclose(f); delete[] buf; } void iterate_main_loop() { while (g_main_context_iteration(nullptr, FALSE)) { } } TEST_F(ScanTest, index) { string subdir = TEST_DIR "/testdir"; string testfile = SOURCE_DIR "/media/testfile.ogg"; string outfile = subdir + "/testfile.ogg"; clear_dir(subdir); ASSERT_GE(mkdir(subdir.c_str(), S_IRUSR | S_IWUSR | S_IXUSR), 0); MediaStore store(":memory:", MS_READ_WRITE); MetadataExtractor extractor(session_bus()); InvalidationSender invalidator; SubtreeWatcher watcher(store, extractor, invalidator); watcher.addDir(subdir); ASSERT_EQ(store.size(), 0); copy_file(testfile, outfile); iterate_main_loop(); ASSERT_EQ(store.size(), 1); ASSERT_EQ(unlink(outfile.c_str()), 0); iterate_main_loop(); ASSERT_EQ(store.size(), 0); } TEST_F(ScanTest, subdir) { string testdir = TEST_DIR "/testdir"; string subdir = testdir + "/subdir"; string testfile = SOURCE_DIR "/media/testfile.ogg"; string outfile = subdir + "/testfile.ogg"; clear_dir(testdir); ASSERT_GE(mkdir(testdir.c_str(), S_IRUSR | S_IWUSR | S_IXUSR), 0); MediaStore store(":memory:", MS_READ_WRITE); MetadataExtractor extractor(session_bus()); InvalidationSender invalidator; SubtreeWatcher watcher(store, extractor, invalidator); ASSERT_EQ(watcher.directoryCount(), 0); watcher.addDir(testdir); ASSERT_EQ(watcher.directoryCount(), 1); ASSERT_EQ(store.size(), 0); ASSERT_GE(mkdir(subdir.c_str(), S_IRUSR | S_IWUSR | S_IXUSR), 0); iterate_main_loop(); ASSERT_EQ(watcher.directoryCount(), 2); copy_file(testfile, outfile); iterate_main_loop(); ASSERT_EQ(store.size(), 1); ASSERT_EQ(unlink(outfile.c_str()), 0); iterate_main_loop(); ASSERT_EQ(store.size(), 0); ASSERT_EQ(rmdir(subdir.c_str()), 0); iterate_main_loop(); ASSERT_EQ(watcher.directoryCount(), 1); } // FIXME move this somewhere in the implementation. void scanFiles(GDBusConnection *session_bus, MediaStore &store, const string &subdir, const MediaType type) { MetadataExtractor extractor(session_bus); Scanner s(&extractor, subdir, type); try { while(true) { auto d = s.next(); // If the file is unchanged or known bad, do fallback. if (store.is_broken_file(d.filename, d.etag)) { fprintf(stderr, "Using fallback data for unscannable file %s.\n", d.filename.c_str()); store.insert(extractor.fallback_extract(d)); continue; } if(d.etag == store.getETag(d.filename)) { continue; } store.insert_broken_file(d.filename, d.etag); store.insert(extractor.extract(d)); // If the above line crashes, then brokenness of this file // persists. } } catch(const StopIteration &e) { } } TEST_F(ScanTest, scan) { string dbname("scan-mediastore.db"); string testdir = TEST_DIR "/testdir"; string testfile = SOURCE_DIR "/media/testfile.ogg"; string outfile = testdir + "/testfile.ogg"; unlink(dbname.c_str()); clear_dir(testdir); ASSERT_GE(mkdir(testdir.c_str(), S_IRUSR | S_IWUSR | S_IXUSR), 0); copy_file(testfile, outfile); MediaStore *store = new MediaStore(dbname, MS_READ_WRITE); scanFiles(session_bus(), *store, testdir, AudioMedia); ASSERT_EQ(store->size(), 1); delete store; unlink(outfile.c_str()); store = new MediaStore(dbname, MS_READ_WRITE); store->pruneDeleted(); ASSERT_EQ(store->size(), 0); delete store; } TEST_F(ScanTest, scan_skips_unchanged_files) { string testdir = TEST_DIR "/testdir"; string testfile = SOURCE_DIR "/media/testfile.ogg"; string outfile = testdir + "/testfile.ogg"; clear_dir(testdir); ASSERT_GE(mkdir(testdir.c_str(), S_IRUSR | S_IWUSR | S_IXUSR), 0); copy_file(testfile, outfile); MediaStore store(":memory:", MS_READ_WRITE); scanFiles(session_bus(), store, testdir, AudioMedia); ASSERT_EQ(store.size(), 1); /* Modify the metadata for this file in the database */ MediaFile media = store.lookup(outfile); EXPECT_NE(media.getETag(), ""); EXPECT_EQ(media.getTitle(), "track1"); MediaFileBuilder mfb(media); mfb.setTitle("something else"); store.insert(mfb.build()); /* Scan again, and note that the data hasn't been updated */ scanFiles(session_bus(), store, testdir, AudioMedia); media = store.lookup(outfile); EXPECT_EQ(media.getTitle(), "something else"); /* Now change the stored etag, to trigger an update */ MediaFileBuilder mfb2(media); mfb2.setETag("old-etag"); store.insert(mfb2.build()); scanFiles(session_bus(), store, testdir, AudioMedia); media = store.lookup(outfile); EXPECT_EQ(media.getTitle(), "track1"); } TEST_F(ScanTest, root_skip) { MetadataExtractor e(session_bus()); string root(SOURCE_DIR "/media"); Scanner s(&e, root, AudioMedia); while (true) { try { auto d = s.next(); EXPECT_EQ(std::string::npos, d.filename.find("fake_root")) << d.filename; } catch (const StopIteration &e) { break; } } } TEST_F(ScanTest, scan_files_found_in_new_dir) { string testdir = TEST_DIR "/testdir"; string subdir = testdir + "/subdir"; string testfile = SOURCE_DIR "/media/testfile.ogg"; string outfile = subdir + "/testfile.ogg"; clear_dir(testdir); ASSERT_GE(mkdir(testdir.c_str(), S_IRUSR | S_IWUSR | S_IXUSR), 0); MediaStore store(":memory:", MS_READ_WRITE); MetadataExtractor extractor(session_bus()); InvalidationSender invalidator; SubtreeWatcher watcher(store, extractor, invalidator); watcher.addDir(testdir); ASSERT_EQ(watcher.directoryCount(), 1); ASSERT_EQ(store.size(), 0); // Create a new directory and a file inside that directory before // the watcher has a chance to set up an inotify watch. ASSERT_GE(mkdir(subdir.c_str(), S_IRUSR | S_IWUSR | S_IXUSR), 0); copy_file(testfile, outfile); iterate_main_loop(); ASSERT_EQ(watcher.directoryCount(), 2); ASSERT_EQ(store.size(), 1); } TEST_F(ScanTest, watch_move_dir) { string testdir = TEST_DIR "/testdir"; string oldsubdir = testdir + "/old"; string newsubdir = testdir + "/new"; string testfile = SOURCE_DIR "/media/testfile.ogg"; clear_dir(testdir); ASSERT_EQ(0, mkdir(testdir.c_str(), S_IRWXU)); ASSERT_EQ(0, mkdir(oldsubdir.c_str(), S_IRWXU)); ASSERT_EQ(0, mkdir((oldsubdir + "/subdir").c_str(), S_IRWXU)); copy_file(testfile, oldsubdir + "/subdir/testfile.ogg"); MediaStore store(":memory:", MS_READ_WRITE); MetadataExtractor extractor(session_bus()); InvalidationSender invalidator; SubtreeWatcher watcher(store, extractor, invalidator); watcher.addDir(testdir); EXPECT_EQ(3, watcher.directoryCount()); EXPECT_EQ(1, store.size()); MediaFile file = store.lookup(oldsubdir + "/subdir/testfile.ogg"); EXPECT_EQ("track1", file.getTitle()); ASSERT_EQ(0, rename(oldsubdir.c_str(), newsubdir.c_str())); iterate_main_loop(); EXPECT_EQ(3, watcher.directoryCount()); EXPECT_EQ(1, store.size()); file = store.lookup(newsubdir + "/subdir/testfile.ogg"); EXPECT_EQ("track1", file.getTitle()); try { file = store.lookup(oldsubdir + "/subdir/testfile.ogg"); FAIL(); } catch (const std::runtime_error &e) { string msg = e.what(); EXPECT_NE(std::string::npos, msg.find("Could not find media")) << msg; } ASSERT_EQ(0, rename(newsubdir.c_str(), oldsubdir.c_str())); iterate_main_loop(); EXPECT_EQ(3, watcher.directoryCount()); file = store.lookup(oldsubdir + "/subdir/testfile.ogg"); EXPECT_EQ("track1", file.getTitle()); try { file = store.lookup(newsubdir + "/subdir/testfile.ogg"); FAIL(); } catch (const std::runtime_error &e) { string msg = e.what(); EXPECT_NE(std::string::npos, msg.find("Could not find media")) << msg; } } int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } mediascanner2-0.115/test/bluray_root/000077500000000000000000000000001436755250000175625ustar00rootroot00000000000000mediascanner2-0.115/test/bluray_root/BDMV/000077500000000000000000000000001436755250000203125ustar00rootroot00000000000000mediascanner2-0.115/test/bluray_root/BDMV/README000066400000000000000000000001011436755250000211620ustar00rootroot00000000000000This directory is used for tests which act on Blu-ray-like media mediascanner2-0.115/test/dvd_root/000077500000000000000000000000001436755250000170415ustar00rootroot00000000000000mediascanner2-0.115/test/dvd_root/AUDIO_TS/000077500000000000000000000000001436755250000203105ustar00rootroot00000000000000mediascanner2-0.115/test/dvd_root/AUDIO_TS/README000066400000000000000000000000741436755250000211710ustar00rootroot00000000000000This directory is used for tests that act on DVD-like media mediascanner2-0.115/test/dvd_root/VIDEO_TS/000077500000000000000000000000001436755250000203155ustar00rootroot00000000000000mediascanner2-0.115/test/dvd_root/VIDEO_TS/README000066400000000000000000000000741436755250000211760ustar00rootroot00000000000000This directory is used for tests that act on DVD-like media mediascanner2-0.115/test/media/000077500000000000000000000000001436755250000163005ustar00rootroot00000000000000mediascanner2-0.115/test/media/baddate.mp3000066400000000000000000000207031436755250000203070ustar00rootroot00000000000000ÿû0ÀInfo(!C $$$**00066===CCIIIPPVVV\\bbbiiooouu|||‚‚ˆˆˆŽŽ•••››ĄĄĄššźźźŽŽșșșÁÁÇÇÇÍÍÔÔÔÚÚàààææíííóóùùùÿÿÿûPÄÀ€ 4€LAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄ]ƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTAGTrackArtistAlbum-1ÿmediascanner2-0.115/test/media/baddate.ogg000066400000000000000000000346411436755250000203720ustar00rootroot00000000000000OggSÎFÍŰ-”vorbisDŹwžOggSÎFÇqÍìÿÿÿÿÿÿÿÿÿÿÿÿÿÿÉvorbis-Xiph.Org libVorbis I 20101101 (Schaufenugget) ARTIST=Artist TITLE=Track ALBUM=AlbumDATE=2015-13-33vorbis)BCV1L ƀАU`$)“fI)„”Ą(y˜”HI)„”Ć0‰˜”‰ĆcŒ1ÆcŒ1ÆcŒ 4d€( ŽŁæIjÎ9g'Žr 9iN8§ ŠQà9 Âő&cnŠŽŠknÎ)% Y@H!…RH!…bˆ!†bˆ!‡rÈ!§œr *š ‚ 2È ƒL2逓N:隣Ž:ê(ŽĐB -ŽÒJL1ŐVcźœ]|sÎ9çœsÎ9çœsÎ BCV BdB!…Rˆ)Иr 2È€ĐU €G‘I±˱ÍŃ$Oò,Q5Ń3ESTMUUUUu]WveŚvuŚv}Y˜…[ž}Yž…[ۅ]ś…a†a†a†aű}ßś}ßś} 4d  #9–ă)ą"ąâ9ą„†Źd ’")’ŁIŠfjźi›¶h«¶mËČ,ËČ „†Ź išŠišŠišŠišŠišŠišŠišfY–eY–eY–eY–eY–eY–eY–eY–eY–eY–eY–eY–eY@hÈ*@@ÇqÇq$ER$Çr, YÈ@R,Ćr4Gs4Çs<Çs=:<;<=:H14&,H;<<767:<=LŽ.H=;> JńŻF„ÿ«!*ź €€YiŹgf-?ș%#EQEccdzíGă ÔĂ ±ßF\ƒ›\2/ojŰŻétê`©ÿ‹~â-oĂÛź„T†ùWă*Ü9P‘ÍmE’ìętÀ.g,Ș­ƒÏ4‘om"úŽń"‚ <òÁ-áG†ŒöĄ‡ávw<$đO«M¶”țœ—æîb9T|d«™™3ANÁă`V§pź:âe"îîîsèî > N‹ êč\é+ŽĂG“~5ĐÏzP€ŚZX>ŚżïÏőŒțÿĆìXOê > Nś‹ êč]5ÒW\&ęj Ÿ ôŚÊX_jí`ÙÏŸ>ő„t?Ń~çÖ{-7> N‹ êčl#é+ŽëŁIżôà.Ő°~M–ƒ“žÈ \é¶ V X‡†ü> N‹ êč\ÒW,ŻhÒ/@?ëwj°~ «Î=ŽbŚț]2™́?ž> N‹ êč\Òw(ŽŁŠ&ęȘPe°NvXčŒÿős{źśYl3FmKT> N‹ êč\Òw(źàŠ&ęȘPe°žUX °òòńr֋<æ™x3Fwá> Nś‹`êč]ô•AŁIż €J:€w•%€őĄƝD‡ÁÙ'<cƖ> Nϋ`êč\é+ŽCŁÂ2* èo^#Ź›Wx‰Ăű‰rÔœ€`Jo\Ś Žà"R€ˆîÏHűÙäòBÛ]Ÿßč3ȘëÒ€„‹tÎïjìï@ßV?%–ßv[lèêȘȘXEQeîŁ5’MMM…Ę}D†ÉWź>ż–ra îm0żę˜ë^D†‘ßčúì[šŠ î~S€uÆÁwvfèĂT†ńÏ\yömäÀŚW,—ț1ŹąÉ<†ÁÏÆ5ű6r` îÊ`Œ<`I-L†ÙWă \K30Æ-W6@ûó/E+Ś$†éŸkđmä†1nÉČàżúæM5^$†©ï\ÿlí&a8ńŽ€kđ/O)'ŒŸQńŚĆćúÛŐJow]\‘5&-\^uś„».Xőmn#î<4yűËWŒé%:slŒë/yȘ#Öđ!pû—dćț‡<ÎőQç ŒœïžžŐĘ §Ś,nàéśĆ«}#VÛÿŸčƊÎę·ÔmÍjyKŸeê_ûŹ$nÍWăŻAÛLqÚR$aÿŐW^JêcŻ%V9Îz â ÚŽè%ącží)ăÉæùÍtžçyFQ Đévê'ÿ#ę^ Ę͗żżżèȚᏟ”R]vŠÁčĆĆżÿśWż> .—_°R/c$=E)c€_ Đ?] đŽ`”Ę~Ÿ>ÔÖ{r”;Űgú9> ź§PF/ăBzŠÚ˜~ĐàćËî{·ŒKwúĂ†Ś›žIŸ•.Ž> .—_°R/c$=E©c€_ ĐżSàfWË~öő©ŻK/ë耣z7> .ϗŸ©—VIO1Ž‘~ Àp%Ç}9ˆŒ"2ˆÌqÈ芠™;Ę > .—p©—M%=ĆÀ:FúUÖßÔP›ŽÚŸxkuùá‹'ûj”š> .—0R/cGzŠČ‘~U€u™V5rŚóçśő\ßî;ĄŰP9y„> .ś—ß©—M$=Ć#ęȘë[U€‡áȕ—·_—^šś:—6H&> .—Ÿ©—1“žąÔ1Ò/Ücy^ëÈ:Š; Â`îME”ó 4>Ț Žđ%RpaÎÇJyïrù6æu}çΚźkôy ° ÂđžvŻęôÓó=ĐÓś#ëȚ{ï§ßr4bŻ^œ€żÿ $†ÁGțđì,Ć cpçV€xgĐ[—†IDŠÙGź=?[?P™Áę5pțûëŸAC64ŠńGțđìlę@ew·•@öÁ©·z&T†ćgź>?ƒ{Œ‡Ÿ~zèĂD†Égź=»6șP•Á]o·\‡†TŠùGź>;_šÌàțjëü7gĂú°D†ÉGź=»6z 2ƒû[ ŹȚMÖĄ‡#D†í[ăzÔ7ba îÊjŁęś–aŸêŚlb©žŰ–A…±YșUܱÙ[n‹±yύ?˜œ·ëò·ŻüÄTxbWțZ‹Ÿ‰ÓTá‰őÙ­Tę ‡ @Ț4àÎÍÙ܈9[ęVuE)ŒćüCVûŸœwu]ŻCoé=dŠsî=  €œßv»žGćâæŐi`uæ?’äŰúéSÿ"ÀńćÔA/4țsÎ>câî>zI|bßFk· cßĂ1XgJ\”yŸ.—Èu±Ôyśï+*–ÏÏúŽöMrhôû[$ü„ŽăìüæÍ<Ïìü§sđ«łKP  ź·È͋żç|aÁS6Ûÿ4Ź€Ź 4č`væ"> Nëć·UÏgHßă‹?Ú~À›ú™ „eX†Ö.ê<ŐÚ玀ćqç> Nûćw©žś6’Ÿ_4ƒŠö žŸ țòŰÏjZ+JWŹèÚe ”€eĂĘ> Nûćw©žś6’Ÿ_4ƒŠö žŸ ~xœ€[5 Ʒ˶às'- Ví=> Nûćw©žś&Ÿ'/Óąíü<p? „eXR7tÀa fk@Š6”> Nûćw„žśh€ïdjŽ_@ÜÏ€őęń ÊT0!UJsu‘vM€pșÍ > Nëćw©žÏ&Ÿ_$ÓąęžŸ >?MÀ\.€oû_n:”C Eum·> NëćwĄžÏ&Ÿ_$ÓąęžŸ ž]pż†VdĆ,9Č2*bûg> Nûćw„žśĐHß/ÈèŁę Ÿ ęú5đPÇ‚)„J&œ<ï&Ug“> Nûćw©žś¶‘Ÿ_$ƒ¶đú™uzÀ’nm'à”Ž àU€ő€Á öŽáEš{t>6„Ÿą4hŽăőíÛă8ŰăÏ_ź੔>0áLž¶6.趗ç/;—oŸ^œ‡Üżżżż|né}Â1S_òȘx wŠƒ·Íy2“QÚÿ?Æ'­¶őÿ·fJ”śśƒ·IvóÿôĂ‹f϶EUÁô Cș臚6ÉüđᒶÇgeËć2Ä_O4đlúŠE¶Xîâs™â ô©w–ł1L3—"„'ÓÆx±]ÙG5č«u68\ô‡E‡ĘÚLNùÏ\<ś;‹fźÁÍĆ#‡ăÙź:țÙVò.iŽêŻŚĄ…ÍtN2l~Íw#Žî„šÈűúŠà=ŒfT;)ÚöáMČŐèęzEűK[>îȚœ; =ü~!>Ź(ûe\~íŸü7]æțÍ4ÔTšÍfŚŚË…ÿțÂ,”$> Nëć·UÏgÓHß/’ĄŠö 0 ŸÀókęçî ÀömŻ-Æű-@˜5âA> Nëć·UÏgIß/’ĄŠö 0hYŸ:x ÿĘ Č"+łoäz&M‰ > Nëćw©žÏ`€ïdĐhż€ Ž,€őᓠżì˜*„‚čÚț Lę‹Ä> NëćwĄžÏˆ€ïÉËPŁíüĐ€JX†ehÍÜ€~»”`‚™È> Nûćw©žśÖHß/Zö 4 |ë±ęe˜ÈȘXŃț»œk`‚iŽ > Nûćw©žśĆHß/’ĄFû~xŹ@ٌo—mÿúz«u`V*> Nûćw©žśh€ïdhŃöȚZ”a–„­őEô—]ŸŃ€ ŃŒüń> Nûćw©žś&Ÿ_€*Ú/ -‹Àś—€țwWÀ„T)Ì·Ï|­zÛź5> Nëć·UÏgÓHß/’ĄŠö @?€ÏOè?ś€ăÛŸíۛTŚ”fç=Ÿű5ÜMą{æ‰[Ò[ö>ZżX‘loaæű§—6ÿł:ԞęĆCłw€cq{è9zŚ-?w_gZrˆ(Âș±ÖĂ țÖDĂț\1œóăâžĂϝ‚û°üW—`\Qk”Ź),GźgÛł‡ïż–míÂCęâÖz ©Äé$«ùùáő/>ăDQw1[>öÏ0A±dbKŠŐŁv4KȚî {ĂÊk?^îȞ‹|cŠi"żŠÿÖ&ëčÈÏÚÔÒ&òk˜ș”©ę9§ŽfMKi‘Ÿ‡©;ŸÚĘù9śżÜ“ęyđżò©]›’Ÿÿ‹-™VŸS‹ìÔ­Mdođ/LA^|D2ÁŻzŚÂ ›Ÿ=ž»{ù‡S‰uT•ŻÈ·WŸ"ß^™ő·©ÌÜŚžÎDvá߯ôßŰDlewK3çíęËhS$ÚŽÁVòFŁÏ_ÂOjhÌoȚŒ™ç9˜żÇ  éî„?˜ûДlțÿęćïögłÙl6«”ÊöÎG/Œžžžžž(> Nûćw©žś¶‘Ÿ_4ƒö ĐÏ?ß}ÀSM[0Á.g|ûÔ žZ ș±ś.> Nûćw©žśŠ‘Ÿ_Ą¶đ~ę €P†ehfŒžjŰ#”@ôźÓó> Nûćw©žśŠ‘Ÿ_Ąö h€~ôŻw@(O Á”R%Lֹь‰ŸOggSÀłÎFdDìŻc<<=:97M”1#K<8=>:==::<=:=F«1P==7779=9=L¶/L;=:==;:85> Nëć·UÏg3Ÿ_$Cí`@?€Ïžèï{3Û·}[Œ9S- <Œè+> Nëć·UÏgÓHß/’ĄŠö 0 ŸÀÛSęçî Č"}$f§{$„YùŽ> NûćwM=ïÁHßÏÈ ąę*ĐČÖÓû ÿĘ0!UJłï°G*zÚm> NëćwĄžÏÆHß/ÈĐąíüq@Ë"€èNh•­ô—] Lđçç‹> NûćwĄžś%Ÿ_$Cö Đ€Ÿuę@™ŹÈȘXŃț?ŸŃÀì‚> Nûćw©žśIß/Zö 4 üù €~X@0~Šl‡ë–Đ€ ©oȚȚűuÊ]Jśèűí…đ–[ ”_ŹŰ^ČP9ìę?ëôđÿò|êȚƒŽL7ۙ5sŽÎ‘%ąȘHbüöù[őȚÉR 3”âț:țæáŸ'5Ë#\lśĄ{`Œ”łžŐ_ę]€Ő„”ÖZ+ ˟ăwŻI€äàżŻü·ćŚùĂÿ_Ù$“éH›żùûŸUűŒtÎ}—/xFŠżŻšXšX2ę}¶Äà©őLÖ3ŠäEàÛ]Šű„ŸĘĂ?kƒürSûł6‘_Údê~ÖŠä—6ńßÏ4Ć/šòżžË?ӔüzŠű™ŠűÙ=LíÏ4ĆÏÚdjčáÇs[UüĆOăx/ŒÈXČŻY%žÔłÁS‰óÜ:MŐü$Oú$żÛïöÿ‡ ?țIȘ-țțÿg$já?ëđÜSŒÍČ@ź\><9$:ÿ—ż.B™šÊ¶_ÏÏ ÚöÊMŠŐèóś-áOUűĐĂpśîxČ g 8*Č€ôßÜæ…ìÔăÿKËlțüÜÌÌLC©T¶ś>­ȘȘő:ź"V> NûćwM=ïŃHß/ÈŒhż€€ŸęëĘ`«*SJ•Lx°Š6u„,:š> Nëćw©žÏIßÏÈČhû_ €ŸB–aIöf%d­ (4Re2> Nûćw„žś&Ÿ_4Óąę €Ÿ~»û€“Z"+JGÖé!ƒ4ČőB > Nûćw©žś6’Ÿ_4يö H?üóźnŚ1À»œńÍĂ.[Ž äÒĂæ> Nûćw©žś&’Ÿ'/ƒŠ¶0 ~@X†ešfCÀó:Và|-ĘŸ;> Nûćw©žś¶‘Ÿ_$CíĐę đïŁÀS [Š”Ș˜đșȚÓZ șáî,> Nëć·UÏg;Ÿ_€>Ú/À€~űűtęĄÜ°}Û·ęžEŐ úș“g> Nëć·UÏg3ŸŸQm?àÿè'€Š^CÉdfˆ@/Ï>‘&ü> Nûćw„žśIß/È ąę*@?Àúțęçî 0!UJső›.fiȚűuÜMryôęû–ô–ûu?è:€ă/@ Țóśÿ€”Ćúéo“°FÀÍüșHë,+ô-š–käąć{íc€T"|Ț%6òśleĘż^ûqÄ-Șp†huü6F”ÒșŽV€Ő?_żžyśő'ku鋌ì֏ÿ|=Ł«ùŻ \qw öž`Ÿ œ}œèKJđòù™†©) Nûćw©žś¶‘Ÿ_PíPú°Ÿß)űRn&€Jé`źȚő•&Ű_/> Nûćw©žś¶‘Ÿ_Ž>Ú/@ę đùi^kŰŸí;|{Sî8 öăNś> NëćwĄžÏ&’Ÿ_$ƒŠö  ę đìڀ·uŹ€Y1K6>B€<óćš> NûćwM=ï!Ÿ'/SEû4 ę èŚŻÀœuLŠ”*„Â̕·)rȚç­> Nëćw©žÏ&Ÿ_iŃöț` ę è%ʞ€­ȘÀ %B}¶> Nëćw©žÏ&’Ÿ_$Ëąę €Ÿ~»ë€Ź5€ÈŠŹÈ:œÆó*©â\> NóćżUÏg €ïé—iŃ~~űóźÀIUÀÛ·ƒù~ÂZ:Čő > Nûćw©žś&’ŸÇ/ي¶PH? ”aÊ4ž]Çœk@È„/> NË楞ÏI߯‘AEû4 ę đÏÇÏëX ˜ÈŠŹđ敜u@tăòȚvüMrÓÌ·€·ÜbÔšëęû~&|>›ô€Ÿž§k‹Ó/9ąïlĘ~]Î6ÖëuQ$o«”Ö@k­”ÖZcĆù_Ë.¶{“=Ă"Xę*űCZk­5á€Ő?żŠÍgÿ] wżțzÛÁăӇ§u”©A»đŐëăÐűŒtdUaú»Őû\|ö 4),.JcSĆë™LM1°žŸ”©)Żçg“©)ÏÏτ255ćÇęüüüŒajjj Żççg7&SSS~ NëćwĄžÏ&’Ÿ_€*Ú/PeűËc֟CÁD銟]–X+˜17ïU> Nûćw©žś6Ÿ_Ž*Ú/Peűáqëû@°ęLÙöÜéą@0™W?> Nûćw©žśÆHß/ÈPŁíü<š€B–aI=íÀúP?Ł€`PÔ± > Nûćw©žśIß/šFûT ą`}<Ź›LH•RÁ\]€© @0„Śn> NûćwĄžśÖHß/šáFûš(đùiëiàÛŸĂ—łźÌ)&Œ±j> Nëć·UÏgHß/’ĄFûš€ž]XêX‘łäÈš‚±ČkY> NûćwM=ïÁHß/È ąęšČúők°țŠ”*™pòŒkȘ“>oÎ1> NëćwĄžÏ&’Ÿ_Pm?àd ô€’޶“Xÿő*ÀŒŒàô)> NûćwĄžśe }żH†>Ú/À€~ż>°~ "+JGÖéoÖł?ßoŸű5üMRóèűęŠôEùh€/U`TŸۗ–Ïâ§C™a‹›_s‚>œ ôź€Yyœł­ÿ^QÓXô] ă+śŚŚ„ƒíÖńș8îŰÍU>ÀgæìËÌQQkčŽÖ€rà·©VûÙĘnÓl¶SÀ5yÌ„ŐL„Ô(ÿ;ó’î_ÙöŚŐç&đÏf%[2ę}ZÆ$ͧ˜&òË <ÓŁLá|»‹|Ӕüą©őLț抖6‘_îÉț(Á‹yŠĆÜô•ąąišZ ”L‹©Û=™ș”)ùY›ìÏùÔîŠnś ?»'òs>ĆÏĆ?ùЉ Œ€ ßAw-œȚOÀ ˆS…ĆéÆPöÓŃé N·ÔmÍjęç–oQig„ŹWRŠ|rÛw#ŒX›P™mΠ{8ymæ3ˆ"f ÌÏÚŽÊUJæŃùy?éÁ~zîȚœ; Cêü ę~§OÁxŰO=żü?ÏíVž›ëő}żžžŰëû~Ś›2wwwwwww> NëćwĄžÏŠ‘Ÿ_€>Ú/À€~&x{JÀk­ Y‘‰Ù[t֓‚û> Nûćw©žśŠ‘Ÿ'Ż>Ú/ ô3`}x\X_Șv`BȘ” fŸ3­j`œ_ç> Nûćw©žśŠ‘ŸŸQm?à§ú ”P†ehí­ëKuÓsꯏ> Nûćw©žś6’Ÿ_Ž*Ú/@ęŸő8€őïÄAŹ(]±ąęSĐłÂùË> Nûćw©žś6’Ÿ_Ž*Ú/Peűëc%֟Ă@0ćLÙڇWi3æĂKŁ > Nûćw©žśHßÏÈPEÛx;šČ„–aIűR,`ę9ä:À4ÿČĐ > Nûćw©žśÖHß/’ĄFûT ’űó.°>Ô&˜R:˜onÓ @0(ŠZ> Nëć·UÏgc€ïÉPŁęTűütëfđmßöíMóïVLéÛ:> NëćwĄžÏIß/šFÛű§ˆ^ ]éß5°n‚)œuÔOggST]ÎFoVnÒ1KŽ/&H:<<<<:==9Cź(,&P<:>;><<<=NgȚűuü]Š{ôùÛ á-·őđ}$û]gè§èŸżùFïÓx7ÙCzâdĆÙÿ.(ÓăXR‹Zk ŽSŚ`Zk|Țö€żö(k{œÆźÎ-|́aHè/î'v^ŽÖrÔZȘŻ—sútq6€[Wí§nÄĄ\…ȘF T6êĘÖZkàq:Ûß-ìŸQÊŽèŠűćÆ@ńăÙ=LÉŻgššZÏÚ0…—VüxŠa ÏîaJ~=ÓÔÔęŹMük•˜ŠČV ûw çuN-Ÿó)ù~Ɣüz.SțŚ3MM­gś0ĆÏî2…gm˜âgw™’_Ï4ćÇ3$Ź=_ÛYR~/ìï À%|ƞ6Ș­=ŸœN‹'ÍÄ?ęÿ9ă;τôęÏD/„bÍżőźŐïtŻ ƒ2eFł¶æÎ`áș·Żż[«.Šöü úŽÖMŠŃèÏç†đW”˘ç7ÓyÆüő•Ò°ŐńGòk^żűùÛ]$wƒęęYZç\mÊ8Œž»ûrș3> Nûćw„žśĐHß/ÈòŃöȚp?”° ËĐÌßN@Ô4€„” …j> Nëćw„žÏ`€ïd^Ž_@îg@ÿz0— SJ•LxßÖ@Šê]…> NëćwĄžÏ&’Ÿ_$يö 0p?||ö€óš`û¶o‹E瀘ÿo> NëćwĄžÏ&’Ÿ_$ƒŠö 0p?Œ=%à~M ­ÈŠDïțó$ˆłÿ/ > Nûćw©žśŠ‘Ÿ'WíPú™°žž€ŚZ0!UJ•Ìȕș °žL}> Nëć·UÏg3Ÿ_Pm?àèg€6Đ OŚ|©Ú`ęt{> NëćwĄžÏŠ‘Ÿ_€>Ú/@ęŸőX€ő„:°"+Čąę;UsęâW> NóćżUÏgŒ€ïéWíĐęț} XÿNÁ”m߄·WìÒ fEûç> NÇćw„žś&’ŸŸ‘ĄŠ¶PÈ"ʰ KXƒÀúwHM­`Vš]UȚvÊMrxvÿ¶!œćQ? źă?ż€àï }Xï“ûËÓ­-žțæ-Ÿ e2b&#Âzÿ9Ă& >Uąíïț„čŸ]šuüö•ăTâ;„9†^ȚÌÆ(ŽŽZ+@őőòÏŹ_Ì<ȘÀ‰ő›Dđ;S#iźN„êę­WÁ|:sŸțț‰æÏ^çxqqq‘->»©=»LŠp»nvűmÜÆ!š‹Ș ęëIŸ<űőĂÖÌłK›ö_ńÏ‹Š»ŐߟÍfłș„́sÎ9眫\ ö—àač\ê[»AúŐÀŸ€țÇZàű ¶ÛïÓ­¶ÓÇï+"[0 ö—àač]ő=Šb €_ ì èK——Àòyÿśęčűž ƒ: ö—àač\ő-Še~5°/€țšæé€ÒÏŸ>őšôY‡]YD?­“ öŻ—a9m‘úĆô û5l폆ižàæ ĐwU•R„Kf ö—àač\ő-Še~Հ}ÖßÔ °> Őûob­îÿvEr Pđg ö—àačlő-ŠÓ ęȘÀŸ€ő0——@.Ÿ?żŻçú¶țZ] €ó—; ö—`ač\ő->Ź ÒŻ ì XßVŽïV^Ț~]ziN~2!ù] öϗa9]ŒúĆHż öepƒăőYaTąŒƒŸżŠU@ČőH öϗa9]ő-Š! „e°/ ÿžf€‡€ŚòńJìőè°5Ę €Ù§Vțűò)i㖇]Qßb Ą_°=èŸÉ đCƒ—‡ïm„œÿûÿUß§&ű2êź±NXï!Éêß?ȚcAuUȘȘą(ȘD_ŚÄêżßMڶ_WÎÙFÏÜÁnaVk€ 4áš{ÿgNŠûŸÿ_ËÂÙ?©ońP_ÍϷ֚T[Íg?ț3[đò1”˜ hȏ#qŽM§*ț„a=Ï^îÉrOd SÀmediascanner2-0.115/test/media/embedded-art.ogg000066400000000000000000000166061436755250000213240ustar00rootroot00000000000000OggS KŸ26ϏvorbisDŹ€8žOggS KŸ2™§Faÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿvorbis/Xiph.Org libVorbis I 20140122 (TurpakĂ€rĂ€jiin)ËMETADATA_BLOCK_PICTURE=AAAAAwAAAAppbWFnZS9qcGVnAAAAAAAAAMgAAADIAAAAAAAAAAAAAAud/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAsICAoIBwsKCQoNDAsNERwSEQ8PESIZGhQcKSQrKigkJyctMkA3LTA9MCcnOEw5PUNFSElIKzZPVU5GVEBHSEX/2wBDAQwNDREPESESEiFFLicuRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUX/wgARCADIAMgDAREAAhEBAxEB/8QAGwABAAIDAQEAAAAAAAAAAAAAAAUGAgMEBwH/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAAG3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqhGneW45zeaDYYgzMzmNxmAAAAAecHo4KccxvIonTWQwLMQBcSQAAAAAPOC6mZTi0FXNxGlrKyTB2AsYAAAAAOIgDtJMgCfMCOJ8rZkSQJEAAAAAAAAAAAAAAAAAAAGs4iRAPhiZgAAAAAAAArJVD0o1EUc5HlwOkAAAAAAAA84Jw+ms2GRzFrNwAAAAAAAOMoJOEKTJMGkgC4HYAAAAAAADnMDrI42G82kYdxuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/EACYQAAIDAAECBgIDAAAAAAAAAAMEAQIFFAAREBIVMDRAE3AiNVD/2gAIAQEAAQUC/Z2jpWHeua4xCibgGuuQHzTMRFDiJN71HFCjL1cox9VtW8dSwGJraLR7aUfm0vDXbt5/RycfJZm1gTxNHbL/AAXvwskAIbkBbIubXw0s3mCSW4oPbJFkNAb6xK1cBcuuOau+og42RSbO7IfIyUl3mNQPbOTEqXqEM+b7Xw8X4fuMKiarOHXuvkjARhYbVPQ6911hq0cVhsKmXCx5jvBcUV7L5FQldV5gUleGH/Iveo6CfXMTxmYjqt63+nsn8ofKReQkgwjtCWj1lfvqlqbOz9ASgAGqwH6LxeS9o8e6WKfvTd6Rz1zp61IFn5aYGFhCoEf0Gy/gVSTlwnoXQZlHQ3Osv+v1xzdLP0KKDWYq0H6Bg0YoBYS3gVFc9yqBPUQqBH1bOVtIxUDX9b//xAAUEQEAAAAAAAAAAAAAAAAAAACQ/9oACAEDAQE/ARx//8QAFBEBAAAAAAAAAAAAAAAAAAAAkP/aAAgBAgEBPwEcf//EADgQAAIBAgMEBwUGBwAAAAAAAAECEQADEiExEyJBUQQQMkBhcXIwQoGCwSAjUnCh0TM0UJGiseH/2gAIAQEABj8C/M7Y2DBHaasbmJ/G2dJiY7PjBy6o2qTyxVJ0qEuox5Bql2CjmTX3bq0fhM1vuq+ZqVII8OqDetj5qlSCPD2iY+LT17BDAjeraYxjicEUejXDKkZUAfdeDVu1z3jRu+/cOVM97pKW/UczW64ZZg4TkRS+v96L7TDBjStnMmZn2kx2WkeIqdqq+DGKFtLgZjyotwcVtMYmOzxoNwUE0Lg0cfrSczC0FTS2RRHSLjI3DPKgq9IYsdIYGl9f0NN6/oPaxcHkeVbt8/FaDl2Zh8Kw3B5HlX8Yx6aw2x5nnWAnCZkGtobmONN2Kg6VNtynhrS3DdJKmdIoJjww0zE0Ux4paZiP6SXYwo1oJbuSx8D9jOt1g3l3NbQ1fXyq1d0neWluLowoG60TpWlz+1W7idkv+9FHVyS05Ut1Jg8+5NnlOEUot3bZNrQBhT2TwzFWPm+lI9xJYznPjVtF0VwP0NM11MRxRqaCWxCjh3G5c4gZUwxYQBMxNfzH+H/a3vdaD5VY+b6Va+P+6y91ppkuKxBM5VtFEdxwXRK+dHZLhnXPqx3Lctzk0guJiCaZmglsQo4dUmyPhWG2oUeH5cf/xAAqEAACAQIEBQMFAQAAAAAAAAABEQAhMUFRYXEQkbHB8ECBoSAwcNHhUP/aAAgBAQABPyH8nGXKI0ESSVVaBIFeTS0XDoPHCMgABknCaN1RM2bKhHGakpEtfyxNUtExwbkZEJqYon9x1qcbzqeIomBZMdJ4wCzzj6HV4ZiEp2taWiAjdyLd4YeMx5DoTDiB3Ez3MSAkCsiIhJuPSCZPuvAa6xGQuEAv9w4gQNjvAHOP6khUOcI4UBB2C7TffruyUC4VQfZd4k2K2fxQJ4Q/N2Yklc9gF3lb9VAAQ9xCPVQOOQnxcPm/ugy+RZoYMoAyZ3htisYJXOVi8dDbubmPdcvFD0BFqDzoClg53gDABIIg4wwDzkQRODACHZNIP7nZNIH6/wAlAospgZYFPmPoAsgBmYLJFAoknX0bt1Hsf3pAC6qzY+c5i2DaZXLIlw0ztV/cYomQwsIOeBTCsNdINAHoXUK9FSQgWiaADwykeTgSLebRs68vj5rwQ1yrNGIQM6EIJTpgVUQyMwEvZ+hqh1Cwl5OnCAHAqO883fhnmaoTEWo9qjvFnQyuX8ggcBJCPobsM0w6QMO+hfPgevV0uhi4wWG+dJgJezhDCNpV8aiB8GaRh/HD/9oADAMBAAIAAwAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQAASSAQAAAAAQCCSQQQAAAAAACSSASSAAAAAASCCAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAASASAAAAAAAACACQQAAAAAAAAQSASAAAAAAAASCCSQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/8QAFBEBAAAAAAAAAAAAAAAAAAAAkP/aAAgBAwEBPxAcf//EABQRAQAAAAAAAAAAAAAAAAAAAJD/2gAIAQIBAT8QHH//xAAqEAEAAQMDAwMDBQEAAAAAAAABEQAhMUFRYXGBkRDB8DBAoSBwsdHxUP/aAAgBAQABPxD9zitWAkqfybv9UYGEBK6gKd4oo3WFwNTdgmPTkRGrdJmlHuSgBdV0KCtQpAxrA0hOkEBLy2oRlw1gxMNsNNBzAmfLRBvwdO5SgKsBrXHgXfE0PcMFHk+oBGY/UC73PUm6HcLbi4iHmeK87MxE5NGkROtPIZFzaWeEljSOabxCM1at8M1n2cOBHkvhRZhrBhf4Chkrm9lTAW5ojNyR0XItIPZowQAsOTFQFJcO+iTMI/pS20GgS2t0D6hVcRLYVYvyKdZpdEkombJ7Jo90qSkEuzA601bY9YZ8a14zFj6mbTinpbMronxpSRJGXRH4b0nBjtvcAXoruVMnYRvPaaNA8ojWFSEd0zV8agszqx9e5c1sZPZPsyU4dDhXkH8UV20cC8hK+aktClMNuPwrPztKz4aU7SSlctuvtit0YA5m0kyKZqFd7KFrpTaTvT31ASByJSBAmZ6S4x1Wgt+FhR1u2rTFNSgIiT/Fa4poUhES/wCv+SbJtosB0osQlexEtwMH6FJ3lICoS5pgyhjW55+zWLIebljvDyoVoPeJD4HoKV22J+R1GTtWYSQMjEwByeaHAbZD7q1VKMxY7UeoovghqL3VbI+ICRTCmR1+ytMvHkIVdCZTzRjs2BKIAb6NLyixO9gdmH0BaKisIFhjAaU+oQrLBkrDEdCHiAMrVw8LyEqt1XK/YwxBZ1fcSh7cSyLARJm7nT0K+fUSBLMOiDtTkR6D7FYXC9iPa6o2CFSKBEU2VKY+5IjF44h7/Yz0xY7kxdDrQFyxuMJi6d30AyMAkwQWAxSxkjQQgNwLhmcVcPC8hKrdVytAgCiEcNLQ2ynhwH4oKNsxQLu7vP7cf//ZOggS KŸ2‚ÚçŒ ÿÿÿÿÿÿÿÿÿÿÿÿvorbis"BCV@$s*F„s„BPăBÎkìBL‚2L[Ë%s!€ Bˆ[(ĐU@‡Ax„ŠA!„%=X’ƒ'=!„ˆ9x„iA!„B!„B!„E9h’ƒ'A„ă08 ƒć8ű„E9Xƒ'Aè „BžšƒŹ9!„$5HPƒ9è„Â,(Š‚Ä0ž„5(Œ‚ä0Èԃ BˆšƒI5ű„gAx„iA!„$AHƒAÈ„FAX’ƒ9ž„ËAš„*9„ 4d ą(Šą( Č È@QÇqɑɱ  Y HŠ€HŽäH’$Y’%Y’%Y’æ‰Ș,ËČ,ËČ,Ë2Č HPQ Eq Yd 8Š„XŠ„hŠçˆŽ„†Ź€4CSČŻ# GŸ°,]Û6șŸM˜uĘèCá7†4ÓŽmÓUuĘt]_—uĘhëșPTU]WeÙśUWö}[ś…áö}ßUŚśUY†Ő–aś}„î •U¶…ßÖuç˜m]X~ăèüŸ2tu[hëș±ÌŸź<»qt†>L(…†Źâ„œCLAˆƒBH)„RÄ„Ì9)sRB)©…RR‹ƒ9&%sNJ(Ą„PJKĄ„ÖB)±…RZl­ŐšZ‹5„ÒZ(„”PJ‹©„[k5FŒAȜ“’9'„”ÒZ(„”Ì9*ƒ”:)„”Z,)ĆX9'%ƒŽJ!„’JL%„C*±•”b,)ĆŰZlčƘs(„ƒJl%„X[L9¶sŽƒ9'%sNJ(„”RRk•sR:)eJ*)ĆXJJ1sNJ!„BJ%„SJ±…Rb+)ŐXJj±Ć˜sK1ÖPR‹%„KJ1¶sn±ćÖAh-€c(%Æcź­”C)±•”b,)Őc­œĆ˜s(%ƒJ%„X[čÆsN±ćšZŹčĆŰkmčőšsĐ©”ZSLč¶sŽčYsĐZ(„ĆPJŒ­”Z[Œ9‡Rb+)ŐXJŠ”Ƙsk±öPJŒ%„XKJ5¶kŽ5öšZ«”Ƙkj±æšsï1æŰSk5·kN±ćZsîœæÖc&”BCVQ!J1ĄAˆ1ç€41朔Š1ç €R1æ„R2ç ”’Ræ„RR „€’RkĄ”RRj­€€4%(4d% `pËòA'@pŽBd†H4,‡•1$&(ä@…ĆEÚĆtà‚.î:B‚Äâ HÀÁ 7<ń†'ÜàąRpÇŃF†ÆG‡ÇHHÈÀÀ!DD4‡‘Ą±ÁŃáńOggSDŹ KŸ2^uj- mediascanner2-0.115/test/media/fake_root/000077500000000000000000000000001436755250000202515ustar00rootroot00000000000000mediascanner2-0.115/test/media/fake_root/Program Files/000077500000000000000000000000001436755250000227035ustar00rootroot00000000000000mediascanner2-0.115/test/media/fake_root/Program Files/.keep000066400000000000000000000000001436755250000236160ustar00rootroot00000000000000mediascanner2-0.115/test/media/fake_root/do_not_find_me.ogg000066400000000000000000000322621436755250000237170ustar00rootroot00000000000000OggSűż2WJȘ?ÜvorbisDŹwžOggSűż2W©ț˜“ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÉvorbis-Xiph.Org libVorbis I 20101101 (Schaufenugget)TRACKNUMBER=100 ARTIST=DJ JP DATE=2014GENRE=ElectronicTITLE=Goin' upvorbis)BCV1L ƀАU`$)“fI)„”Ą(y˜”HI)„”Ć0‰˜”‰ĆcŒ1ÆcŒ1ÆcŒ 4d€( ŽŁæIjÎ9g'Žr 9iN8§ ŠQà9 Âő&cnŠŽŠknÎ)% Y@H!…RH!…bˆ!†bˆ!‡rÈ!§œr *š ‚ 2È ƒL2逓N:隣Ž:ê(ŽĐB -ŽÒJL1ŐVcźœ]|sÎ9çœsÎ9çœsÎ BCV BdB!…Rˆ)Иr 2È€ĐU €G‘I±˱ÍŃ$Oò,Q5Ń3ESTMUUUUu]WveŚvuŚv}Y˜…[ž}Yž…[ۅ]ś…a†a†a†aű}ßś}ßś} 4d  #9–ă)ą"ąâ9ą„†Źd ’")’ŁIŠfjźi›¶h«¶mËČ,ËČ „†Ź išŠišŠišŠišŠišŠišŠišfY–eY–eY–eY–eY–eY–eY–eY–eY–eY–eY–eY–eY@hÈ*@@ÇqÇq$ER$Çr, YÈ@R,Ćr4Gs4Çs<Çs°/ÀĐ Sj,„fu^`žYž3/)}Čžaçì @ôK»l‡æ5Ș ŸI^ì·ÔÖÙ߀ęàÆÀŸ@ßu-@0áą_żRŸY^—”ÖYÚ°ÛÊÀŸT€‡w—€íÛŸ#gƒß<žiž2oÉœł4a§ö <œ©RȘÔx#v•žiž2o)œł4`§ö \CR„T©1uvl”ŸY^2/©œłčagő}šë[wÁ.ÛŸœšy:mŸI^­—Tûvźì €ìL)€łțđžYžĂ§äjâ9qv¶ö@űëß,ßömëŸA_ȚY^3iœŰśàyČ<°/žú € éȚŠJÓț;‰ŸYžłO)œŰs“Ț'Ûû ńÍ6ÁȘ€+VofŃ~+ŸY^2©­ÄżïÉ5ÀŸh€ńÖ`BȘ·©ÒxùaëŸI^í‡TœƒߓKö@Xw@°}SzòònŸI^ČOi=8òà{ê}`}ę>ÀDVd<'źÒ žYžČO©=Äÿïö€~Ü: €J5U•Áź†žŸYžłO)=Äż‰ïÉòÀŸĐßśKlo¶§ĂïŻmœs_žYž2O)-$8đ=čŰôÓÙéuxtűżœ–șžIž3O©ć>đsŠƒ}`€mPé*EÜ·áőžI^‡TT"áçû€Ïÿ`|ÛßüŻ» žYž2/©ÍĆÿ‚ï û@ïL”UIśæ]|G=žYžì§äpńđs2ó`Ű( „ÔÇéc9nÿۍȘžYžĂ‡TüƒŸì ˆë"ŠÓWŸûx^ŸIžłOiĘ9Șá{ĂŸĐę !]eEé?oöžIžĂ—ÔźŸ§*Ű*Àr°*ŠsoŸIžĂ§Ôæ•đ>BĂŸPÖ7?„t%Yégò&žYžŹ—dTBđĘÀŸ`€3Î ©Śé.űêŐ8žY­—äR‰„ïú S‹„Ô'ԍÓŐtw[ EŸIží§TWŽŠűžBaa_0À—Ă@”źÖGÖÜ|/ÖžIž”—U‰ìđȚ”aa_š đìgÀÌÉŻžïžIžŹ—Ô Ï]û€Ôœ&€ł"«ôÊš†žY­—dW ŻxŸRXŰ pò ÂötxŠIÿ|ĄžYƒ—ä&$Ț§ú ˜ßwÀös‹^[ûHž žI՗dUŽìđ>aĄß@Q€ü±] žŻíO~žŠ2žIžű—!$ÔăȚ”ĄÂŸ €ùïˆÈŠ Ùđi-JžIù·U -žO† û€8ŸH`|Û+EUțmŸIžű—U _pŸrš°/h€ăźSR•tƄŻÏŸIč·ĄZpŸêĐĐo èß)‘U•U„Šż«€ŸIžű—U_pŸê°°/èúŚÙ€źÊȘČą3»^MžIá[ČTńÿł«ÁŸ + oÿ1‚)„J&4Ț>Xž9釩âï…łÀŸÀ]“"Œ:]«žIîèC .ț?°wŐè7 țč{@0ćLÙöí5śŒŹŸIń‡d©âÿgè7 țŰÀ”»S„ÔÆK OWŸIá‡dT‚/œ]ôĐùòêRĘŁœ©ÒŸAŸÜӞIîìCČqńś‰čO=Đo@X͘éɔíYÙś_‘~9îńCâèò0öśô{€8}Á,uČ^$žIîôC2.ț}`œóôĐÇ5@°Ëßö矛SžInìS’Qńïæ>yàhôoćł 0!ʛź€*ËÖMžInôS’QńïsŸ=đ@ÿuÿ±ÒœéŠU)žzžIîæKRrț 0ż Đï ĐżîRʍ„T©û™œq~9nț[âàü`țf‚?àđüć7ßöíp`ü4ž9îé[ÒèüÓ`~UÀàŒęń)Š”&˜5êžInî[bîìû;ÌSèśŰ RŻÓW}ć:*žInț[ÂŃù/Àü*ÀàŒśűŠ) €BUCUE©2û)žInń[âèü`~ đ€Z@”60BêăT”ŠŰݧ~9nć[ ćïóû ț€Ÿ€~ïi`B:Č"ËŐ!~9năśÿJùÓ`+àPÖK—„RHvüË žIné[â…óÏ»*űêÀĂÎ}Sj,„JșźžIźń[âFùw€Yü*Àć…Z&€z›*V„iL›ÆúžInć[ÂkÊ?öÙż€ °ŸO`BȘ»©’ź€3—Qß~9źÓśÿšò{€yú? țXßR|2ćŒoöw^)źç·_ Êï ÌŁö& Œû0É~9î˗?çśæ€_ÏÿŻăgŠłœ¶hùđ?žIîăÇÿZÉg@9U]öęge{ß”śûœ?ÚÀš»ž9§ÿ@Š? ăTóHżV­ |ٝv§ĘÍïżĘ€‡ź ~9ś—?@—ść=~\ŻžšœMÚ~Ê=ŽÀyky^)î··.·k”ĂŁ_ë€ÄÛË}Ëwù~9·? .Ï Òù,ûß°ČJ+ećÚÍ lÍWž9ç·žŒ7H§:èc?ài“bŒ/vʟźK,ȚО9Ś· .ï Ò©űèÀ^ĂőđâÔzïśç” >șœž~9îû—?@ćśf ô àŻqOĂeï篻ż=ßĐ^)îś/? Ÿ^ĂúđPq›ߌT€ś/g^)îŚ/ż ßźa„^ß'Àú?ü”gz(~9îÛ/żÀ?.0éŚő ČˆȈ0șÁmK(~9îǗ?‚[˜%@ Àü4\a‰†qxè¶~9îÛ/żÀ?¶°éŚù–@V„‡ńkżą*^)î/? Ÿ^`é€őö€Íż\ăúçÚ\l^)î/? _ź`O€źș0$Ś9`XÇ~)îś/ż ]źaÒŻèߙ€ćÎțyöRúŒuŠ0~9îś/?`_¶°BXđbi`Ę_V9N7}9!-~9îś/?‚/[Ű!,xÚ ÛâK§ÛۚŸ€%^)n/? _.°éWűőË]ÿ<[îZśAw^nÏ/ż ïûá!œžżWđk^-^)n/? _FŰBé—xęĂXîúśzò1ÿ{ž~)îś/?Đe„=Ò/üèćć팧öR9éÿŻ—~)î/?Đe  „e€üńu@·Ć—țN7«œò‚˜^)źÏ/? ”/Űs8~©ë„ŠÀòćăŚÙK©L-,^nÏ/`o€ęŒđ€Nô5Cű‹KÌÈʒ^nÏOż o„ł~h€țߥàïž«™ ă:ÿă ^)źŻ/ż`/ÀžĂé8[%`…qQÊIDÌăúëkJ~)îŻ/ż`áìQ ę"o‡€Ź0Š(و ”Ì{E)^)źŻ/ż /À^GÒ/ –ăNâN„Ú ŁÎ€^nÏ/`/À~:ą@èàÏÇúiŹìżx-m^nÏ/`à!ìŐÈBîxș üțdSdC$^)źŻ/ż ÏàŹ# Ò/ŒțÙX>Ÿ?Ÿ?7?słû ƒ^)nÏ/ż`a~ƒ=G€_üĂ`čëß?żßŸÔÆ.Ng^)n/ż a~‹8û‘ę€ʗĘăÛă_ ŽŹĘËÓűOggSÀűż2W#ÉÒ6b,',-.,&,---'*...+),.-+',-,+$*//,(),--*(,,+'$++,*%*+-+&(,++*&*,,*$)++*')++,)'-+,*&+)))#'(''&%,)+'$+^nÏOàżĂ~Fd€ô ë§îj»œ:·Ù揆Œ(>źÏ/ż``^pžy€ë=öX?0àŐÔ^źŻ/à {Y ę@żPò!vP*úèȚ^)źŻ/ż`a~ƒłŽ(HżĐ– [œŸț}șÙ~›ńßòù^nÏOżƒ$óœŐH‚ô ęaČŐûżO7۟{űć|>źOżRx$ì'# €_àäz X±CÔ)Mț8 .Ï/?)ao#ÿ@Ww—€šhéê>./ż`ú„ędÔÛ ę€ùî°ÂC”RT¶h›J^źÏ/ƒjĂ^GHż ÿg@¶Ûę‹ß§‹3ƶŸÍ^źÏ/ƒrÁ^ „…đÙéûퟟ>2=c~ Â>./żƒș„ędÔł ęÿ»ŽÚNż%æÛć„çO§ .Ï/żƒZÁȚF}áÀ#W€ŻțG > .Ï/ƒŁàìG= „ ßÔ@›ËLo‹Ș« l>NożƒrĂ~2êHżĐę[äźçăŸg„T©Žæù>./żƒ4ìęÈéűĄf wöòöÿ{»’źŒéűB>./żƒBĂȚ:€_hÿžrù|üóŁ’*œ™ß”` NÏ/żC„°ÎőÛ@耏ß;Đæò…-ÛaÌ< NÏożCî3G}„p0À՝$àš#S±‚Y±]/>NÏożCÚÄÙF]éàüS[ì”xO„–Ž>NÏ/żC…0&Î6Č@ú`ü{šíöêËoČœ¶?í/Ël>.Ï/żCvÁY"€_ĐțÏZm§ßœ[ÔáÛ 'Ú€ N/?ƒ…(ï3ÇŒ €_0€}ËČÂU…^æțűOßĂą7œAÔÛ@àRw*Àș/á&F  Ž/żCÎÁYÇl €_`€Ę5rôItL/­Ó—żąS> NÏOĂaNÜKDAúô·j&Xá!țŰI!ĆS_Ń!> NÏOĂć8K€ô đaߡűc'ŻR|à»h Ž/?Ă ÊÏÁ=Gż€ô °I@.GqqșJ…șŃáțűO?Ă œű[T9P|ÿT‚€›óÙù/?ĂQțî)Ș=€țy–˜^ŠȚćśž ŽO?ĂȘü{âÌQŃ ęNȚ7Pm±7çÖpòÚȚȚ-[% ŽO?ĂȘüĘ8sô Hż€ùÇ€Úê«sßäzm?3ŐæÛ Ž/?Ă!ÊżśUÒ/ ß'ˇxO‡~ÁĄv”țűO?Ăz©7nŃŻXß°88Č$‘Ž.~HțűM/?Ăr©7Žèk „ú‹Àß0~نJțèïo?Ă·|éoqD=€ș”đW§ąT(őöžáÚțűmśO?ÿeäâolA@èú© œyz螯Ăÿévz7țűmśO?ÿeä’ol€ĐxړáìO/·šĂkûWžlç^țèíś?Ă7|é7Ž@@èèę„Ș*J…­>ó ȚŰíś?ÿcű2ß8"„€ËĘÁŠ,I悓Țèíś?ÿeä2żŰb{€€ùú€RĄTH]V{’țèíś?Ă7|é7Ž` ôúż@›óæŽ*g|Û_}Ž țèíĂžoA ę4ÀzÓ°ÜígLOs9SÊ\["Żoțèí?Ă3r©Gh ôÀ·ÏÌù]ł‹3ŸíWM­êȚŰmżĂ™o‘B8Sj@—B©P 9à‹$ȚŰmOĂJYòŐ á ĐśÀz0„L0ÈÉȘzȚ譏OżĂJYòœ =Ÿš†©/«*+ČąŚú;<țèmOC€LƒŒEwB '•àŸ_1Ę8}L”ô:$ȚèmżĂŽšd@€›ę™˜Ÿ*+fEMáK[ŸŰ­OżC€Ìy‹j„p€đï7€œ^J•RÁ„}3GjŸÈ­ÏOżĂH^Nò” ä pûH!…„ÔyóúȚŰ­ÏOƒ„Ìńˆ„p đú„°Ÿ^nîS6ÁèăŸȚè­ÏOżC€Œ“|DŚ„@üęŰëR•t%LIgÍû ȚŰ­ÏOC$ƒxDőB |ƒ őß-5ö6ULHččšŸŰ­ÏOż`„Œ…|Daá*À*.üEŠÜܓ ŠÔüËp žÈ-O?àR.,Ä# »! ŠëÀ,ôa %ŸÈ-ÏOż`„Œx‹ÂÂ4ÀqŐÀ…]ÎôŰ>üœúeŸŰ-ÏOżàRűKÄ#FU „h€~[À_\€J©îT0aŸ…3ŸŰ-Oż`„Œ…xDĄá ÿł©JSo:˜0«ÁjçŸÈ­Ïż đń…T „h€Ÿś>ț"UjìNÂćW۞È-ÏOż „đ/oQŰ_ äàöé&€ô%°bĘɞÈ-Oż €đńˆÂcáűòà„T· &0MŚFŸÈ-Ï/ż „đ/àˆÂZ á ź G&ÒW”ŽY‘UččFIŸÈ-ÏOż €đń…lcá €9ŠéăôRHę‘\€žÈ-Oż Œđ?àˆÂșcሗŸ82‘ŸȘu̒Vôś: žž-ÏOż€RűVâ- ăs !@ôW&˜’ &€n+ô•žž-ÏÏï5©|QÈÏ„\·€B )"e7 žÈ-OżÀ„đ—‰·(Źw !€ °țZÀ”v9»k¶o»ëz }žÈ­·_ Fù.8bü;†@Xđ©RŠ'LH}op džÈ-ÏOż@„đÓo1ê;†€őï€żÈ”3=ß“îXË ~ž-żFùö8ąŰX@€ûCĐ„o{B yæÈ=Ž~š-Ϗï(•ó[àv !·vL0ÁXçŐò~žMïu©|Â- €1„pśÀ€JU…Ò mkJžž-ϏÂ- 0ChŒ|)À)§Ê)§_=^p䞾-Ϗï•TȚ/Â- °1†ĐÈb€rzMwÊ©j+óHÀu~š-ÏŚw+•ûEzŠŹCC€)€B©P )dÍ<^˜-ÏŚŚJeÿNzŠÌ;†Đ_Ô€ëŸ^šMŚg#ûƒôű=†Đđ¶ ”° K(Ą±à€ ~žMÏśśățEzŠÿz Ą°ÿ-€SN9ć”S̶6Ç~šMÏŚw‹rÿ"=EƒŒ!4öï8U„•rÊ)qȘ4€u^šŚ7Ç8żSžą€›, 4<8ćô@”Sos>˜^˜-ÏçWJ«‰”§(ÀđBÄȚB )€űê7^˜ÏŚŚ«”§(`sÆ2\ÿ§œrŠPyÙș^šŚ7ÆùòüÈB8X/paBȘ»©” &€ŽŹ{0^šŚ7V)oQ@§ÇüŃX±/Ƌą­ÉïŁÀ:^˜Ïç7 «‰”·(àŠG€.LHu§+éŠ &śÿ‹>˜Ïça5ć- ˜őBűjlßöí@˜ęm•>ˆÏçW…WÛPȚą€î!€_8;°ą=€ƒâ >˜őÏŚa5GÚ[0Ș áÀł €)€ïđ:Œ!Ć„†+OggST]űż2W”™€`+'($),,(&(*,)&')+AȘ>˜őÏŚŚ «ioQ@k€§"€íÛ~М)ÛŸíŻxóœ >˜őÏŚWȘŐHoQ@©ÀżîgÀ)§œrșS¶Û{mDˆőÏçGݶĄ|EŁ „ĐHH!J…RHl7ăŒțwőŻÇ§„Wč&œE…ŠJÈ/ âjçˆőÏÇŚ…WÛRȚą€FE%ä@b:)€B©P )Âß;z>ˆÏçWݱĄ|Ćț.€țŠș¶ïđ:Œ¶bèÏ(ˆőÏÇW…ŚžPȚą€€E%„>%а}Ûëđ:ùRÈé~*xÏûŚ5Șl)_Q@«ąr€}JH!…RĄTHyÎ$—țgÏÛ·iTûŠòÿOąr Źo> ZŽ'țwÏÛ74ȘŒŸâăϊJÈî€B )” ) ń…&țwÛWhÔž&}ĆȚE%ä@ôż„Œoû¶oû¶›ëT»țwÛŚ5Ș]“ŸâCŸJúÏ©o|Ûϔ3e;À n\țgÏÛWŐ«œ"}ĆŸA#äź›Û·}Û·ŹŰęnmȚgÏë·iTzEùŠŸ‹ŽŰ"!…°|ȚWÏë·«Wù†òĆûŃr€/ß&ŰŸ „.ÜNȚgËW=jŒ&ęˆb[4Bpű &˜`"+˜`JżŸżȚgË·<Ș|&ęˆœ //LdEV”ł"° \?žWu·ÛgëQéšòÿfÔXĆ/DțèƒpűTą.Ÿš·śÚ«–χ^ë^ŒhÿçeFïŁNŻ*ú+.>¶ńósÍ­Òk|ęÏì'ț›űś•cË?ŻwN­ÖÂUÿEs1ŻÀ_Tôlœh¶d)-âĆâż” ?ÿ­MűčűomàüKűÿÒö /*/šàčűomjü/mjü/mjäM-ś żhjčùES·{"?çS·{"?çS·6áçâ_Ú`$mediascanner2-0.115/test/media/image1.jpg000066400000000000000000001402311436755250000201460ustar00rootroot00000000000000ÿŰÿàJFIFÿÛC    $.' ",#(7),01444'9=82<.342ÿÛC  2!!22222222222222222222222222222222222222222222222222ÿáLpExifII* †Œ›Ł(2«i‡ż%ˆ#û#CanonCanon EOS 450DHH2013:01:04 08:25:46š‚9‚A"ˆ'ˆ@0221I]‘’ q’y’ ’ ’ ’‰|’t‘†’ű!’13‘’13’’13 0100  °   Ę#ą#ą#ą€€€€ 8 2013:01:04 08:25:462013:01:04 08:25:46`7 /qy"ĆÔ  ì P˘  v€  &0ƒ“t•@€–ä—ô˜ô™5ü ĐȘìŽĐàű@Ë@° @¶ @ Œ @üÜ @ Ű!^ÿÿÿÿ07 Lÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ7o·"!D ìÿ l˜ lSűÿÿÿÿÿÿÿÿCanon EOS 450DFirmware Version 1.1.0ȘȘS0S0hSS˜’pw7»»@¶q #ÚìkôhûÌÌÿP òđ0[07‘u’1.1.061ŽddQ dedûžȚȚȚȚȚȚȚȚȚȚȚȚ ’æP‚ș%•VEF-S18-55mm f/3.5-5.6 IS Ÿp` ° ° sssąÈąsss™™™iÇi™™™™û^ę^ęąągAżț[„ęAżțÿÿÿ0ÿÿÿÿp¶qEF-S18-55mm f/3.5-5.6 ISK0336838Ô 8,8ÿÿP ž<"Ű<  Ï6 œ <x<·ïîùżŒ,&çć±ÿÿś” Üêé$qtt8„ ą 0òâ Û V Ïèè${[YŚxX U ia7ia7eę^7ŒœP ÒXe ;pÈhhn € Ô ÀŒœE©  Š-2.ÿÿ}ÿÿ}ÿÿ}ÿÿ}ÿÿ}ÿÿ}ÿÿ}ÿÿ}ÿÿ}~țdä”*–țoÄ'Ïțˆyl íț—RX'ÿŽpBÿÂïàdÿŐÊP•ÿó›\Ìÿhh@7Ű9f Ź x™Ț€ ŁÁÁž ŐóŁđ Fvi` ô"ÿÿÿÿ{D=5)+6:"- ,) 5B G>70*#'$Oź^L8'(>/-BEF@Q:H@6HU"'(«”‚o]JP?>:6Afź^M9&(B-,AFDAQ9I@3HQ$*)ź–ƒo^JP=:51:dc3&"! &/!%  "q\NA5(* ' " Èèg  .±<#]â‡c€šł }Ò"őÿˆ.Ęÿ {ÿ"2ïW‚UéŒÿÿŁâÚÔŽ~&yÌ  &Ű  dyęęŚ;'€/Anu+H S‚#Eš#Č#ș# Ò#Kq™ › â›.2013:01:04R980100I$Q$(Y$(HHÿŰÿÄą  }!1AQa"q2‘Ą#B±ÁRŃđ$3br‚ %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™šąŁ€„Чš©ȘČłŽ”¶·žčșÂĂÄĆÆÇÈÉÊÒÓÔŐÖŚŰÙÚáâăäćæçèéêńòóôőöśűùúw!1AQaq"2B‘Ą±Á #3RđbrŃ $4á%ń&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰Š’“”•–—˜™šąŁ€„Чš©ȘČłŽ”¶·žčșÂĂÄĆÆÇÈÉÊÒÓÔŐÖŚŰÙÚâăäćæçèéêòóôőöśűùúÿۄ        ÿÀx !ÿÚ ?üÓ ü^cŠAáÍ_d 27Ű`z}*ÜŒu-ëZ'…uŸ” ƒ €†àx>€ô—2 „űc➎ M草ˆđĂ}y5ÏÂOÙڔ͌†őhíÔn2Ž'hÇ_­Cł('€ŒC#*Š‘xKg/§Zșÿ |aÀ‚OjË)çi„țšąè,:ïá/ŒŹ7Œđæ©È.è»ĂŸçUĂoșŁÿ~È,I?ÂßÛEæ\x{T7ÜŃ2;Už~ xæâš k±EqÚXg#?üšșVG{đƒÆšlŸ]ÿ†”kwÎ6É œ‘ëĐțU·ÂŻȚMVȚŐI3±DG-Žžőąè,ÆÜ|.ńeȘ–žĐ5$P@$ĆĐžÆ«·Ăÿą3>|Ș§)Œvț†‹ ±đNŒ JŒÈê6RkĄ.ïi$·žŠ+€đ^ș@#KŒ śÙMÿ„;[ ƒŠ]çÓeÉ$đ7ˆ!Ž9%ÒoQ$ÉBɍŰ㊻7ÂßۘÄȚÔÔÉ•”rÈ3“ű`țT›Iٓ*‘ŽìûÎ róQŒû_YY@‘\îr)śW‚N8ʒ3Z_kœóo§ŸK2H#Žç!ä`l©ŰU”p ‡?6-3„”|%i ȚĘêâ»›ż6xä‚ò LHXŽ{UŠš+‚ÄÎF:V?‹|#§ęłQ»ŽÔìnæYGm:ČÆŠÙ GRê1Ôg9Âč6<óIŸÓ|9š) #<2 G;pÇæê>§Ú»Í=ŚÄQÛÜ[ʉo¶öĄTÉÏL‚v©lc9ŒU‹WztbŐ/.ŸÛ :ł+ ¶ìOÎr{Ÿ”aË"[\ĘìIH;g #AóŹ}I=xÁĆv&đŹûä›JÔ/tèmíĘäY”îi@2Æ{1 NNpqŽ+NÛÆ“YNmmc„»‹y’„‚v’Hă,ÀqŽ(jàŽW9]C]—WŐąșh–S}ŹB|àŒ$äqԌúòhĐïd¶q6ąžkÍčŁ\€’¶vă'$óÓ©iX–ú˜~'ÔŰ_ÉćÍsN3"°ÈR3ƒÈ<}zúŚ-fÆ[çžáŁeÚÊÄăŠÈ«Š"^FÀĐÛê"î’HÈòÛ žœFGù›."ÊÍł$Q°UD ’9Á<ŒààPž­Ÿ†dÖŻ”m“Ë-ò“œ'Ż{ŽȓìÛI  ĂkțÙ«a+–$ÓüÛT’A…;v‡;@öÁăéőŻD·ÖîużZi°Ź w§Ć-€mŸ1Ąg1dÎÆAlg“XâaË&țùŠŸ[˜ŚŒZR}?]?S6_Nc–íŹ$Hc d,Ł|œÇ·nAÀ8Éă©,Ż­šçTș¶‚Węă@Ê€2ĄPûz`ć—$dg܂—‘Ś{&šé’ßYȘj„vȘÜ( Ìăp«Ű(ÈÉé[đèööłęžâ;f†e\}ža•„àœd ź ìAëžÎۚ+t9„±Rș[+8 ą“,pĘ۟;nÜ•F=ÿ“Ś4Kč ™l`ŸxVá{kl(HÔüÀ6NQțqNäČÖżȘ˜ŒšŃ†DÚK9$ƒ’3êHgÿŹE?ĂpÉȘĄ¶·Ń,昰Î/ ‚2ùÈééÛó5c5 đ~4%” JÜÆςö çr± 1ś™ż‹ߑ 5ë&/łÜ””ą#I[čĘŒ›qÿtsÎ=3Êœô&ÚÂë1KqqËL҇P'ŽbYHl0qÎz{ăȚ·&żŃÖ(‘ŹòoòȚÙ 6țpÛńÈcÇAÓ4Ærț*ŐĆÍÒo¶‡í “#Šrż1#±9$óÜT-Obąc‘!] ąžI9ÉÎÆ{ ­ŒȚЌ‚KˆÚàCij­±8—hŒőQ*¶ lÿŸ‡€ț’čœÉ ‚kĂ4Hąœ[sŹĆ‡ƒÈOśjܖëa2‹ŚÜXĐÛd1$œp8ÈÊuÇ5*â[’ÛH°\Kûœ’*ČąD ĘóœA#hŰ;gźZ5,–â{{‰m„[Ł$茕ă 0psëŰ{UnXö»}JâK€Ùo±œcÌ}Űn={Vž‰©Om4žTLŒòBÎ9 GR‘ƒÏšȘ±w=;ĂŰèÍe{©Ì·lnĆÁ“c9RèÜqœÉ<AŹ/ˆšXm[Ì šdfó#U;ä wc•*ŸęG­JZ’ô<ËSÓțßmq"—tnčÆț2„ƒŚ“ƒïÁíÊYY›Ę.Ò/‘1ž–?0`OàđĂ'ĐV—Đ‹–æ€À͔*…\ï=ƒòôQÛ·<Óᷖh՟~ÉäS·‚1ƒŰÿ,ś€ŸpHé8džéQvM$›"Vȃê:gùțŚ[‹ÛˆcŽ.§;J,7€ő­$ÂçGá…ț$ń=Â.™€ÎìŽßy–2Xž n#ԌW hÿł Ćû\ŸxżHđł"±—eŒ·NIè6€rpMeZł§Úæu'ËaúìŐáinLÚŚ‹.”Dc…`rÙÁ\¶ïáàőíéÿ ü#ŠkWú—6ł˜@ûNšÌš™8ʎ3Œwš„RŹÓ¶Êß3–„iz=ÜŰùP€¶ź-–_0À>єăqÁ㎚3(7 dąE’c ƒï*€äàc*ŚšĆhÙßanŽy,áž1M=șÌ<„*°“'nì.zŠ©™p&wh$KŸőHŽžP–àüĄ±žsŒŸÂĄ ćc€k/Tónd‘o„C+dŒ— 6†`GăôàÓÔïŹa‚FYâO˜łŁ+6ÿ˜Ž6žS9ę ŁEąÔ„„xŽÛZȒÙ,€yiœŐJš$ăw,zÇ=Ew^ł—HÔÙŁćĂállœÆ}œˆïCVQêz~(e†Ęm†ÆfHÛś{¶Sqʀ9^ÈN9ÍsÌé4‰.ŸV€(]pɌVÀ`œg‘yà)ŁÜóœbÙ<ŁÙ ,]Úa€Ä€8-ÔśăĄțqé“ÙYÙ*šxÔ|Ì\©%Ž2>ŒžŸ”h†v~Ö.Œ9/‰ ŃŻŽ€eÁŸ-û”)òœ¶:dŽ źçƒțizÖĄšêöđÀA!–B ă*\( O©ÏJN$NjČIáÿ†~Ó^ææŰȚy*Éz†nBăjȘwô✠ÁZo†<5ȘźÙ§‡äžŰœŒń!TV\gžA#ĄĆTn2ŽÇ›*“šísń·kŠxƒQŐ<1{,…‘EòŽÀC#ìx8Ï­]»™Ÿ(­Š—Ą\Aa«Íq<1>áś6†O3<<‘ŒđN9ź\L!);őz|ŸùšÔ’P·ĄäSxŸĂáìÛ\€òjos#M(Á\€…0r~éĂò=s]Ä]PÙ[蚖5ćí•ê6ËdM!ÎÂO\nzhœ=Ż,„ßOš±„H§čăZœ‘Eœ‚X 1_–LnÀó»Üçï ­™Ò: $’M–Ú.wà/R@''ZIo±ÄÚ€:Źś[ Ń„źžÊÓ"K1ÆFâ„ȱ9ź]mĄŽÍź$”ùčyÈ]á—,Ă$öÆN?Ú<S}u*Öz˜úÄwŸń:ùŸ[G<„#iŁVv77~ÄqӞ8«ž%ŸOŐìÖæ…'‰đ-ʱsžo<67#>ękUm šaÛ„űÄ<”8aßÜă?Zößű¶ßJҞÂkdœ”$JłnċۂHìpAëÖ©«‰+žÉ‰ôęvÒsŁ$Hž\łÆÉ»xć8nß0#ÈçĆŚńűtêCÚKÁžCÓű†=űcž•{-™àG«qöŸjÿÙzr&ű­à_6îlôU8Lî$4„ `€ŐÓxÒő­€ÿ„oI·»ŸÉ)öGśÒœr }ŐCÀŽ3É5†;57Üú.áŠčĆ^KÚ?‹=àWÇŻ‰?ÓVÓäÒí5ŸÉ,ł¶™Ș!Q’6XÆê8ËJà©9àšï—Ç—Ÿ?ŃtïèŚș/‡ok.M“$/#bTÀɌDZ‚‘òŰÀ 3˜B€9)?{uòŚńíÜú|ŐÁ·SżtŚ,—[7kù%Ło”ÏZxŽ}oŽ{{œ—Ńék©Äc`ńJęO;ƒÈ錄bűû\ÔÜČ«nä¶Aă&ŽČłä‡0DîŻćÆü‡æÎO`Ä ŒüčÆ+KʞĆùŠ·Ÿ"¶Ó,­”È2ŠíÁ *í+߃Œ‘pxÀÀÔ|JfšȚ4ŽG. 0Aʑ*9'ćëę R‹ëđ“âˆôéöi)óÎ&ÂHƒnY¶Ÿ›Š:û{ԚwìÓăÜj3i·%ÒZY5äűč#ÎW?Öp~^à8ë…'c)b"9Őü €é‡QŸ6)nč EÔAŐÆ e·ö»Ö‡Ăęz 7–&‚ ”茯Á˜‡ ƒĐă§óéYÎ.ƱšzÍĄüAĐą¶‡ûJD±GŸ&fê SčŽr %žÆqŸQ^GńSâȚ6Ô-m<6HÓąÜLÎm3|Ű<ă#8ïÀï1j>ôŽœ6Š&|ŽÖżđls¶tvN‰1%±—\nÈ?ęzô?űÊïĂPę›KA4Í»æ‘I ßùW'öŚ“U4‹țŽ>ăĆŽžk•a’•dšłÙy»}éoŻCÓì5ęQôdŠïK}RăQ&ßÉÀ“qêÄ6GÌp=úڱü<đퟄÚÓ]c~|L ™a%#\`ìéÔdzä;áˆÈĄ„ĆÆŒș”·gÓäyő|MÄărYá+ŻȚÉòčigŸÙÓŁșł4m/ô˝P_AŁÛéwFÀ^$nź &đ23“Ç=ÿkMv?Ése ”‚í­š_Ž€8û zŒwÈŻJ“Œäžš6­ę[Ÿ™òźtG[žĂÂöș“[‹ÍêÌy·vòݜ”ČO €ż…s_>%Èö2[èóȚĘévś1ζ’1sč)Ü•$úqDáì›qíŻëú\±ćvè3Ć?ŽŠ…ń úŚÄgP–xO—ćŠ>a|šêÄûæ» ℚä:x7ĐéQ0T’tÊvĆÜ ć\`óžq^n6„”ąźÛg=XòÆ-ő>{Đ~űŁĆö6™±„<&Q„Ă6Nxqśò99üzśűhŚ—ZííË\E¶WÓíöFaà.ÌrŰÆxûœxŻQŃqWîzXŠ—)™ń3À~±– }+Kč·—Oq-ÌÁžo0àVgÈV ÀŒ{ƒUő«Kh”­ü?„$śŚŹ–ì›Č ]ÜÀb:ùUэąŽÔŃȚÖ3ŒiđçÄ_ŒnÚ-ûèriòĘ%ìÇÎInŠFʈÁH*€Ù㘠wóHÿcíb;yćŐ|X`Cn'A«Èeˆ#ïCÏĐw­\”w& š.Exd(Òț[{ßÇIÒZÙT±aœàżcÚ€űłû%ĆđŸËÔ[Z»Ô4Ë%ű…aÚN>\n<óÇ<ĐȘ]6ŠÔZÔđˆÌÒ^Jš2Êm™¶Ź“ Ôdț•Ò$‘ȘDúË+Xb@€öÀ©ë֛§ӑSÎ+á©ș4myiČșëčè>ű]âŸŁ\hș\†Ű>Űś}2GOÀ~8ŻE›áOˆt .Êáô© Go-­ŃŚ|Ž€†ˆÀéxïéÔ«rûÖ>ziÖŹÜúő}OGŃŸ&ßO‰<=޶ó6S)+H͐KżVïߎŐŃÍăí{țèSH{“zDK q!ˆÆȚărHùMpÖ«Rræ{ÿÝZ>Í$…șŐoźüau©ZÁ âń­šO37‡rù‡ÏŒ ÉÀqŒcÔàMźű“SŐït­ G—Xž’%ˆŽŃ6mFàžG*ŒőÀ&±övšqIžß Łń5±o@mSKÒídń Ù[Z]—&Ű\+”Œ›‚êÇê+O᭜Ÿ»ž0[ZÙßȚÛìŽpë!–|`sŃc`Ă$ô†(›ä’’{Ûńÿ&Dê|^v8±ąű‰ÎÍ/ÒîêáśČrŽHÜń5țœàÿÚéńÛ5ĆŽ,!Æ"TLn §BûŚ-‰Ę'ȘjÿžŸq7æv]D±ń…–—„èI€[‰€ĂIæ;ì9_”‘Đđ@śđÏÿڈæęoï$•eŽÙŻ”¶ș–ÈŒžžÈŻUÉik ҟSÏôˆüQáLèsÛêWš‹Ì!|M™|č©%cÆG\ă”eE柭ÜéZ§ƒŠšM-guÔdf.­9Aș=Äî!žô=JìuÆŁrw=›X†o ÜăĂ'ÉÓîÍ»?y—Ámč?žză5ÎEźÌś¶óX\ÛÁpTHmćÎűI9TuaÂço?E[+ö:b”)¶Ń[Ć^wń&ŽòÈș‹I=ÜnźVcęÜWĄê0}EiŻĂm+ă>wąxŐuWžÒ$žÒÂ+‡ ŰLʌčÈS…ś>”­4ćĄÁ]¶î·Lâ›áŻ„†‘€ÜÿÂifĐ[Éi*CoóË"šÛ)ÎræÉÎrqéXÚwż¶gK=;K‘Ő0ŒÀF$„… ۖ=IéYFé{۝P‚„inß|7Ô>ű–M[ÂqÜM€Í K2!UpyÎCÍ{6€n€mâai đűłÔcń<+·}BÏí$;. 9ʶ*TôĐÚRqn,ć”œ›{›Íbs:5șNá„ •HÆ3Ă|§€zŚ›xkĆ7SiòęșÖâ!>8Ù1ż“Ç^Œțu†#H¶üÄê{ș§„üIŸqȘ8±ž·–Ɇ8Ű”•Ś©à=ëąđź«ȘeZęĄnæîYĐ"ï€0n lzw=ë–toÆ+ŻæíęzóJ&=—ˆ5/ xžÚëFż‚öìʅÈčy€F= çżÒ­|Vđ-ڈ”»mxʔĐÓ͝l’U,€“&ÓÈț"ĂùVXjnkEkmëmŻ!{D§Ì-=RßĂr}ą+ű–ßśĐ9 ±Î=*~ŠŻűNmB-B?°¶‡†ìàx玞o”łrš ü?œȚœ:'ÌŐÏOË2›[lsZÌwŚZÛ[Xê–ș6©peÿA™Š"à‘”+ÂFïXRh·:/Úu{é–âtŒ¶/f“oHŚnÒW=ˆÎ3ÆjÒ1ł8’Ú]śÿ3”đ…ŻŒń*ć$ûMچ”&7ú\ŃÈUa9ó23ÀP} vúЉ©Űî·Č6zĆ„Ăʙ”3—óCŒçŠYGQҒk™ŁČnđM„ëSœő췓mĐęŹÉ,@'¶NOzÆńv±ŠŻŹìü7 Xˍ|čŠ} bV’0[qÇZn[€ŻsÂ똓â$:ź„„iŃdÛ Jț‘ö•Ț TÚ2äŸ_jĄàï \iwW§Æ7ÛXƒ˜Đ»FÊe HÇPkĆÊąSC§6ôòf”ȚƒwâYAŻêÇ€ÜA¶$1eŒ ·RÙ ÉŹá€ȚÙ͉2ęźâòFÜŃJ̱Æä‚3Zʓ•Ú}IšÖÌ䟏5‹ÏTŐćțֱҔșÛ6UĐäb@F~QÇșĘ;Ä7VȚ »ŸÔK6êćÛȚËòÄJ’ƒÈŒŹő…j5%ĄÏ^űoûKIÒæheÓăœ»È»ŸUÈÚ1Œçő5œȘ^húMæ”lô›S Ś ÂšVWïH«ÉVäęMiȚë]'v‘WÇ7·ș‰Ìt—7ͧż“ ą±`Ž8ï\–œńZÇH3XnőÆœÁòŒÂŹ€dOr „DæÓ{ëo›ÿ#IÚkĐî>űGLž]ś\ć‡R•™-ăNCp z–7#śźłÆ6šžŻ€XÇztû8R)PŠśuÁa°ú`źG±Ź€Ôa'n¶ÿ‡8ç.ZœÖżőÿá4Ÿ\ÂŻÂ4닩ډp‡Ç&† 2¶zšäŒo„Z·_JÖu x…€æȚŐĂŽsí! ŽG<jÚSwG°æœbșÁęÓBŐô»keâ]m/u]‹mn.­ń*¶sč_$Ÿlg5_ƞ(Òu_Oe«_YÄbŒjŁ dœJp j1Œu犟y4Վhòsz·„4-)të=[UŒŐ€°Âń±ˆĐ“–A»yHśúV‡“ÂșŠ•«Á§k•ÆŒmYn bˆ’ÚۉŽœÇ=>•)Iô:ă6™ëȚ[k/‡koȘßJm5…íæž‘RCòÆÉä`uĆĘxÒ4ű„éæ·E‹eĆĂLd/1È ÛÏ@z©JJŚ0ƒ‹ŒOvńEźčw᫛]cGÓe‡•\Šà6œ€n*æu{ =%ê^4YŽđAxlìrf*û‚îôÀ§^ȋwf ™»àÔ|/â AD–±äŹL ÛRûž;öí\^Ż„ôiÓĆ?Ž€ż!Öf„Çž’­Ûć$’ŰîH5MG˜Ò„Ö­˜–sxfűę„Ûx€K,ț[Z™G˜Ä6~`ȘGB{SîÆúÖúîïIžšÊÒha€»pPŒ…] ÀäÒi”bbĄÔïÆÚN«Ą€2XÚÍ{a3Fèäš'ąŒŒ€»NMxwŽ<]qŠűÒ'A°”š2Ț^ńčfSјôóȘ«Oí“ {KE.> êZŁÇkig„:ÄÌDid,HÎ'Šéča Ÿ”%œŠÍ,Éçe(ČsшìZÌń–“càę¶§~yŸZŒJYSq88ëšÖXxžĘœ°—Œ•ŽGĄhĐiÂ9ŽmVöÆkœńÁr_<ŹIÆp; ŚÔìnŒImk§k–ÍqxCGu(#0ŸWsqś‡)ô"łćn7:Ęn][<öâÇUÒőËO±\ïÜ4qÉ*ČȘ•bœwÁ5ÛxƒU[?ÚéțhxžužăU •$`†éÇJÊpæN î:ĐçśH4ę: Sƚź•er5%‰Y€Œń!ʐwÂő:U‹ú}–‹âhä·¶KûËćK9rèn`Ł âșj'$íÜʝ“ŐÜiW„ăÔt=+O[ïž6I74Š ä•ăżț:EgxWÇ:Ö«w{Ł„Č$Č/ú#KnŠŃÌH\Đdnü«™Es+oțeĆ©6™FžŐĄ úĆ”Âjg˜ŠȘ…;9çœ?„t·șOˆ|gö9lü?qȘj©‰ŹâbYàäń€ëÍa&Ł>nۙԔcnSó2ïăŽoۛßű‚b{œä‡Ÿ}}yšbű±ă8"»Šk‘Çv1p«vàKțś<ŚiÛ)9nAğĂ|·°űZKĆ9 tâ@ȚÎ{ łgńwÆÚwÚțÁâœ~ßímŸàGy"‰O\¶'ëM6¶3qOt%ŻĆŸXÜ ìüQźC09—N:žÿŸ!Io,ă_%É’?·I”ÓÚFpF8Ć Ml} ûoŠ>/łș[›_ëq\*”„ӆőÏz->(űÂÁć{k–í)̆+茞ô]”őÜȚöű§ ĂÄ?$1Œ$cS›jŒcn㊄ńŚâ&©—©xÛÄś1î¶[ùXn'­M©ĆlŠÒücńÔö±ÛMâï<±dŒȚÉ”Iê@Ïvożngóçńڊ^lmóP—v1Œg>œRòŠ{ą™űÇăŠ*[ĆȚ"%I#7ČpO^ôÙ~0űây€–oűć‘Bł”䄰‰ÍO*œì;+Ü[OŒ^9°žŽ{/x† Ł9I#œ‘YO±„Ÿ?“PK韎È>1xæŚț=Œ]âŸmß%샟^”<Šś°čïbÓüwű‹"“ÆȚ'e+ރ'N˜ëì*Ü_ŽgĆ+{…žˆ>.Že&„(`1ŽčŠâškž8Ÿ‡ÿÙÿÀ@à"ÿÄ ÿÄ”}!1AQa"q2‘Ą#B±ÁRŃđ$3br‚ %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™šąŁ€„Чš©ȘČłŽ”¶·žčșÂĂÄĆÆÇÈÉÊÒÓÔŐÖŚŰÙÚáâăäćæçèéêńòóôőöśűùúÿÄ ÿÄ”w!1AQaq"2B‘Ą±Á #3RđbrŃ $4á%ń&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰Š’“”•–—˜™šąŁ€„Чš©ȘČłŽ”¶·žčșÂĂÄĆÆÇÈÉÊÒÓÔŐÖŚŰÙÚâăäćæçèéêòóôőöśűùúÿÚ ?ÏČÒlŁłßrÎ0€L›·Y4Á4ł%ț_ččH$c­N.‹ùvW "ŸÖ^Hú$چn.PŰæŠćXzi°ÚMyȘrAÚ}}*MRÖŐ„‰Œ¶Č1ŰÿœÖ_|.ăiÏÌĂ żż­[čżu -ԑˆ†qm’Ož€+KhD%­ŸdmʃŒb·ŸŐæÚ[čF   㱿ç”UÒő->Wu›ËQž4lzûŠ‚òšțK{I€HK8$ʁć±·–䎳B7žùæŁžIštX# źJ‚­ž*9,$œčdŽv,qßÜô"źhșj[_GöłóY>o•żZǎÍűŽH_ ‚ rMkIȘIkż|`šv‘ÆGzÚm6Tž3ÆUĄF7^k]†yGPWżœCŻùŒ±…,ÙÇżÿ^Čő'–Ei!‰b:ƒZ6Ö;cifˆ‚9hòzzŐWޘÀòőEŠžEgĆ\ìçh=sűԞ]äŠ<ÇĐtcĆ=Ź$YüžÁTÀ''š«BȚH›ËórÊ2M0±jÆÚU·$œ%Qč#ÓąöđITSæ?'œú}jœœ1,ęăœçź1ÔRÇ đ^3Ć'PIÜ:žÔ€Łuh…ÌÒ«‘òô?^·čÆ-ÌkóÜ‚šoĄ‘îc33ă;‡84á NæpEńŐ/,§xm∩ܓà1Ç|VażŒ»`s‚6Ä՛y·čîsÔRÜ@êąuٌ$»ï@Éo șćÙ2Ű@ô¶3ÌȘ cÀ>Æ­\‘âsŒà`r+2i&„șČ1=>Žą–73Ę) Æ}qUаș¶čęâ© qóg5%»\>›·vû„œZs\3iò[˜Ë°mÁùÊúŠ`>?*âàJĐ(í<“Wo!ŠXÒ6à„;H·Ö°Ł•ą‹t|ŸrA=Ek¶€ ™ŁO20Ixé@\­šąđ4„€d|ž=sWíă[kż2"$2ù1ęj;)ŸÜX”DüÆLtúTŠ‘[‡țcłäzæŃنöÚèÜ1… ŽŸJlW-Áu‘E`8àzUfž*ë6ÔP7}}qVnoâyÒĘNì2Ć9 źïB\,Š,‰ć¶äVĆÇÍ*BFG$à ÔŸÔ­ăÛŹqd(ôź~KŻ2䷖[qïM š6q4°“;šPxȚ ńU‚䙁eb@`xÇj!ŸÙ#G,9]Œ…â©O:O.!fUÎá“@‹užPł:˜Q·#=ńé[:|‚pæ+…ÛŽŁŸ\W=蔻Űä0=GzœivÊŻ¶ Í'CŽ‚ ;șoDpœÛȚ›čE ˆ‹€żÖŁŒ‹Î‹ _#=9Će$ϱ៊zńL[”„‡À, ^™4Çhcf“kŽjfä2ă<‚;dÀ‘č˜ƒŽ}é,7çaV<±ăÚ˜Ûąq–ÜčëUĄÚČa—"ސ$v«"šb=OJr©l°ÀÈ'9š°ńŠr2*ϚŹá‘’öˆżÁn~ą“ŃÜOGr“śÈëŠ"iU‹ «ąĐ3yaÉŠ…îê àôÍPÈZwžÆś8^ĆB#É` e™€#žMYUŽč'žŒP"ąnnzŠ‘Ęv•ă)“"’vüSíă&3SÉïLcC`c’E&ìäœm=šÙûÂAă8©•ăoN) ©9Á„I’IaÏ”=_)ŽȚŽŐç€êhbÆt…‹Ê>èÇzŽêáçpÎzÔȘ»ˆ8ĆDń…|`àu4Ća‘Nc‘] kČŃŻĄčbÏnà3Œă5È~ìȘPk đŸ  œû;6Ű„2}Ź1n<ËtgR7WCo4łm©Áglë<ČáJđ0iúćŻÙźă¶Ü ەänĄ{kŠ”óÁjEUËIRÏ5ÊaŠæë‘žÔBòŠ€ÿ8ÚÎçFÚpș–)#òÓ9!ŽrO„AioêIjS“čяÌG”-ƝĂȏfțjƒ–`vő€wŒ·»û*șT“ÆTëZ'KkbÆ14OlÈ1ÉóbZȒÊvÔ€†{e1ËʗÁÛűÔ 7œačP|±œ*ĆÎĄ%ș/Úei§8d(Hú­ .N4ő1Œ2;w(ÈőȘé3ʄJ’H6b9ą9ÿ&„öąč…Ú6FùaÜț;ƒEœ·–öÿh”’Čî“?>ž”€Ąę±-Źâ8â&3“†>żtZ^Ąkum,“:G*üü`W4Í=Ćì“Kn±Ärźê™Ú;ê)"i,îaIeFŠă%Ăț†ŽÊïQÒ.ÂÁ$Á€l ŸđȘŁ[ ©™ĘŁóy:-s·:ŒÓƒćBÂ8úśȘ‘ꚍ܊gÆ/ćL‚ò†}’Ș…Ú2’1ÇJÏčŠÌaQ€ #|ĂŠ*šáćFš\·!=ż­KpNÆ $ Éă4Bæybo”«"9ÈÛfuĂ{ûSHW_ âAÓ5”•7íS{`[ŽòUì;AÈÁéő«jČ:bĂű»-b*ÈŁæGš>†Ž­wÁÌO^ŽȘcÔÎł"œźx8«ʑ,†{<ĄÂ‚Í댭$™ąù_n ’@êinn"Ù–G’u'śyÀÇáI€Űla2ïŽ6UțÇőjKpĂp*ńlĂ)#ȚȘÈȚJá ș‘ž2[ŠKpȘNïșYOZ`FŃ"GæFví8ƜńűÔvÆl)ó òFüTq^žÎàd–]œj6ԛ–/șyV  äĂa°Î€ĂÒŽ­íÓìńŽ8%ŸbÁłšĂKŚ,Æh„ÿ„qę)öŚ·ފ’Ą€P°Hg?Ÿ“q¶ă*îgm$‚i˜Ž„p@GLŐiŠ••äóIPăôȘæyíüÂŁfÜsœP:íMá†ßlQ„‰Ô6âôöŹ–EI%”­su>”R-J;°‚äÊv ­ÖŠû<2áYŰŽŸtgP;”ŠĘö€ąćËä}:Ő€†HQ€hČÛr6öšo,­Ą8߆SÓwéSÛœœ‰ČYćOś[€=èG|W„3Æ#áđ§Z]‹9ˆ0©,8,:U•„±ó Č!+HȘÓD­2ˇ ($ś)y1‘‚À@;Jś€ƒÊˆ±XГ€2ż{ȚțŒcŰ y óŠŠS†sȘ6€=jÈȘ‘‚…xÇő^ „E| GőȘ’Mt±«LìSÛő„‡d“ÂŒ\FO!œh ’K4Žë*9ùGBk.i>óqÏR+N[|4űÉQÀ`qÂČÙDkʃžyÀ$ž$€ź7gĄôȘ€‚C1ăúTï[pèxÀ„ Łù HöŠ„Bî† U˜Śvôä` pÄËaÓévnŒgç­LöŹTŹq’Ÿw'źiK‘sÏÊÇ­[Žúâ5A(;GcœNndl ÏqŽ)5p”ÁŒ„Æ )Ț €0}+6xœç$“È­s,â7}€ ƒŠkBŃÜrČJ,IÜą#“ìîv““ŽGJO6l)a’;bŻÀćcpäłòŻcQ) ?xFö힂šeYŸĘȘüĘÎ* 3źÒ„F>”cx–@@/Ț‰Éïé@Î3èWő§șß.OÿȘ•ąZEéé’Ê€•ÇOC@ș‘ … ŽŽ€xÚy§˜ëÇN:ƒN‚"c| çœyÁ>űô«›WËhœ1Ă ̊T`ŠtÓl$â˜ÈąˆȘì`{ś©ÀÙ22a0r1ßÉŒÙÀ?­.ÿöO¶}(`u æÖY$H‘Äd+パȚ°â„D©–-ƒê "^ȚE 1XłEjÇvšŒȚ-æ?|šûÿcyCG±Ž±E(ź§l­hźžmÇę‘銹rśȒ YˆÉSéSMš,Ž; ćH=?ÆŻDłC„Ć!Uh\‡iXŽZł Ą,ÈŸ[™C]žlàT«$„`t–3–ÎO8>ő ĐLŒ±«gr°7éPÈdLĆnáЌ29€ć•w• yÄęńÒ€ŠÎöèțí7.Ț„㊄xYFęÈÆ5Ç~•z UȘŐ#–5ćłÔ{PË€›Ł’7Ű Š}ìXm6ÈIpPäccgĄ«RêS=ŹK(’7ÇÊѐQ3«A™.·äïÀ#9ô4 žÒ =ąB€€˜çpöȘsĘÿ»ƒ0¶v•<ƒÚź«[IfÒÆŃă))ÏŚ…JđìŁh6E#±VŒ©ĆAif$…'g$ï*Ű©‘ŸVXѝA8lô>ő^H„ކE¶čÜà tő© “PÓíæ`I óÜc·ô  :…ŽöìŠF žçôȘí{6Ș9(țtëíFMF`ÒíD9ă¶êˆĂ,O ;Éà‚8ö  Œ·’-ž[š#5™qr2FÜđ dSćkbÉFPÇ –žŐ,hŁóÄ8üèBÚæâ8‚/Ż"ŠŽŽ[Ù„wÁcÛm+Ę+%‘Xăćç§űę*X’X䕡Ûà0Ćyś‹HŠEÚäșŒòz ŹČ‡fó‚ ǂ ëSI!Údi6‚~Ÿő™‰"$«ùQś±XæïsÈJmő úUžï­ žT·8éր+) $x%ûŐǶÖò<Đ!*wGórI§yks•p$P?‡Š;Őa~·A “6ÈóG|phàș·he8;v‚ĂÁȘ x„Y[p$qĐÔYV»–é€1Śë2YK•e cëLL˜Kzï<ț5vÖAnÄí,p ž•›æ` žx5q˜yšcÂńÔw čIßr”2ăH’E±6ÒŃ«d€qš]ŠdŁjÉÏZ­!u‰â*8~}hšúȘ[˜ÒŰŽŒÛŸcùUH_ F>RO4ҁBI$(éŽô©ĆčeKŒôÀ&‹’7žGëNXF{ŒĐpŽŒ óőĆ8mòÆÜ©8àÔ»­PšièWi9\=ê7A4űÉÀZ\2Ù©Ą‹“ÀéTÁ%O&Q—ÀÓ„ù–7cËMX#fÚÇ ÏJ‚MÍlŒì•Ą%š}L̀ÓŸZ͞ë|)lƒĄÜN>ń§°ŹDRâ8Ń€%†FqőśȘŚ1Ü© ČeͰ7\TÍqs3šËì'’`jŐ>Ìf T†är=ë2È “s ș\);wO„\†ÎÛçVču ƒGœSHüÈcšITz”s˶‰6—à`šgMnw™ĂHcÏ"«Á É>ŰÛŻLŽ jąF‘ˆćˆćł‚ŁĄúSä‚·EbD„xĄ1•Ü\aLB6,Œ†ù}iWdƒ,{|č«"+~ô`t5f8 ìsÀ eۀ`ò})‘ÂŃÇĂô8=êÂ>%‘>XêGZo‡s2ŸĐy'„0†È84%ŸŰ€Žs‘Ú„¶ŒŠŐŽűŠp«ć†$2O„Ukf’0ZæMŁ ÏŻiš-Ôź%†6(ŒH~êęjłźä*ÿ ŃłÖf·Ò>ËíĂGcšVBqMzÎ—=ŒvE6I#hûŒ}AźZâ"EX9Ò©ši~VóüȘséZ:Ä/ew«ÊÌŃ foBGJ¶îMŹÊv-ŠĘ Ä«j#¶ž$čö=…6dKæ22ÛžÁP3»ë6î+XíUą,Žs•=i`ž™cŒJ“#nńŸËÖ&„çŽÄŒÜ/Ę>ŸZÌžH‚ÄÂ3òä±dŐż°ŸžYœ țèêGkș@ë9Û±~^ƀ+HžmĐćăÚsĐ҈^V\!MÀü€ÔË ÌaÊäü?ZŒaŒSää—aïî(–š"ù—)dç„Md#«>PrȘy*=š)$è«æÇ\Żz–9#‰ ›vdpÎ)ƔHÙȚrŒ(=Ć[Ł2Ż9e#îńéTźpàˆâè6 MY”ùŁ‚äőÇ ÄČ Äźó€N*XìZ\OüQäœsE».æ'ËԂßʗÍ]òO(àóÉ=©Œ©•;NG#4‘]5»”Qæ$f SûŒđFèE~…–·E‘VC†žÇBGfEhÆÓčÏ 3Wîàû\‹‰vŸĐíҚšhû<Ùf2$e“éLv+$żè«%UË6ΞæŠvw.Y€Ś„T…O–@۞#„XR„ mng>ÔbÜ3\.fA ÁéțMnéúJ€óD!ÚI;Čyÿęz„M<3áÆcc†ŁLTFk¶č°`YJƒÇÔÒ\K(f˜ˆÄd·źh •ï-V;’û”źÏ—pç â#ŒîâŽ^ßí,—ä23üȘ!m f‹ÎĂ<„2( Û3œIæ:J~sŠˆĘ·. S·`ÁăéíD·Ș"ƒ89ӌf€Ó­–VBî„# Đu eH%&DŒ/A»=ûӚwKÉeÁŹŹÊ@““Ç?ç5 "G}ó«„.vžÆšdK'šŒćo˜z7ćW…ń’O*\ć—nűÛúšÎ“ æ#!,8<}jx Č'™”e^2i P-ÁY$œa›©¶„”‘šXÚhQFHÍ>ÊȚ&ˆN‘+ÁvîjĆĂÇof$ffˆ‚%@œO§ë@$[”Yf҃Âí$čÛŽ§5M‘îmÚ`ì g”Ú0yçŸZ]2öIU"¶™BG–ĘHôÍJ±< u˜B1+ÛڐŽjîY&ș]㉠Ś=ꏕ4jQd–û€ö«òž[‘4Y]žÎ{‚ű«>U8Î@éÏÀŻ>Ԇ,"«äć@țuŸuƒDë‰3ž84â­$țYm쌏Lő©m­^HȚ-ê“”…1\ƒ3æȘY1Î;UÛEv;It?„4ÛžÛŒr2)-•Ž@^Aûț”î›Ű Rî-†nHȘâC'%FёŸZ”,y”IpKÉúv„šląàm$qßҐ HWÊÚç 0@ÎriT bcÎ:TątRȘ“Q4‹thűĘŰt€!«"œ«(ăqȚ™*ä‚Čš$ç=Ï”#œ"ńß=1D*Ți«1 öÆIÜÿŒÈ ;Ž”*FYÎÙ€úÒóäçc(É-Ò„A#:ùŒĄW Hś  gX„çrŰ$ś©ă„Ć6íèOĄš$|ÀźQÁÆ2qSE XęźqŽæ 6"du]Ĝ~•ó<“æ0ć‹téSŒ„1É8öš•—krKm $ĄYÎE&X'ï $œàÓ&xÓ.KpAIp$ôÀrag.x!rÔÙSΑX“‡<NT.XĘžęïjœw4𠷐šœhÙ4ˆœxŠ!ß#JÀƒœękj êWGG' šÓ¶đ}șenź\û"㉄{ÓS‹yvNÀŻ8ăž>”]ħźO9WČi^đÜ.Ke›óÔçôźŽöMȚŚśVöˆÍÓdj eíŚ7-ˆç>{ò.6#r1Ù{Ő»MQŸ`"¶r}ÆŻXŽM=ĘÔÆ‡#ŒŠŠĐGîb6<{VŒÂælâ-Ÿê±{ˆą=Á9Í9|êŒg›%zȘŽ+Œ{šàbàuÍgêĆȘ@|–ïY©M» ™˜^‰a ߌƒyȚ”ug?èđFvE—ĘŐÙ&5ÀïĆ^¶ÓZPæW9=FhšîÈ~ló(ÖH‚—‘ŠÇíO–1< $Œç`țjÔÖ ‰%,H'nä’*#9”‘ ÚsŻùæŽ:ƓÛG"2Çs'æżÏœY{Ÿ;MQ»t˜ÛłšńȘŃùšy3Ì\Gè(œ`ŚlłŽŠ”@.cŁ”,±|ŹŹzŐ9Ž·RZU!˜’9ê)è|…™W8|Z”=ă[v@ÒÀn߇zAĄI6-»‰Q¶ŸșzàÔâȚȚf†— ÉȚG$ăMčXć¶Ì!•‚îun2}ż:,Ą(Šæ0ìNQł·a=hóŽ–öËžƒ ùN'š.nűS%w6üŽÇRôÊ/œ;–ŒăŻ<ŠČg3@‚UĐgäă€)€‘Ë |(lcŸsÜ՘bHŠ Œșț‹:Ó폑gÚ^=€©Î0iȘn.a@„+#99ÉíŠ`EŠòy#*U˜:țÄì“Ánû ùքš]Ôqd“v ·8ú§J&ThȑG^œó@ŃŁ4Q;•‘Óî ƒÓ5I‰¶˜É*6ŃЁšĐDF„32eryăì*ޱì*ČIčś=(驆Y`„GV#€AéTŠ#iTvÈJ·”XŒ¶HŒ™cË69èW‰„•Ü˃ÊÉÈÀí@‚ÒI%f`PŒH=…K IćĆ$m"ŁźòO„G ÉÄ,œ6r}*갎âIčÚ°òĄ–nčúPۊ2b`źÀÈÙltëĆ"V,a_—Ï=Oó©?=‚îßœÀÇzĄč$c ó“GzËŃÂȰ`䎅O$VŠZ™-śŹaŠđTđoΩÛa:íÊđxæ·,.íâ‰Rcìrzc?퓈"‰!±Í_”Ç -4‹ÈóҟŽ2:y{üÀƒ–ß±ËäŽńÈdòyÎ?DđIjÂHÜ,Š˶ŹGrdłšk€l㠃ÏÖŠ¶r°™€RÇ=)nPŻo YD«łŸà V0ë™<܈œ7Ó֔.è&8!gÓÒ§č”ÚÈWjșž#ńšîhh™ča2=( w2d•„Ș"@îäwś§„ž‰“#<čîjg„[È­œ“ÔőĆ7rÊEșÜg=鈥&ŰŰÄäÆzç™i ŠBž‚iłÚ21“86ś«VVíčq€1Ăc&€,v…”aŽw7··áHwĂłà“·©š$i#D`źœ{.*Ė‘˜@]­óuÏZŁrûD~SqóHĄ7țęŰ ä ÍIwm±Á'v>ą€(L`a†F7j@S"Y7 °û»i‘ÈRD‚Áș†< Đû#‡ùHòˆÁlT/ ƒyj/Cë@ƒYàÆ…cő§2>o0ÈÀđžĆY¶‰3æ1 Î:U»]*]Bî4·ˆù ń€ęh›Z#‚ÛƒSPHžYNžŰč„iéx •8«ŚV2Y|ÈNÁÚź K‹œEî‘iavŸP8ÜIțu„§]°"<ńW”­;ušžL–Æp+žÓ^xî <(ïQQ4‹W”ÎÎÊtŽF8"«Ï‰îćäúV-­óÜjcê:ŠŚ[Ű-îăYX'TBíY•Sč™$ŸHhÎTƒT.ÎéCzzÖŻ‰mÀ–9cŸJä5 źRì§iΔjY˜A{Äï©ù€œV€±ž”áJ‘Ț°­ óźăß۞xźŠhŁRV(ÀÈź‡$jÒ&@ÓÆ<ϘŽ;XÖGEÆMA„\JČŽ.cSO°ĘoSĆeU;]/"€>íăqŚ‘WPT]êk#S–GŒ@ÀgÖ«Ëkv_ eOLZÎ)‘Ë„Î~ÜÊÒ\3”ûšeë;O–8üŠŁqŸZłkm)ucہwúiôĄôÿ5 șă* vț”±ÒFU Œ(Ć~`űę*š„{Î[wN9« #8rX+7ɑÎ=ńU.VWHĂc€Y„ZčŒRšÊK|ÙĆUPś**+)É$äŒN•jÎáŠYšUù`ńśžíWíŹòÆîH‹E.m<(ÇCúP RŽ!ne/ PČÀtÏăôš.Úq!óÛj‘Çž•§©i,>uŒŠ0I<țV­îä rž™“…p@Rò€4U•ažKx˜ŠđŁ/ŒsUŒFń,A Žž#5a<ć Žÿx’·WâvT!Ő0vç9ő [dÂ#|Ź œíèF}ȘHcčX|۔ČdCßß4ïŽŁ«HÊn InCtÎ1SĆ?šĐŠ čŠ2čŠ[§›d)1â}p¶Â0ż1ȚêsŽEÈțtłŁ†PyLzśŹè„’ÆrW&RI'ôĄ Ńœ¶© Æe#9HŸi#ž†ÊFIÆàùŰgo^*ŒșmŒS4ŒkĂgî:±˜æáO›/€G|b€Ûł33"“ÂgŒçÚš^\Ș†)"a‡@ŠM§1‚fv%ŁÁR‚3ÒšÜÛ,D(R8ëœÓ Ûłƒæ7=qZшŸÒ|ˆĐČđ `ƒXF‚yÇ`(Ü6ńy!6ćÂäńžzšH ž[•‡Xے­Jʃ„JN qŸQüêÂÉ$öòFHŽL <瓊Ê7‚XíÚh€ q̇Egy‹4 sŒ,ĜźqZČÜȘČ@æEàčçżFKD3GBGÌŒnÍčRæ7ó~oș9ì}ćL0Ăs(“vÂȘIÁÍLąe˜‚㚏±ې70'ŻÒnBêdÜ9àëTf‰„áugö«ȘÍ+Ć“ž* îbFwÏ#€GűP"Äí¶! ͘·oŽyÊĄb5TŸfę*…ĆÓL„ùqŽiË UY#Ț€ž›˜,€aÈ ŸŐtŠycéUąšVùJä8^I­té\üêWș†Ší ‰șF#zՖÓïbŒ1†P§$Š»àéâ·ńä0Ę:šćSrćI#ŠöÇČI­ŒRF­žW±—Qźç„›G\ŸŒê~”~ÇJȑAšä°=GJèŒEàłŹ·:|ł#Œ»#|ǏJäô›?”Î‘ŁIÆÖ8"“’Žă9Ô|±Gkjš%˜M‰@êy4ÉŻŹSQŽX\đÓ‘gi"CwŁżG<©üj–Ł ę’ć$ŒüŒqIÍN§ńŁŁ“U†',ÎJă­PäÖn§źÏÙòq€}kFńȚ+«‰”őĆs·m!iN@cžEi©{Æ»ê^¶ŐfžX¶Ó]ț–Ö—ZkE9azŒ +DăûWĄxCNșÔm€,H 8ÏzÒʂKC Ä1Ïfà‚ù\VmŠŻxÒȘă#8ŻHșđsȚé’~ű™E"čÍ+ĂqjŠ€S‚*_șœíæĐÂYqŽòÉ%CdŽ{VLŚ «rˆ{rN3Ö”&ˆG3»L€…Qކš!Ę‘Íő'±íê(6Ű-óq·I?}œÄÏÈÆh{+Ÿ)–fÌN^ùšä€Bbçæ^˜8ÇçWQśŰÉnŒûrqÿë€%‡eșJ@;\ÆÊŒ1ŚëSZ«4„ùńŐ= öÿ ‚I Ć0òÉt#Oš©4đśIč ‰n ‘Ôòhj!Žéd`0‡œă$ŠÈžçę ZH„Ùčž+SYÍșW$"ÜĘ»ŒŻŐ{·…çDÀ |ž‰ bIemż&H႓Ž*˜Œ›–š2›ˆÉ?("™pfŽáRH˜«ŠÍčéV€¶Üî2žˆÄ Ÿ^8üúRR ÈŠKt\?Î0s“Ó‘N”‰‘‘ ùÉ?"‚«"őbĆ|€3g?äÔ?iĘšą#vŒțtÆg^™índÚ€üŒúUușŠçt’0W È#„k^ÂZgTc”9òĂÛ”ĆŃ„‘66YˆNœ~”Đńß=ÄP«°‘XĂcWR*#a7€‡ü+°ˆËćq‘‘Ž2œńÇœjCsäDŃJű•[•qĆB^Dò‚ž€Œß?„d9,BNțőŸ|č>O9Yű+†ăŠČî,%„É· žÁÁZÌŠU*\nÎqŒZŒÜ3ÉôùÁ r9ĆDiˆœXqN;œfČXÌłü§šŹÛ{¶‹r2’ {Ö€!]†Ś ŠĆˆÆG”v67‰v·G$Cs«ž ïRIÚeSh6íÁÇŸJä,§{iæÜ…Ł”àz՛k‹q YY‘3€Û0čŹ;“ W ‰±Ë/#ĐÓXÒ‹šoVOl`âČ-o„[imą»71Ü71ÜŁ8üüȘĘ”ę”èqć+ò†Æw{P XzH%RìËû§'’~Ÿ­ ‡ÍŰÈăh%F@êjÌ3*±†U.œWżÖ›k&.BH͇NƒŚ&•ÄYœłŽ+•òäV†8”ƒ4ÄNïÓ r=ëzCžlCÈÎI=}+šż*ŒJ«šÇÊÔĐٕ-ÛEč°x9ȘNŻ$à“R$WWÌĐÚÄó7\(­Ę7ĂvZ|ŃÜűŠö(Ôr-PîcìqV‘&$:v„©Z™,ìć–țńUâŻišä‰/ćKh—źy?‡jŰÔü_ˆÍŠ‘¶¶^c§jç–y »–'©cS)€Ž:(ágS}–ÇUÒt‰Çn$aŸ›'ҋÿ6 ÿș”HGfcÍeÁeæĄ%pqR_鳋%òă>­ŽŐÍíW6§Ł,„ró+ÜÆșŸž}Qnp±Ë+í^…á_ŠČZ:Úë1–€ÉÉZóŁjêyŐË+ÇozÖSŽìç†oD}‰Ž;ÛXŒ:„,Č0AƒÎON+Ä^ [‡“SÓĂ}ÎŃÂÊ=ęëĆFäĘFeÁă5Ûiž)Őìç7/) ŒHr*'^(Җ]^śZXŁsźO|Dr–‡ §Œ]3ß鍇q‡'ÒžnȚ}Q€2v2łÁ$ŚgáÂV;€PźWПsŁéÔÁMu‹üNŠ+hЁ%ó>u8Ć8éS_{iìì{ÔțTűtÉ t8őŹÏjòË4N§rkĐjÉ]\ùƒ2śÎ„ŽN»d^ŸőĐhôżgXg\2Ž=é|S7&…@qÖȘèđG<čzšÁK•ÛtO‘w[Ô-” s±€˜EyőŹ,5€9'uo]Ú=ŸČ%ÎPőœ2x’;±pƒúVÜ˚ÌÖ-- ú¶“(‰$Śšź~Sw Ûbèx9é]D’É5Č1lńLÓôŃš^m“ćP3ő§9F.èi€sqèśw,Ò62k:ëNč¶bdBTwŻCŒł}<‹æš¶ÓĆí©iÔsÚŠ7Ûg€‰à‘MŠw?ź’ËIš[Ÿ:ìc‰ì`łșĄùYNp;ÔòȚ]O Ž,(Ïzæ”5Śs&›z“ßÚ@!1ùyb28ëY)€=ÚyRÇćÆ=șŚ_ncč‚#"5G5ÜE‡îŚÚ·mGDRguáëx™YQNZÙŃÍՃ—·Çх6X.3Ê63ÆjńlmŚÌÀcŰTÆ|ČWfnÚÜ4 Ÿ6çš©Œű"%đ {VŠądżSV1͒çÚŽ?išŒçtuxò©„őü*Df CÈż8b0ÒźEËMl»N2}ęêŹ71ĘÈ"‹Ì›o#úæ™Ô-ޱÎ$Êl ˜`çÓ&œòìH± ‚s’ŸƒŠSŠ-ž–ńL1ʒxúûÔÏŁÊíÂH”aޜ:Bê$F4»BÛ swą™4ńÏ‘ÇûŽÆFóšaHŒS”bC.ŐÜq·ž?SYÆn|Á–0pÙĆvˆ‘…„‘–7'ŠäÇ8«0FđĘÇxŠn›p22sJV6RàŻMŹ äjÂGjŹ #Gč0đäó@Ìë澞égX 8%v¶CTOêéàąnŒ ÎX}ß„^Žî q3»•ÏÊ2=(–ę’ÙĄ€‰L §žÇ4ŒŚETˆÎ€Bűg Æ}* D±îwO(d7p3VžőŒ±A„cÆ?‘€ÔŒsZHŃó†8ê8íL,\yREŽFÉÂçzyÆéYš”OćȒmRW*ŰûⱊhȚÜí* 0OȚÏZŻ%ĂŽoç1#ą‚•2źEl›g!Ù6бy(v€BäúŐ/1„“ÌGbP dRÉ;”<Çï1šdmÀ8±N7RÙËnàæ©DÇvc=9©Ë°0„`"‘A? #=Ș2ó©˜e2ž2‡lgÔÀfIæŻĆrŰ*~ó*‰_Ż”M‰H†­5­7JȧvćàçùՖ ÊČ,Ș3ÁÎA:Z܆˜Ï{Ö­Ù3) € ćSq°łq0•g‘UÆGÍÁ_󊿧ÇšZFpC’ì=*Ɯ֑iž’4RÌÇ<àqQB^k=Ș<Œyg^âl›ÌI„2ăp:őühŽàZM•"@A\íàJ’țΙÈT(ùpCzUfœÂ›™6àÇÜ3LE{íAaVAä€$Ś){yö‡!bÔr}G֏êÍwvJ€œÔV1c&Ő vŽȚőIiq„wcIu‰!„[X ·OâuûíîM@ż?,ÌXś5QrjmŒ§*3íYJW=:~Uv„*ca•%OzĐŽ*HSÔöš烕#>޶jË19ák)jŽêISš·SȘÓàê}+oÉ"žźzÚńŃ·—žnă[VB‘‚=Ćr{ •ˆőjcđŰx~òJęșŽ›J‚Ir@©-ô䍈UU5 Knp:WCg<0ƒžo•ïZVĄRšîŽ\.g†ÄJËGæaÏjäƒÓš©d81ëÖŹê;VD*GÍP$S]2C ™$nÊەJĐIȌ­—†8Ô{+Ò4Ż €>óÒqç•ÜȚ™ôŹm;ÆĆË\ĆæJFF9 ZÖśfȚ6„îUôŻKIGZ›Ÿ%šcaSśTvëæb­ĘĆša¶ŸAбl” +T3€lAEz žÎ{EÀPËT_ìŹrŃź{dVí%ÔđìMšIöŽ·° W$W5i©Mg±źIcÁ­kÀXžÀäfł­p%HĘWòk9Æ/^ä€>ÜÜĘ ŸIśŁ>œ©HÍÏz–;ˆá—băÒȘjW·;}+U”{š(ʖJ©¶R‡É©m™Ą&D'$uȘÖXh2Ü->;Ԋ@7âĄÁߘ|șÜ//%ËLǑ IĄkÖÓ#ÛÊÁ\60jźŁg.Ł*Ê­„ękŸžÓ•d2Äćč§ ą­ĄèȚ›lmî#ó/+[jÖâ$°žćÈV\Ÿ`œŻ©ÍÙű~kÉLQË`:?š„đî©KÙ:•8íÍV+š97Ćk9Ï$šžë‚ F1O.…6ú'FöŒezŽi#ÈćxȘW:•Ęۖ–\’sÀŁ[čAê֗+1ČČ~‘ïZ*pw36GšçąÔ±€ńƒŽâźG­l\ô©qcčÓZ]ÈŸT/*€QU— ±«¶wêŚ:…ÈÀSÇá\dÚÂ?̱¶ÿŻaŚ~8äsùŃk-FąäìŽìj0†a4ûbC虾ÏűŚ;©xŸÏ Ž~a< đ>‚°/..gÊ<ŒcÎqK ±1n ŠćW7ŁAÎV} o&üÛz–+):óŽçҝ xđrsß&ŠK„ÜzÒr“Z4èь›™:[:Cœ\ŽĘéC‰ÈĂ 3#)uˆGŸ|S æbM(є·ĐȘم*.Ńwò&Ža°‚?XÊ),Ă?„0ÇáHx+XҌO.ŸiVź‹DMö’ĂăÓ”Xˆ3€i"°-Ïu«đBò:Cł18ëuyîWwŰ.áWź!kD”ł,„o*8ÀퟭX° ĘÄ“ÌŒeÈSì;ŐĘOIŒ–[ÒȚ[Čç“őöȘ•ŁK‡3iٚÓ'Ô5KíĄ §vìą»M)ąŽžR’G æ„°ÓÊÜÉAQÀŽôû(er«‚sšâ•(^éYž„±ućOÙčhY›YTSŽxŹÙő^D[œŽŐvçHîŁI$Ź{S§Č¶ƒt`ùˆ҄f·ŰäWG==ìr…òIö«qŒĂŒpz‘G—œDQdŽx4e<ßȚzÎS|śHwÔ”'ŰÄEdfF#ŽzŐH€ŒyRƒ9ő©/ Sș;cć=*ĆÚĂöd’, Œb¶sMY-ˆoSžƒYĆŚ–äî*kęGhRÈvžő-žêĆ^"ÎÊvńȚążCæ=Ž©€jâïšÇ]Ž‹M‹ízk±9ăćÛXw§ÿșĄêE&šK€žRلńŽž•Ò__`(„Ă86ŹŒNNmj{HÄYíÖšÁȘo“l„|Ęêö­bo-‹Æ)ÔŚ0Öì‹Ë|ÂłŽŚ.-4uș}Ǖ#»ŁÇ\t©4ùž[čb DeČUĐâÖÜyńï†S€zâș+œl,Íì2ç<‘éJp—Ù2žš#Ä”șuÊÆĘx5ϧˆ. Čż^zÖNJö”Œ.ƒæAÖčtŃgvv3WîŽ‚> ŸDÌ?:цńž?łFÆ5#’”ÌÇĄIo e–k~ËLčû:\0>^y汩O™Ę+7Ą§‚ËM’=Ƈ©­«ôűôA4’)Œ±cȚ°e¶.È€Ïźh»ÒÓìxŽr[źj!MŻyŒÎ&q2łaК«pÊÓ:ËUÎpN1[:VŠśÇșP q9ĆmĆàÛ)$iöf 9kŁ•<ÈâăXу9'#?Ę€ș[‹‰‘lś•©<Ÿ§Ò»;ß ÁkaæÂ$ è͚—F°òcŽls€bŸ+[‹Úv“ȘMnàZ•,ž,~\țuzŚÂÒqöÉv°òÌç5ÛZÚÜŹŒdæ6ç"łźĂGxV&ÈÇ Ô„qÆIîT”đ–“mjŚ4Č8a#ńúV/Štx °†ăNˆ"ŽĄk€Œ—ËÓ$nźxê@Łç̏ 8íš$Ì}6ÎyŠIą9r+ŠGun±6<Ä=kȚgłÔÀ€á[ȘšÓ7{/p8ÈÇ­h3ÄJöóą6­5ŽB$RŁ zÖf·8»`— †tŐ]:ŐĄe—sŠÓIjLŻč”ŁNŚl©"1\óí\ÿÄ)tčőȚäÍmÄGŸÆ­éšƒ[ë[U>V;p}ëiü'ŠOxn%”Cžćf˜Ż©âì€qIôëúφô«sCc ©88Eá­=æÎ!‘…ąćšÜò§ÒèkÙŽ]+K·„’Î ‘†@[ž,RŽ…c~8@(ș°žŽÇ€țé§ yOHŰțëz„ąXcP€Ž•b=5MŸ™”2Ț‚“Ò<lźîĂ!ÿ€šéWÊkY€=ÌfœŽÂË^[ȄA@ź«QŠ ­=š›ëcæï±L[çÓ:éwL»…Ž€zâ»kę"hőX)ÇjÌșż—NRźH^ÙïSÌ,đ˜~üNżQŠh™WąțŠ€żÔfŸœáEUQœáA5CRk©anа!GçW†©$€"G*”vžs“éVâAÇaĐJ—œÊŽ2„$ùX»Ÿp=üjÜp„ś`ì9?֘0:œ{/űÔRȚ”;#Àő"­4¶9'^­YnHòìl6I€W2§Ö t­žI5»§èw7™™<žÇvê~ƒ©ŠäٔíæRM˜ûжŹôéeŽkhÓï0€_ ê"¶!C(Æ —MŃnźŠxź^_-yŰ8К[“rf|·7úšöšʞżJ·§ÚÜÏ"ù’ÔôÉéW§ÒÚń€l€Ź‡ŽŸV>[’):Ž˜ÒĐô]*ȚÇLĂÄQć#—<ÔŚŚ"qĂ1Šóki”H˜ä·xêwm!‡\ÖR”™„@ƒPŠ=5Ą™y fi·[n€T“iìk‘‡ZŸ—äxÉÛZÖ7K#‡d*NJM·dÄâlÍ4†ì™Š.œ…Q›Ux„ۧ(O~Ő=ŐșÄêË(mĂòźcRûZHÁČç‚-v!jwZV€ș5癎3(ù‡;iÚűŽ»í–.ȘP;Śc%őòGĂÚș­cĂRh:|w"怄ÿŹZŃ;Ć«l5Ąƒ ÊX–$­[šőĆșU<}œÍ•Ä/íT5{ʶ«Qàő„bùœìÛč§o«5މpš€šĆAy«Ăype`ĄsŸ}ĆˆăàÒe_ÏÙ zŚÈ­§çyȅăo­_Žœq?”>lđ3T|9 ]êS4wyE{քÚ%ƅëFÓ.Gè”WDKRŐà{kVĘÎ+&–Ù€ÛšÓ‚8ïą’ eÁÁÍeGašÛ3 }Г€qQ%%Ș]ĘxD?g‚vJł~֐”ŒŽ|§(5™„[GŠ] f$ާ5ĄŹA/ˆĐ}Ž1ˆÇ&ïgȘ!êÎpʱ!mĂËÏJ‚MVÙH F}j­Ö›shÙ›àŻĄš‡n|Ż;æȚČŃ|LŽŐ”7,/až“ËcźÚêíklöÓŠĐßv™áM2Ù튆p|â~SíVüCdˆc…GÌŒękXĘÆè›Ęèe-úÆÎPüÂźè.·ÊÎr•Î]ÄĐÌ['‘Ò‹-FkÜrĘ (-nÆŃÖèKonC*.Ő8óšŐ†(ăԜÈr’r=Ș == äjŁr“’âŽ5(aYŰŽ'8¶IÚć7rGhĄȚŽCÚ°îŁY%Û‘ڔmo,/l%@ĂpîkŸÊ.°’‡ő§-Q)'裄ê·0Î ‘çNM^șdóDËprÂč{}êß^@ӓnčï]UìfÔì,Ysš…"ăîjrzæȘ%ԉ șŠž+ gdșDÇĆb_^ˆuŁàbșÛÁ· ©CŠ["æúœȘùrêćÔac?1Šê—0.ŁŃ>ćÎĆ-€°CȘĘ Gî߁ZđÀ©öžżxŒ ÈÍRŐf]úÇ4éŒSƒšÚY Žâ„}ìtźkY’Pɏ,Ž+cĂ ł0†äçoF4-Áír-cm\ÉùQż:í-"‹Rł*§˜ĘUçŃâ‹çˆæł_Rm5ÛÈ{p†ŹJIšz~èLnsƒȚ›c"Ă « Ÿèšmožæ1,ù O5Éź”ŁíNàw©6ŠcmàStÊx È©ŁÒ>ÙrKń}ęj”Ór›«pj[J±ÊàŁ3JÄJ:™șÓŻœbʀŒfŸ •ÍĆŁ­Œ«…\œš·šè©p­4LXžO5–[GȚ\da…5Ë}DÚZ‘i·sÇ;Ç7U8Îk¶·ž„ÚĆ&ì•ê+‰Ž%îrrǓ[p„„mYrŸ•Jś3š?\žÒíôë‹Û—È"4Xډê77…ÓI+`tQŰ ö3á/űI.dțLcŒ65‡©x*ŚL”y;ÔčÍ&ùUÚd’<ÒȚĆŠ`ϰâŹYNĆP€q€2œĂA:chí"Y”•'`ÍcȚÙĂöő†8c {í_D4č2[ ą’Cî8v öæ/25LŽŰĆzö”#¶*ű9éĆfÁ[…z·4&?džçq ^«ˆüŰÁ>üRKáÆČPóJŻß ]†Ą§Iž1pqÜSEÛc‡éւęœvG/eqmhêÏläőÜèš„œíÌi ,c°Ź OGKkPvóíQivòÚ șL€jQ•ÉŒWDz›*Ă"ï!”ÔŚÉœl T­/ ș°I‹àzÖĄč{ëXÔ(iĄœ•ŠZőȘ­Čù@ՙeŠfîœŐûۓˆ$!€àUÈ„Yclp Ô”©q•ŽB}‰vËŽ`rjŽÍnQ¶(Ü5zçN™ź„däőNÛKdwiOáRÍ.-„àƒ5hŒĄĐ §Ž)‘ŰFča&=iÀÁ ‘nő€üˆ”nFNùvï8'jè ”· ·ÏŽŠ±Ł¶/;ò· ÚG2ÉçN@;iÓMČ6)\”șmò-ž1ÖŹê·—Òi±Ewó!ÚzÔÄi„±\n} UÔo.Zê$“@íOU{±s#°pănzàUû‹$Œ¶EÈšŁ˜O!RŰ«1łFĆcçjCR*Ge“+íÉ»<›ÍæŠ5 «ÓÇ^I!f§Yj—0[ÉGƒÚœd“ł ź©,.îŸ+©ìzTÒ]OšF$òG­sČÉ?ÚXÈ€dóĆli»È-»ä+9JËBXÉ`žĘÈÏzŃŽÔe–Őc‘1Žu=êìŒ&òšéŠŠö5 6.늩]+ĄtčJìŒÄdàVƋuö&8F៕Ï]\3°T\ł›ĄčcYÁÚölhêöĐMșĐïgäŻ^ióĂ8ÓÄ2E‰1ĐT:d Ąç‘ÔƒW”mv5R6€Ê:ÖUa'fքœL;ž+űÒ/•łÎ{WI-©Œ™€lŹ­$Őæ’â>Żj֎w”]݃Ž”RæŠ­ŃŽ:Ω„°gf\x„‚ËO–ŰCpHIïMń>­<ì#”ŒŒőźb«‡čĂ+jÖ)ź„ő„áíEÆâŽœjбȘîX€f;Ž«Ű`Žòțó·AV—I-’qœńÓĐÖüÎÖŹŹG„Ú€nű$€œő隔=ČÁ,qDÄä皇G·óo$čAœJÜŐ4yÉnAiᇄBÜM3›Ô/e‚郫.ŃÁśŠ6©uqe$Ž UjćŰk˜ŸÌN@êk>=òZFmüjź]ܝŠZLșeț«æ\‘æ…ƒë[2…ČI™rTŽ+ΊłžÏW|±CĘ]Ž2K5„k1ùeÀßO„…5fUżŽˆ,S:í3šœeźĆżÙƒSÂóZ·6Öwz8ÓՃLW MyŸ§áęSD”JÌÏnHíEŹ4û›ZƝÍÔé3(Û‘Ò”­$1EóçjÎk+ę 7IWÌPóšŠȚŐíîÂș•4- ¶¶:Ž PÌŽ”ÌȚ[o»fƒÎkyÖYʕц?ŠĄș¶Q`$CûĆçI•‘‡=ÉM/ 6ÇŠ“JšÖî‡{ÔZ…Ì"È+ °#8Ș—@öhđŻ’űÎG‘v.ÏeLW~àM<Ú »TEzȰžV'™Üô­˜dž‹oʐàÔ;§u±œïsZÎòÊŰ­Œ§ćn >”ÎűŠ;k]Go•zŰčČ·ˆ$™ Ù­Wșł¶’àž„‘ß"©Ü‰ZćF°TČK‡|’?*źÈÊȘŰeŒô5§}$iŠ$`}ȚŐ5ÂŹú$șíĆU^涏{VȘˆÙr1ÇzMji-ŹZY`$7CŠĆѐĆpč8Ç"șÓ{ț›<3š=jÒRZ“É­:°Ÿ–Îć±Űç?JÔÖcX°±bÀfٰDì+čĆKš\[CcƒaéYÚÈۖÎèĄyȘ4–šJ{Öv„;H‘ÊȘTđG”\šú'"8ĐŽ«â XE07CB-ÚÆu’òMŹçźCŠO§Ę;dșcú$6țZń‘YTŒžÌÎ~æöHИ>v=…gKwvï•v–+€]Ć’.[ë“ÔÚàĘlƒÒŽ…kl‹\DᘌŒÖ}¶è/ÜHr™â»èđßمS‡Ç&Č5]­5,œŒőȘ¶— ĄËxȘ˜ćÇćVŽ›KĘRèꌕUć›”gĘÚÇhŁkÄs]…î/4«)&X7C!q^öŠr/j±-„ Gǜ;ûÖ[X4¶Â]§8ëZ:ęì2€r–ž&«I«G%†"*?ZrI¶…mią•.È\âŽ,î ŒßŒERŠô”ÙgCŠ™fWž,ÌqX5ÊŽ•G¶7™Xń#[ÜcĐÖÆ›z‘Ł’3Òł”IRæB!'”¶ęDÈ//–àykÏN+KKÒehÁ#ä#5‡kA+™A Œ]-ŽšđŒQȘ’‡Â„Yżx–VŒci '#TŃH.ŰFGfŽ5Kh. ż<‘Qé’[E+ćC€ŠčûČ”ĂR„äQ8úJ©Ł„ół€ lĘBí! )íNh‡Ùv°Â”`œÖRČ+Í{iöeàIŽ”ÊßĂqpëbŁœlK ș9‰1»Ö§Ò™mù«ò[ÆqnÏ`hŐđ€rivĄpsšŽK€m]ŐÎPô­›«C{ 2Ä äăœa}‘%Ô% pbë5R1Nɚ·k áÍorÉ­•5čzđìA™ùźbËKWlá·FÇ&·źtűÍ· ;±ëR֗fÎs]…­cŽe Æx5ytžgđúܗËőéUîà–îŃ#—$gLšâK;lÏò€=(M “kc/ĂêÉ©čÈ*šê,2śû$`r§Țž« sw"Ź›qÉ5ÓŹ±y #“(È„bșăìúÜÙÚr3ĐÖȘjv—2ŒÍ‚1\ŰYnq*Ł15]Ó-c7ˆY¶œô§c)Ąòi·olČK~‹WàłčJ1ÌëŸá•© ÖùŒžŰكëXÒÎò_K‘•Őh‚;X”XËœ7üÎ>őKö˜ÍčŽF 팫M«m Œ °ë 6óTI  2žA5ĘôA9v6gđǙhf…‡MŐÍk“ ÍÓ Úș=.[èŠÙ™“òœöź{[”šÇÄ"9$ 0ÜïWșșES}̻Ȅ±ș+œ[ș.œÚĄȚòí)ÈŸ‘žp%ù—ŸJ­p—ș5Đx9…"„Őő*mȚÇ[cšœŒâʔŒgÖ”ZăÍÉè=닃P2Ÿ7ĄïŠÛÓŻàž]†@ҝûÊ6Emară” đ±1ù3ùUęNŃÂq’ŠčôDŠŒÒL#ź‡A©­łÀ ŸH”“fÏčś/Éژe]ûIÍHy ąáĄnÔÆč-òŒæŹù°`O­ZGtŸ;mÀŹ=ZȚK)ârééMĘZEŒ¶Ń|­ ç¶jź”ĄÛßZ<Џœ ‚+˜',źÎzdU»_Kf†ÒhۖTúÔ©.ŁiŠOákáktm[ćqÁ>”7‰ądžKƒ’čæąŠ- Üm äæ·íÚ NßːÀwȘé`æ9yŽÁwnłĄ< ֌:Œ°icò‡ Š„±Œû;Żî»Ż©Z±•d…‰?Ę“łșÊrBnŹ—vCrpj;{f@vr;ŠŰŠÆI­ĘEV”MŻ(çŠMjT^†|ńÆag ‚=e„à.@”üA/ö}‰ -ÎqvŚR3p€i?1'u©Ö[\ƒBò?Z‘Ú)ÊíùdïYÖ7­jOšœKö“öŸ1!«9œ‰rîjN"†Njőœ›ÍÜÀÊv苉d2Ÿ•»Vż‡„™ )—îššĆ6'f@LDÊŰă„sŃĘÍÉ[›„?VÔnÚ28öšô3‰‘ó·­)ë ZÈȚ7O/€1Oi'{3òäâąŒ8±vS‚S°ż»6k˜ŰÜnĆgxI™Ś"Ê_aÈ4뛶{uT;Xž”­qQŸù@Ă3YśŰ@ÛF[±©rMÚgQáï0éóòä`ZŻâžÊùîĄ<‚ŁœsšJL/hÏïä}+ȘÏ©[3ÊùpxȘNPćfwł<ÆüË%ë™‚Ç V¶‹`èç_”ôÍtŠ„VᚇVꊳ…UÓèEƒ”–ÆœŚZ[ óÆ?t{ŠŃ0B Ž ÉŁn:i;ˆŠVi0Ò¶7€ˆuiš3û”,íG†.eŒiZőŽáT֌-l¶û$PdÏ~ő› mG|”.öČ °sZ·ȚívœzW5Șűl™h‹lcƒŠlŸ![]F5‰~Vă”ŰÛêpÍhc’0ŻŚą1Č0‹p™ÀCáÖ·Ő–O=€ À5ÒZ]$MČ}Ą“ĄőȘÚÜËpʊÁá”Ö$’JŒžpbÆhvŁÍȘ:«ûË{›'Œą°ĆÒiöQäÊqÉŹÛERáT”őĘf5»· í8éTŽZ 1ł±bń!»‰îž.EW‹Lž;)&O›ŒńOÓŹ„‡O-pw9­Mȏ±śÇœ=šI_SFúœ­†ŸEǗ4„8æș{]ośŒĄBčŒóÄ֒Zk’ŽhUäV߅§Žçęa‡aÆ‡sD“Ôîôëˆæ„»š8c\Ÿż/›{ ‹€OjßČČțÏȘ<„źț2im,ìîéŐ[<ŒóIvŽÔâźt‡Óš)]Ÿś˜Ïă]&›cf$†C+sŒ‚zŠÚńŸՐ•éYv“Gc4"â,§Ę$•\ș’§tn UČ4$<Š„Ú ËpÙ}úS'žó X[śxÍIö„ž‰#^XńŠSD?yhj@ˆtőbAqĐÖŒ·Û”¶v ՕzÖ„žhNÙÁ xë[Sjpé¶Ó&vđjąč–ąWG hg‚Űùń•b9ĘÖ§Ńïă‹Te.Pž?JŚŚnŹŻ`Šks0t°ÿъȘpäTËĘSjöÎXž;ȟŁn“âe’űĂu·çŽș+ š„łX€ì1ÏzÌŚšȚs»_Bàì`Er„1°Rsš}ĘÄóÂä ŒëP$EsŰUY500+&ìÇ)j­>Ț'YÜŽEI§AeiŹù’±àä ńUôèÖç ’{f”“HUI“9ïZ(»Ü'êojWBâò@#«‘ż¶ó“ÆăéW.źÍ•©X˜•ÍŠĄ-ŐÎùNNxMśK èȚHä`Ç©âș;{Ű ŽUb3Ч>ž%.#a‘Îg3)čD`zô€ô%ùL7–ČFFyĆaĘÜ~ńŁbvȚŻ40Ă:ŽqYڐ…­ŒÛòț‚—3ŰQnö4m­ìJ~±”Û_łÎqòò+?N»ž@ÒÆ„=M5” /ïn TŁ}Í·FŸ›{-íŁ âș îÂÚłș‰GRkŸ†ȚÖÚ ÖòrzóÖ©Ü@IórOăU{5c”ÔRȚđ–‡ænŰźbíćȝ[–Ï«š5đEœO“Ò„Ô§·GY%Á>ôŚq3)ôșK'ÛAŒnÀéQÇ;ÜÄÜ=+SNÒ>ÍŸGû§KVôì­áÉ耕WíyiČ|2š”KęDFÔR[É łŠ–”čQV(ćJÊĘ,//dßn€ę;T’ăspGZêt]FÒÊÙY‡>Ő\©èɑŸl“À#ŽAÈ&Șk5ۅ*vÿ:ê.Ąó”J©Á9JËœháLœIÁ-G0úlșNőjăFWe)ˆúâ¶­ákžÄȚI+WnâYŹü„á±ÓÒĄEîÆ™‘mŁB"Y­„e=đkunR;0gĆTÒàeŽ–ᇠV ô©z†ÆìV6wPŽÈœkŸM,źìùaúUûșsó .8ÉȘÂđ5ä«#lO\ő«v#ÌÇŸč›Í0ąçfŹéđ,VŽŃ’íÜÖlș€Pê;†zšê-e‚M™Ô"’ZÜJNç5o `ä'Š*ìú," Æò=* mA!wV^Aă5Ąm™ûáòcŠ•©|ÚŒó=ł•đksM™ź`ù«?UHŻoÛìë\V퍱Ù遀#;sJÁ̋ˆăÄ]=kVY6.śË”©&¶ƒOù11šçcgżž;Û#5Ú‘Ą€‰ÇÌqW5KyŒź9Ćjézlp[źđ2yȘśŠé;FTŠŸ[FÄ­ŸŁÂŽŻ#Ÿ™F«4Àˆæ©Â XôÙo.-dȚ SNzY‡5ÄŚeŸìȘźișÛYG,E·yœïYŚș뇄sW-]ŸYV#qW}nkm rÀŻ{óĂ8È«ž)ŃaČŽYáëĐÖB‹›J6teŒ6O­âLÍn–ÈŒÇ+$Û&öw0téăˆ/š1Íh‹›k…“q@őŹfÊFAF'éT'3”ÂFàjÊ ”ȘŐjXŒŐÌql±ŽàjÍ]Bçymä–ëM:Müź…€ëÈ­+}"xąHČÆ­ m,t¶·čDyÈf^Ț•¶ä„è#ćzVkö]EŒ&3z»â[Ńm˜\`žŐ2·7™6lëtÍN)&dÛÈôȘž!Œ’Í Æ0Öłü?ŹXÇg’8óĄâVŠ}<ÎJŐŠá蛋F”QÜĄgç=(—P]%ʉò»ZvŸÀ‹8êioí­’ń ]kšÒ”˜Ô\]™Ï1DčÉǜ©«Ś7/6$ŒlÛÆk›\MnbZöW°ù;&'=É­îuEÙÜŽŚÆòŃiùläâšë*Č@^3ÂóR[ŰYȚÏ1 ÄöŐÙôŁžä§ÊzRŠîô8äœ1¶’"Àő4—zcG’Ńä Č<ŽttU^pAï[kz0"Š.37KčŒ‚% üčÛ[šEÏÚfy&‘‡"™ou 2n•BńéZ)šŰ<;•ÔnSčÔf„/“Ÿ Wœ֗AÔeșó–æ-‰ŰšlWIs•mʕ»‰śFźš|Í"lŻbürZÜ m|ÍÄt5Âß[œ„Ô©ć“‚+ Òíą±žžiæȚ9#ŰPچ–fiD™çÚ„ïsDbév“Ë 0ÀÊÇźk~ÏFԖèoUXŰä՛}^ÁG™nêŰëTî|^ûżt7p1JìJ*ć-~Öț+ą_h‹ëX–šMȚŁ)–ۆÇI­KUk€ęđm§­Co­Ùh‰Qs»’)ŠÍ5°hš€!æƒć^N UÖl&œ"Uùd„u6^"țŚÚb_•‡5­M-­ÖLD&zŃŻbävvś–‘ÇæĂÚ»:G—N° Æ9źwMy.ìÖ_+*:ۇt¶e˜” â©sąw35 2XgG(È«–ÚpšÜÈś “éI,AĄ`%Ë95‡vf”·hÖfšNăMn.V+ù-ƒç­í>k[xȚæ Ç8ʊ󾡝ŻZ_?æÏzô xçČònYXŻ Đ„f7vńI՘( )ÚžÍqBLËÚșĘ&8šÇśMÇzĄšŰés8If"BœW4dŁvaiú•ÌVÂ$LšïQ>ŁqÆă {Ś[iąéŃCœeÜ1ÜÒÉ•ù•=y©rЧ€—W»7ʰ!Œt­ÊòKŠčv9#ĆtŸv• &}qPMŻYDżșT©w yŒ’ÍÎIvôČÿłź^b祭CŻGŽ>ÀG֘5·ûB˜”ïFćrćđæ ń)Œ6áÓ&ȘĘűGTș¶8uY=3]*ëȅÉQÏAYòxŠé™ü„ćFMV€òŁ‹okÛ #9ûÛșŚEę‹©EŠŹm·!@<Ő)üuqÁIcäŐ2űŠê{fDÇđàT&șă%đĘÙ!žD\ț5!Đ& Ač>‚łbŚoîîDeö.zúVŒÚ„œČá§.ÿZz”Ł2ßĂb~ÖÙ'9û«Ž? Ü1í’iërNŹ-á;GVȘ7NÙcŒŃ­‡§CVßHÓZÛk>ăߚlZVŸnûĐńš©ŠZÌa’FÜS”WK‡yšÉÁőéBOpNçi§\C,Ë.ò>•bńŹ•‰xÆń\Ț—šÇ„$Œäœ^ŽčKžšI›ærHôvș&ZkbÁžĆˆ+üȘ„·șxfR>†ȘKv-î*çÒčĘf+ƒ!˜)ș`Ô8‰1úŒđ›Č"PA­]~(áXvȘ+‘ "±,pȚôûxy2Z“۶ʎču äá”bȘMŻ!ÂXRD±¶ĐŐĘï”y4•ú†œMűŒF±ŸšÙ«ÏŻ[H5ž_Âču…„‹(d”ŐäÓź#|Ędt©ĄÜÚkòîdMĄ;Tö’Ő۶HäV2D‘€žg˟Zèa[h­K!#œUsĆègȘՙĄ•žYû5źòîć”tÍ>HÖO˜ 㚚 dòÊăo=+ž§2Av_đæ‘m&АÈÇÍÍuÚĘŒÜ Ș1\†”òArŚ aș Ś’êq<ž7jȚ‹\șŽúwŸ$‡H›ìÖÿ3‘ÎJf™rÚ”áyہŽ™Źëï K%ܗûđ84Ę;í^Ɗ>Wà:T·šD8ÜÛû6¶€à{ÖțDŠŠžŽkBú;˜ÀIjSž‰. f0+CŠ6mŽW‚d±čFCòśȘú߉&‘^Ț»·–ȘŚî±7űFk›]Ad’a'ĘÇ„Ć1Iud {,ÙęÖÀź§D2yR4Ì Êę+ΟÜRáŃNìŚaĄß)…ÚI6‚»E=ŒȚĆçŚmïä’]xŹĘfG¶·‰íœ§'Ÿ©lŠo6> H«VsC4‰ 1`ăóÍ7š8étmxoV’.©Ê“Ț«”îۗŽL«ƒZZ6‚4xî/Xăć;sL‹FMZ & ‡EJz’­rŹ—ÍćĐ[‚hČđôêÏžîÎj ­Ô–wȚ§ŒśŻ@Óç”}' zœj·ùN*[}9RBCH8úŚ!ow7ö©U-·wC]ö„=ŒŚdàOș eC„A;”éPœy468I\ŠțêY-öìè85‘ulg¶ »·zߌHm&\üёXłKù‘©À&‘»#ÓŻg±QćI‚+u”§Ô,™.p[W,cVÏcVDrFëó}ăŒSč.(îü3Ș[ę˜A,ŰeàéV”œIŽŰ­=Zž«xăŽŸŽIŽÖź\ê+9"#ž(»#’îæŽW4ç%óÏ4_Ź·°n¶ù›žźdyŸnęÇé]ˆíž¶üàśŹäÛd=ÌÈ4 »ŒžùXv­«+ìÄ̊Y‡ZÖIr€ÆG5C#îiTŐ-V‚sl·„jțmŚ—ž^áÈ4ĘgłŹÒ°sê P”‚ŚíŒŸfÖúÓu{wdÀvp: ŐkËf;»ê\KčV>‚°ȘÖó;\5ÉAÏÖ€°ÎÓ~aó'œC±`ĄxÍG&ą“èM4…‰Dè}jÜŸŒ;ÖĖqŃûČŁƒXÒ©Ú)_u'LšÁngGš±ž6û€LÖ„Â™Joäő—ö«Šu%W±ő©cÒ.cč%[ćkÈŽÖÇ]±dÇN„Ô7âCäò_‚ssĘÙÌŻŒ„}kZÊêYvN+Ži·q+'©™©éŠ·›¶óžžŽXë1ZHmn Èn‡­›šŁžćdó:f±oŹŁyËĄćjbŹ^„W!f•Ú0O§pA.íËă©ȘÏ#éž@ȘđJK3àQr%œN·ExÚÏÉję«CpȘ$ž•RÓSŠȚdÙÎ+rc! rÉ»žȘè+jhc·ÒBÛ\ÜVòI1x—*zšžîFč\–ÚŁ”K§ Œ »”.8ČR”§ŠÆnśČ*ê°G #ceûóF—rò2†à)æ–Kf–FBᘜśšm`•fxq[2žȘÆĆÀ‚âí&ËŒŐMrê;HăI'§J­ Œ¶·Â@IçŸzÚÖŹÒöÒ6Ûócœ;è¶g 4&êpă…ô§:”ČákÍj–Ê #ŽÔ[iAÜ2qÖČÜĐÀIÜFzÔšȚTëhXžűźąÓLłw.Çő«s.Ÿit‰qžZ‹\OSÒ6Ču”&éH抚;›Û‘æ| Š]CT‰Bę˜}«:ăVžL1Sg IĘűző›€©ÁĆjÛÇ$ZhÆžišwˆtŽ­žj–­©ÏÁQ”wì6›EĂ ‚-Ê2i°_ÜsY6úŁ2˜Ű{æŻÛÆfˆđy©”n„ĄÜÚ¶șó‘€rbÉ`2Üç5ÎGvÖÈqZ¶śÉđj5‹ĐM4ÈćŚ<èčă©ć],J›ŠTw–†;ćă-ŚĐTöZ|0ŒŠITÈĘêÔSEèΆaÆb•\ŐXźÒßJ‰yÈüjexRèJ1žàĂc;ژrqž‘ô­șŠŽĂŻn Ô#Œ ău(€˜*äJĘŽ_ČÌË/ÿźȘêą[{…ÆV'őE+‰êŽ:;IŠ“!p ź‚ÂX­Žù-§IŐMoŰxsûP,€óŠ/m-tĘA#șP0ÆȘÆs2)”Æ8îi֎ŐŃsƒž+CTX–B±:˜XqYVM5„ÈGOܱÎìTĄß±ŚMź›9mœÀÁśŹÍV›KœtB^6è3Ò”ì펫űZ$ 0sŠćf†};V+è‹‘„y,“j-uކÎk{JńD0Y4.ŸŒbš\•zăO_Č$€ćÊäŚ7%Ń”žËŻ+ҕŒÚčÚiŃLҒÇE^Šô$MĄ8'æŹm;^”žł [kŽ”qïĄ[a|ó֚ŠKCXż€é¶·—M<ŒN:c”jMc}«Ï„EĄÆ±[‰.d[ąŠÓ™!ČęERBl’ k{2[­TtkóńŒæŹErƒ!È#Tgș3(~J§kCoŠKp_ű@é\òßi·ï Fž+7ÍTŽEnî]ŠûfȘj—«©Ä‹Ulö|€©àőĆ>eqrèvmj%pÀ*ćć«o@*>ê;‹Aù©“ȚH-YÌÁTOC—ÖöąYčŹDșŰBÇȚŸšŠŸlíĘĐŚHú•ÍŒOn?x@ Š‹ÙCOŒòŠŽăä&”őh!Žh˜Ür>”–tÉíČ$9QÒ”ŽłÂ" ŁŠRj1ԉw{íg*>íT’òG8"¶Í©žà™űTőïUg™‚œă„Ji€Ű&ș•ôiJê@žÖĆĘŽ·đÉ2.@=j m$BźćÀfé[șT‹mföò°=H>”qł4æČ9[h'Y€‘x©ó۞Ćn} OZçIk‰(ŽOœLôDó·FâDȚ!…>Î;›X<ìpOJі Č€<àŒVʚc›>ÈZJœ€źKf ÜSÏh&ˆæB8ĆbOŁx„~ûÉp‡»”ÛÛ*A±Qrî+­[Kf€Șđ*\œ}ÒS?ÿÙmediascanner2-0.115/test/media/image2.jpg000066400000000000000000001115121436755250000201470ustar00rootroot00000000000000ÿŰÿàJFIFÿÛC    $.' ",#(7),01444'9=82<.342ÿÛC  2!!22222222222222222222222222222222222222222222222222ÿá@±ExifII* †Œ›Ł(2«i‡ż%ˆ#û#CanonCanon EOS 450DHH2013:01:04 09:52:27š‚9‚A"ˆ'ˆȐ0221I]‘’ q’y’ ’ ’ ’‰|’t‘†’ű!’03‘’03’’03 0100  °   Ę#ą#ą#ą€€€€È 2013:01:04 09:52:272013:01:04 09:52:27  /qy"ĆÔ  ì P˘  v€  &0ƒ“t•@€–ä—ô˜ô™5ü ĐȘìŽĐàű@Ë@° @¶ @ Œ @üÜ @ Ű!^ÿÿÿÿ07€,ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿo·"!DÀàô”ÜűŸűÿÿÿÿÿÿÿÿCanon EOS 450DFirmware Version 1.1.0ȘȘv?u@PŸ”’ŒŒć»»ÿÿÿ #”+Ș3œ:KÌÌÿP đđ(S07‘u’1.1.061Žddh dedûžȚȚȚȚȚȚȚȚȚȚȚȚ[ŠæPș%•VEF-S18-55mm f/3.5-5.6 IS Ÿp` ° ° sssąÈąsss™™™iÇi™™™™û^ę^ęąągAżț[„ęAżț”ÿÿÿ0ÿÿÿÿŒÿÿEF-S18-55mm f/3.5-5.6 ISK0336838Ô 8,8ÿÿP ŽK"Ű<  Ï6 œ <x<·ïîùżŒ,&çć±ÿÿś” Üêé$qtt8„ ą 0òâ Û V Ïèè${[YŚxX U Œ tŻŒ tŻ ęuŒœP ÒXe ;pÈhhn € Ô ÀŒœE©  Š-2.ÿÿ}ÿÿ}ÿÿ}ÿÿ}ÿÿ}ÿÿ}ÿÿ}ÿÿ}ÿÿ}~țdä”*–țoÄ'Ïțˆyl íț—RX'ÿŽpBÿÂïàdÿŐÊP•ÿó›\Ìÿhh@7Ű9f Ź x™Ț€ ŁÁÁž ŐóŁđ Fvi` ô"#0(/*/&%'! 0-2C,+,,)%')&"%>EGF $)'+,$(/3 =7D:;4E:;01/I65H`nbkUUWGC=994~w~Škfig_SRS?LDCŹ»čŻGJMPW`X__NTY:!=3<35-;3-%%&687GZeX`LII:6/.-&~v}Ąc^]\SGGF4>4/­»čźEGIJOTLPN??B29/4,+$.%%&20?JPFJ:87,'# qhj‚LFGD=30/")#•—‡-./048132'(&   W`51x?*7OG7\Z:/%W0 'WH»0îBˆ+A ÈMˆŠE‘O0-ł€šł }Ò"őÿˆ.Ęÿ { (6ż3߀ÿÿŁâÚÔŽ~ń  ń  c’ÿŚ;'€/Anu+H S‚#Eš#Č#ș# Ò#ăQÄ ™ Ą*Ô0qî202013:01:04R980100I$Q$(Y$PHHÿŰÿÄą  }!1AQa"q2‘Ą#B±ÁRŃđ$3br‚ %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™šąŁ€„Чš©ȘČłŽ”¶·žčșÂĂÄĆÆÇÈÉÊÒÓÔŐÖŚŰÙÚáâăäćæçèéêńòóôőöśűùúw!1AQaq"2B‘Ą±Á #3RđbrŃ $4á%ń&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰Š’“”•–—˜™šąŁ€„Чš©ȘČłŽ”¶·žčșÂĂÄĆÆÇÈÉÊÒÓÔŐÖŚŰÙÚâăäćæçèéêòóôőöśűùúÿۄ        ÿÀx !ÿÚ ?üÙŃŸxŚÄWVș7†5›«›MŸ|)nÁâƜnSÈÎ _±űńRł[«ëw͜KŻƒÏ±B W+ßđÏÿ@r|!Ź€€łæ!ò€’yă†Cÿ_QV_ölűĄő­”žń^]+Œ4yBcyUêÛw.qÓ"ŽGŰWD:ìőń#Ćvæ x;YŐ" ŹÍi“ha•Üùr9æ”żá“>1žŸ%ÿÀSG# ˆfOŠŁŻ€ŒIÿ€ŠűfOŠßô!x“ÿMŒWę™~*È2žń# ‘‘lO#­T“ö|űäV’x?Z[©Xą@bـ,@\ä?AG#ìEżűf_Šżô!x“ÿM'ü37Ć_úŒIÿ€ŠŽF25‚ž<Òf1j^Śmä%Ÿهïî/ÔçÔńYz‡ĂżéZܚ>ŁĄjvúȘ­g$ %ÉÆȚ§9Ç\ŃÊÆZżűMă]*7Uđ§ˆ,bïșȒ!\°{ôŹ[êWS,Vös;łˆÇn=zsR6š?U|uâę3ÀőÿèńGs«YDȘĐ]#$ćȚmšdFÚûÊXíÀ"žœGLŐŒkáOĆđŸK“Ä2Ÿ°Ö—·ê!Ìl°˜ÎLÀ\+čșèFÒŐÖĘô!łçxąmÄśZež„6łg\Û^jć w*ŽKƒŚćȘö"ێMzÄ_Ú%~.űfkËÔ}^ŃąŽ»°{\à\,òŹ‚3’È9bbĘÚĘs”TȚÉąNŻögńźłàßűS_Öu=5ô_&š·Êm6ފÌIpÍœ@Âă.>ÏđÄ[Oˆ>‹YŃà»·ł™È‡íJF\ŠIPs‘ž«†è”e­-dt“\yŠæ«ęź:z±\éŸ[m+Y·6ą3ÊăđąäțÇp+…ű±â<'ąÛ^jQê §$$ŚP2ˆ—)AÉ`ûˆcćÉ …ÈʄyWÁŸ[xöïڎ„<–70JÖúæYä•\Ï'Dæ%ž„Ï\fžï^5[/‰w·‹€ÚÙÜÛÙ5­ĆʅÄ_kû@*đÌȘT9*Ń“‘śylȚĂ^FĆßí3âM7GIá°Ó5{”‰Ą[C"š»'ÎP¶í©nźIćˆăŸoűâ+ÿ”˜ÖS۝ȚíîąuvžáȚDp6œ_/ž:ÈH4€ÛFϋ|eâ›AȚè'&+ˆűtÈ ŒśSžWĄŻ”‡z߁&ÖôËInŹcč†é€”0$Æì6ÜÄ€đ—8VmŒ!ùÛăm΁ŹĘ&”ŠêpĘ뚅Ăć„ÛÉ lžV‰~BÜïÆÁ› ÀÍùŽ?Śőę~Űߎ&‹©ű‹Bđę»űy”+[]bÚòî+I‚xđä…+» äá~^XV/Ă?€úŒ–Ąâ[;­cÂ7Č,–Ö¶RÇ6öí3ÈńÄĆćò·~lĄŽ7kWč6Ÿç„~Ò_áđN±iyŁézŹvÒŽ’_Lî’|ĆÉ%Bą‚˜Ær*6sç>0žÓąœż‡ÁÓ]Ęé—“Ź’ĘmòŐÆ,"0ˆi-ÇC”p/3$€ÛYëz§‚ü7á·ń-¶à‹xöM«Ëg&ƖGȖć[sšÁp O5ő—À†±|/Óçÿ„gÄSx“AŐH»Yć1óJ€dR€«+9#č~6„« Xöë<°QòőÆkL_ą6T0–h„bęšĘ€sŒŽfÉŠ!î3EŚ…ôĘ{JŸNńŹÖ7 Č[yĐ28Îyßê(bZ3…ńßÁ[+Ę>êoÛAq|öd#čv4,Ź P A°!qÆAđż‹-5y&ż_ê:uĆŹq‹+ŃćO 7‘±Î\d}ń»-Aç™kK·1Ž èzoˆŒ=ąűșăG’FÙ&}Lj‘Ä‘IșĂÆÀs"Țr” á—äő'D·Ń|ńżì‡⏠›vœÓŻŃc§ê-ć-ŚÛ#Uœ˜otVH$mKìm»žȚŰJ›-4~–ű“áÎ°,4Ëev—QH€‘ÔƒÁȇ=är ČđSè–1YhÖYYÄŽȚÚ1&NN$ŸÆșœLîdxŸÀx—M›Oń6—őœȘUă™3Æ1Áê'‘ÈŻ>-~ËÿîeŐüÿjŃî G$7–Ë3ZÏ$ƒ•ă€Ü'Ìą+—ü!źMámĂBđU’xöúâȚc;èpÜZꝶMĂ|Ń\IŽ}ÏȚçê߅ÿìŒá‹h4ۍżM4łù 3 č €“śBšà| æłŽöÙùΝN1ï1êjŹ+Œ:ƒzÔj‘ÛƒE‰čjg”€8Ï9­;}"őˆ)qëHf¶‰vfT”ê@é[ÖȚ•ÀÏÍïLjâß|5ŃŸTl~òÉ ÈŻÈ?0#=»^yâكDÖ”+‹íbÉ/'˜†‘Łä čE=2Cőă ËbîËIđT‚țń/ˆtćíÍÂȚĄ>æád“ÁÇá\„țńτŻï ÚN‘âHg”°šÂO±Lžă9 +gźC˜±ÀŠžčK?ۇIŽ{ŸűoĆÚ$Q‚ÎçMkÔP?ˆ”Ą˜Ç98ÀëŠäüiđëáĆMkNŐș]ê±iò@ÂâÉ.%…žo3rčxÊïR€«+NÔ%‰Ę»Țì|€CÀj€í\îŻCHŽÂăᕜö2±ëžÖ=ï·ìĘq“È©æc?țmâ¶7F~™æŹ'Ă‹wŐœHšwDòłRÓÁËÛ2Èá[6ș$VêoĄ9éűQ`/ˆ­aÀ`{ç„]¶ŽŰŒŁö©4Reć»KŽ)%ž…‡îÙ3ߚ”ŠčĘÀdúç “ZÍÂ"ëÖȘĂč™â?ZűkH–úòßQș†0O‘ag%̍Á8 “ÛéӞkÎöƒÒnî”8ì4«»ę+NŒ6wź-™eÇÙąœțâP‡/#’váC3mU)$Rò>nń'íïqá/ĘY^xlˆcÇfmĆBŹ›¶ìŁcc(ÚÀGÌGÔ^ńä:đn‘ź(·˜^ÛŹ„âaĘ|ŹÀ`‚1žăÉÆkjošM#žNÊìèŸßSˍ@8$œŐÈőšą}Éo°èA5łąßS/l—A·:éșB}GZË%Ăf9jŁFÊÄJ¶·%KËŽ`EŃ v+ÿŚ«_ÚśOśäSÇaŠ™aƱăUs y„țŚÜ•°x©t öăŸȚźCLYÛs֑źărIŸLŃìšȘ™Răc–òdt <T.ŃżŃîû‘G čÓ'ۘś)Ÿ\«=­öÒVőséƒG-Šæ3§Rlč'è*0.ăńóu»Őp;"”‰üëÍ}uŸC“\WÄœGĆVZLűX Ś˜źgr¶êÈۖ$|$ŹŻŽìf\Œóš–ŸDZ~gćTśCQžhnźä”‰fŸ‘•ŐJ©AÅ;ʑ†íŒœœe_‰>!ńv‘ö«È °đŸ™§G†ÌÆŽÈ7F0wg îPc%±Ă7ÏŠäŐK”ú?F[n~•`ëgš|„z{nq‘ |ïÆêëŰë(Ș Eń çpÏÖ€úùlčúÒl,+xÖúTcƏîäOÌRcHWńD‰˜cL,}ÜČâŽQ\lȚ/dSŒ“úUă™P ț9„ËqܓțYž}ê+lûÈÄț.(”s2ëâóć'Ó''źż‹gàHț”Jš*äRxòéùd/ŸsY7ț"žńśÜÊÌ{dôúU($ÌüŠ‹^–Hă7-ĈžXAÇ 7ä†ÉÎ{ăpûWöXŐ.4ßNŚ/š–\Ź0Č,1eŠÉê§%±“‚Iț,Ÿ+ۚ:«%Ê{ę·‰„1/ń皳ÿ \ŠvaïÏóŻAËS‘Ä©?‰îr6ÚŁĆR™v»ÇŸî–­oۋÓÄš§=Áț•:k!ÇÍĂ}jìŁż¶”ź7ƒïšÏčń0”“WcĐ#dț>ŸDȘą”Lűź]ù›„ƒtüjퟔńæ öÎJÆ5Ż{šNŁ'ńx*%*ę -ÒšMâl7É&à1Éïüëą ™6• űˆ?/ÆșjÔl;]j"pû§#ÿ­HuHsó1$ś )6ƏÎÙYËqŐâ( ÈźŒŹBǍÆBŁ– È$1?.Ż­|ăë-_ìû­YőœJçÊX,­@ oÓ·€Ÿ"†`zŁ'pŻ>SźąŸ‡±XëË Ż‘& 㓌T—šÛyż<ŸdŒyŰ?­tó^F¶E;čó™Đcčą8Ą# ]‰ă$óZ{v•…ìúČ_ČÆK‘•‡pÀÔńßM§ ÿhwFyéÁæł–&êͧÔĆԂ¶SL,8^čpĂëœÆ„ičuÈ2©aŰűì4Û@ßÙșÔƗ)"pr ĂûònÿMü đnà}+ûBțȚâ}Vő‘äź8@§9#<öùùèŠzvG§>©g–kY' `âWEÏĐcŠsSÇ©8MŃž!N9üÈź”?æ0Ô°úúÁî,I=z|>#·æFpÄtnŐe-t3äńz™ÇÙæ™ˆ<&TJÇÔŒg,r8 É'æ99ÏóšoŠĆš„«3tMcûJꥻ’dBü/ ț?àzVÒèZ{ł «ë§oe·OLśëXT­*nŃDżx©—gó4W3 vó@<{cëZVșœĘŹ`Ç;ܕ<(<ă=sŚ?7UËâDžèjĂâNæÚY!Ô.š,€Ép€sŽ„ăƒÿÖȘöŸ-e1ÚN@SŽNwc°šëڝ.WubnΖÇ[MJF[ˆív‰c$Š}zàvâ€wŠ\^òśäŠè.Àxû­Áü ©U\_/üqŸŠͧM1ĘA őß7ĘÆ…$*€€$.öwÖòʒĄ°}+ù}kłëÚKúûĆȊvwÓŠËQæ¶AÆàp;qŠÉńΜ/‡È#[Xùې­ïŽß”f\j‘Ą+ąHčÊFJ“êIă”bŻ'žȚÄöq\K)čh!ŠVëć’ÀƒŃ°;ŸÖș 8Ï/Ù$ æ1wwáN03Ç1íŠç«V;†ȚHu1ÊąßhŒ„ásô_LVö©uk,qĆt’7ÆBŒČöʞ•­ §îœD1€? ńùURśŁĄ-/ă­&æÉ#ŽEDw ,ˆNĆÇ\ őăß„dĆ­ZG$Ż ä–ć‰ÿVä‚LáFzś©„*FśÔmö*jwrÌżjŠìÜĂrű<}}yĆxÆOj›*âÜI§:«Éœàáˆ=;x9«sŒv”)êő<ĂĂȚ'žÓîRY.Š!â0O8è3ÙEz–ăy\Çu(Žx`€ƒâą=Q€ŚSĐ4ŻC4&%·Ł)Ž” »}ۚ»&ż4ŃÇ+†1'˙vŽOčÁíÇ^•ÍŹevĆea'ń3jÖÒÉșc.ńśôŰ>Șö©\OnS)ŒŸ›s}qíțzÖu]ąî  ­Ćœl$Ž"ű81æăßÎŻĆȘèáâKûJÄ ŽBŹxÏ!Č{tŻ*sȘöa{î-țș"ĂYÁQ( âRĄœ O?C\ö±ăë]&ŃĐÛ©icÆÓÀëŽNŐT(:źÉŠO±ÎGźBW‚)žĂîĄŒ śÁç9ąhoìŠ s1W99сàtśŻeZé6: ?ĂŚŽšZÒê'ŒmܒcÔÏcÏ#ĄïFĄö™æ„ǘöí&ÂÍ"șÇÍÇăŠÄFSp°XÉ}MŁ+nRYfVÜĄ[h=yŸÎžżˆș•ő€·QßâO28€Hă-ˆ9قÙùyƒĐ“ĐŚ_:‹BQ»5ŒâTŐ<7fóìu‰äR€’ào'œpGaŽȚŐ”OișuÊÁä2^>pBținqœÏüG”Ő^^„r\¶5K‹0ÆȚppn=ĆpŸŽčŒE§Ç>‰FN%•AÎòŻn•ŁŽ”[Š*Ìń{ ˆ­Ü=Ä 9ì ~cœt¶Ÿ*‚28H ÂìúńúVJ|ș›5s€Ó|R\”‘‘Śš=Ç^GŠ:Šê[ÄWW–è·ŹæĘđŽ@n9ëț*RwêCĐ”€M$Aäč’-©†(Üț=xy_`]Î]ő[ÛÙáKˉ FBțĄ–HÀÉ4łŒgo|ŽÜçôśŻVK•Z›—.n%†1ö$coüQ—?0=yŸŐźŸ'x"Ž]9K*à‰$U‡„a(s%™[§ńÍđ+eyłR3Éîz}F֙ŻxȚ++IˆpbżÍË~aÆÜcŻNœEe7iŽJˆ?lűˆ,…”Ž6ś(ï.oÆáśGr=ë#âö—q3ù[cŸI<–üˆŃlLœęń&ïĂĆi+§˜â”ș2ü ©ÏOóƖȚaăhĂ.02G~ŸŻźk—Śő/łűŽ{žńt‹6ő {g%G¶îßęzÙG©GĄx›ÄWđ‚ȚśS…$ÔnÈC %eI$qÈ8Ń»bžŚ»Śź|$·Z<—bÖŃÄnaPÊ»V58ïÉ$`ŻSšç„>[öœ„”<Ű`ž@H©3Zö–/Śt*Ł$Æ@†{ŚSC5,źą”d;rÙĂŒ–#Ó©śçôźžMF8)æ…CŒ(ˆăäЮՉź‡SfńŹ©ÀE%\g-Û=úV/‰ü]ęPéÓGç3>6ș 3ï’?*ÂKšé“cˆo0Ôÿ'í[ßżŸ»zțč5±ázkGQ5ÌRŽ™(1’ ä’z óWb™œy«mß=ÓáOȚrsôăżj±„ÍÔ~dS+G»œŁŸŻă[9(èdŐő4ÒôKr#°šI„ÊÄÁPŸ©8śüȘŸŻŹ ;—“"̀ć‹ŸSĐzÖ\Țò]JHž=hŰił^ÙL Ç”:œĄŽ97S\Oˆ~&ßk"9„”Ț1ŒüĘ2F9ăÜV/Ț•úŁHÄæ4ëÆMV Š”!‰ƒ+F{nžŁœiű—ÄçÄ·bîuÙ1Œn*ôz1Œ~ž–WL»ŽMlhPœÍÌBhÉUH˜1ÉÎqŚ‘ÇÓ­sśsGq©;ÀêEĂä;rÊ w_^3ɧvMș‰źk·7‹om$ÒŒ6È#$'ćÿ‡à*KOȚĂąŸe,Đ@êË1pۏ91ŒšŠGŸ­ő€Ò)鐯Š:ĆńÆnî8éóž+[‹Ź_.Ę·wiÈÞ H|A©3ïkûČÙƟ0ç4r 'kr}ęVęžÇ37OÎȘO«ȚĘHÒ\]O#·Vg$š,‚ĂĄrNMÄčÿxÒŠ©yK™Ő‡BńEX•”ĘE“k_]ô2uÏó§ĆâMV-ĄxŒp xéÍB±j?ű‚Ûł©"òpł°ëŚœRmwQvf{ëŠf9bd'?ZJ ;€1­Źß:áźîô.j6Ô.[;§”çęŁO•.„tŁ q0ï?ŽźˆÁž›>ńŁ•ì“ûfûț~î:mûç§„EęĄsœùòç§ȚąÈWŻgv%А“Ô–§hĘlÛö‰vôÆăŠvÿÙÿÀ@à"ÿÄ ÿÄ”}!1AQa"q2‘Ą#B±ÁRŃđ$3br‚ %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™šąŁ€„Чš©ȘČłŽ”¶·žčșÂĂÄĆÆÇÈÉÊÒÓÔŐÖŚŰÙÚáâăäćæçèéêńòóôőöśűùúÿÄ ÿÄ”w!1AQaq"2B‘Ą±Á #3RđbrŃ $4á%ń&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰Š’“”•–—˜™šąŁ€„Чš©ȘČłŽ”¶·žčșÂĂÄĆÆÇÈÉÊÒÓÔŐÖŚŰÙÚâăäćæçèéêòóôőöśűùúÿÚ ?Æżđf“=Đ·¶y-Š+œčZ”á[»(ï,d›wÙ„Ú‡zƒèzŚGk`°;Ìç̞C–sü‡ «"Y@€; c“ŽŸÎźÀFT2•=žÏ\Ÿ•â‹Íòaw-OCŚőĐêŸ"°Ńï`”č-Ÿ^r 3ŒšĄŻé‚-BÛÄà1ƒj瓌őŠÄmArÏšĘ[¶Đ"TeÈ óùŠÌmx_k#JÓÁbčóçžś€ń;5źšu[VÄ땐~òżűáΧꋀÉglÇíś?5ÄÀòƒû úúĐ#šń‹ZÆâ=7Kq$ႌ­ó~ZŸQńlšnœmhÒG$*\źÜ'©č/ i°2\jú›ì­Ô€ĘÚCÓă­`O9–íä2Œ€¶wčù›ÜÒžìzvŁâ›?ÆpY_±6ŐeûĘó]ìj3“ĐWÜKoŹűŸJ1ș*yq Xž›G9üzn“â3ÄâÚȚFÈVWcćéhžt9Ï_ßYȚiÚź)6т<Ä|Łô>”ĄĄűÆ/Úϧ4kûÂÁsÊ1ÇjÏŽ'Yđn«€GM‰ažÈ|xśŻ5ČŸŸLœŠîʶËeM Őèsë1[ßé֗“ĂsbZE‰ySƒó ~”ÛűÄ·šÊOkšBââžËłh#ĐûŚÁ©jòg‚æTžèáÙ‚ù= úÖï†üSyá[‹˜Ț0”1\AïűÒž#Ę)Àâ©XȚ„őŒQ€š~5hÓ&iF)™âš4!˜E;4Ö4QČԆšiZiZ”ŠMއm!Z±·ŠiZ€­4­XÛMÛ@m€ÛSŠ‘@íŁ6) D8„ĆHV›ŠiŒ’śŹœ[[·Ó4¶ŸÁ•mző«zźššŠ-›» }ćê s°iłÇąÏĄ_NÒ3#2MŐBƒŒ§+€ŃăűŸÛڍífQûÄ$Oÿ^șáÍy%ČĘi·ŃLÒ<‚,a‡žzc]ț‰Żbòć#„,PœĘÔę)& V'‰n” :Ù/ʘ4QœMÜńęáô­I.ăŽa?> ߄߰n 2ő=1L5ÿ„—Vڈ”L,W!ąm„łÀÏ źŻĂŚÁăk&H`û(Ù”$ʓÜçë\]ŐŚüVpù—(±«á2íćyêo/Ùo#趁žÌ“Íćt|ś8ì}3ڕú…KĆ.+™ÓVôç֐őZ˜­ŠùzŃłËčRÚ0Äă=렊Î-éZĐçč¶Q“?ϑŸ\ŸEÓÎŁźÛj?k…bÜČË ±Xőz~•êJÉ,`© Œ8=A$Â霯“eá=n„…ŁˆÏ /̓ĐúÔz§†|1m †–PF“·s…$ś«Z§†Îș†ŠÉaš!Éd\$ŁŃ…V‹P·Ö˜iZÜ iȘDÁ“=öd=ț”Âíng/„,„Ô^ÒÏ]žŽê4ÜŃȘU}ÈÇ5Æx§ÂŚ^č]ÌețäÀc'ĐûŚCsg©h:ì֖Źc:“…K·f%HçSëZzÀ»ŚôfÓ~Ԟ}ŒBK:"ż2ő țtƒ­™ç›M…Ú6Ž"Iš5çžÎjöšÿgےc“™A‘‘‡Í9Luçôšo­u SnÒIs*č Ä,‹ÔdwȘ6’Áóo•đ9ҖÁcĘŹ5‹ BC3baÖ'\~޶Őę ËQq,ˆŃÜ/ʞ#”ŚńȘR.ż„@òyöڅŒjXù Ç&Ńî8?•X7ń„ĂOâ«ÌčeŒ„^zOÇ5èœMCÂQ”Éó< ŃKžxż†+Ë X”-YüçòVfĘÔ/Š}«ȘđNššUź«Ă)É3) ‘•ÇăS}AhœoH}_m•˜g6êWÈNpW>žȘšÂüJš^îPrÉëí“X–ž*Ôîîmôëû4Á09w'čn”ësI™„4“>Ԋ>YŽyĆ=ĂesË@y8őÏz3Ì€§ëZĘö™k4“yk8F 7éŸJϕ#m·g‘8ÁeÁ?‡5-Ő±¶ŰŹà».Y0ržÇȚÎŁĂÖ§c=±]F(ìšlK„c©"±ŐĐèóŸź .țü„ŒJG!>G{őš[{Àzq”țŃyc_0̈́rrĆ;WeŠÁđĆŸ„Ś–îËčÛÀÇ9ÏêtxBHVŽžíT•€ˆÒb„)Q°Ć0ÊçšçuižÌÖæ92HxNӟ§OÒș23M+H.yFŁá ÍežćžÚ3‘5łm˜ö‡N=…7Nń^„§ŹiĐÏšZ/#ÎLIÏ#Žąœ[mcêÓŻæóœ”ŚđÏnÛÿŻűŃȘŒŁeăM&æ57œ€„|É:•ÁúŐ-gWđÖ± A<ŠgNc–ĘK:PGJÌkœJ+‰ì$k=^ȚȚP$Y0Č6I’xü«™ŚÌĘÇ6as„±b’‚ß = éCbFä~ ‚âcĄêśBh‡6śë•t?ĂžzÓźìő[Éž€ŒùńÆ@zŒàöśü+‚oȚܑ=È9pLg>ŠŠŠæêŽwšXă©+Žgœ›ŒëŒi«Ûj¶–0Ć”ÊĆæ>Țš1ĐÏ#Úžű̰ [(YŽ3éűŐ»›U…šăRd—z…R»''°æȘÍe2y‚c‹ˆÉóžAÍ}cűźF‡ÂúƒĄÁò±ùš”èÒŻ0j·±žÙpăő‡­Űë m%œæŻŽeĘ»ì™Î9!°jÄÎ{áÄv s{svPçćÏù➍wIŸKžòf»ŠrŒ(BrĐŚȘÜIqЉl”}śzTË”.BćŁö|:ó/èśZuÎLÍsfß43ƒžôÏ­fv‘o4ú”+±C l‰%8UÇzŃŐ4[ÄÔíWqć;śÊÛD™îCùÖn›{ö èçhÄš§çŒôaÜWSźêÚ-țŽ>Í)Ü>펑È}˜Pđ}"4èć‚c.Ś’h[pÇeéYx%Iàu4ښX$š2rÊ3HŸÀśśEĆĆê[€°R_i9ÈőȘÚçŠ5ő •†ăɆI íÜc“ÔńĆ\vR諟ȘYÆ_™DÌÁÉôÏëY1è14èŻ«iûsó4ŒęE>€ő2U./źBąŒł9èI­č<źÛÚ}ȘKòÀÜ@ °N”é~Óü5lțv”ÖòN)u}Æș<Òł‡1à§±m6%°č•dEÄđLrAőÇoÂșĐyŹÙŽÈ^dž€-œÂ$EƒÔÜUÈÁU±cܚhEÔńÍD š™W**\Ô9Łq6E80ȘûéCĐ͂*2”›éwPc”M&€ʅ–§<ÓH  û3IćÔûE&(_e=€+šrź(<Ón,bșˆÇ*Č:U… SĂP3 ûîaiz#<à:B3ü±Y:ž‘w-Čìą[˜dŠêŰänS‘čzàśÆkČ8ÍG(‘Ła|pXd€8]JăLń*ÚÚÈțMîđÍĐ©ï·ęê=VÒX Š,djä“‘Ži‘_Ò”5}6ïSVŠ”ÔàùŸaû©‡q‘ëùŐ¶Ôí5Hƒ#Ź:…ŸI·‘†OŻž>ą€<ÊÓAœ7Š!DŒH‹ąe<`ùŹHŽËԓÒ+8} FǜŽż…{jZÀ–ĐËD ­î74jyÚȚ«\V­miœú`AÉmßlŽA”—<{ž1žâŠĂ2ô¶xz[;čYf’Làás‚1ê:ț«WNŃí"k­2â6ŃrÁđæ»ù'>ߊȘhzčĄÛiŃÜyv볂‹g'ž„sÇĂjún©cŹ%Ì`;lŠ)·­Ó“ϧQL^‡@Țv”š•ôłÂèb‰˜á”qÜzWC©x~ÓQÒ)“3ìe#.dÇϧć[WșdÇI…-D-*-žáì?„GnÍö»XŠ(—*Ï#FO@ÂăđcƒíNĘęO1Œ°ŐŒ-šZíș–;HŚÊiŁ^NYIé~șŽś@‚užóĘČö•ävÁô©î-Łż”Œ›ËYa`ÉćsŽÿ\Ö'ƒŹtë[»ˆìVæawO!ÄhIûˆ;ûšK@vÜëjŐČ3Q•@clŰÿj>šÓșPÆăv}*ń…­xnȚçVƒV7&)ą`@*ž z{ŐĘ^ț]>Ä]Fbhś ĆóĐę( j_Û\·Œtû”Òî/­5+ˆ>Xo”ę02 źžÚhî`IąpńžÈaĐÔŹȘêU”2ž Šy'‡§ł“Lč›TE†æ%ĘĂÆÄ°îN'ŸÒŻëșS]Co~‘,ȱP ;’29ÈÇCíÜ⻛ ÂæÜÛŽ@BF 1Šri&(‘ ș–0‹”0€=:R°# ҧșœ ƒËęănęĐ3‘íŸÖ€XnŹmśÜEșÊGš3•až‡öŻK‡0<łÉŒŽČ6á#LÄ)Î~è?‰Ș𧁔8çž{3kp“Âc’f ț c>ù©°Ï4žȚ!¶H'È3òö9èE=–âÖXînc H€Ź nS‘Šč&&Ž 떷P!áLkœŸPz~“Y][–6w+}kY2F7b3Œę3E€śêËńëișaœïò]K/Șç¶ U{ËHïm%¶˜f9© ŒŚ’xôűïthb–7Q!·û»Áçć=·s«™Lšd¶‘ÇÆé«zŠ«ákkœ.;&ësŹ˜$ÁĂ!çûZè) vhĘL4™ CŽ €AìkšÔŽ•†B’pŃ̐żŐz©śŃæ“4áșÜ6ȅŽ ‹y9ߣb§ƒ­eâœßQÒ,uDĆĘșHÀaXŽEy~żàûí&Gš(Ì֙Ètäšś€ÇIâč·].Ï-HŽHĐr}ÔT ČB»Ìnű ŽŽBòZ\,ć9Ś#ò5zç^ż»ŒE4ŠĐƒ•ˆ ÚżJ@[‚kRÖßèw èYá\3}W8?\ŚjÂ~ Ă,k Đëć98ęk’đç‰eÒźÒK€òÛ€f5Gž?ííŻü+âDČ­żÚOg_ÿŻE‚ç'«űRÓ7ĘiÒùđŻ#a"@>ÿ ĐđŒ^Őżłu‰â9ŸűOŁ{{ÖĘö—o§ČËmâ)4ű›țYŒĄ—đÍI§xIŐâ{輟SfÊyÎ6ăé€(ł@u*CT‚B)ê9ȘÚnž4ûU¶YăO軚•t  $^8uš\њ‹q„Í8šS3Fq@æŐêMԄLZ“uCŸœMŸ“}CșÔ6ê3P†„Ę@î„ *҂hÀaNÍ@œ€&Íê iàĐű#dU[+OșB“YÀÀśŰüêÒ©4őCE‡sž‹ÂąÒ1†«lƒą,”{`ŠŻwá­bćHțĘV#ȘHüAֈű§É .qóhZüö«o>Ą§\ȘŒ%Ą{ä7ZɗÁęÌrÛÜkp5ł©P­ rčôÉăóŻGòMWș†ìko/#ȘŸpÔÏ9¶đm斻nu-EGËum1u_fB2Ó4û«MtD‚-CNŐàR4êQĐÿŒœ+·K˘-őŒÊÿȚ ȘűЧyö+•ó7}Òy`~xț”Èät­]đꏐjšÓ[ù­ûëgÀžyçôŠűsÆzuźĄwmu#[C<ĄąY#)Ž‘Î{Îș{o ĘÛŹ€ë7;d;Š*źèO4\x>ÎôżžžșA–B;{šÒ†öÚäf ˆ€ì05)Í\|<љƒZ}ąÍÿœ€fĄÿ„gĚzăMń‘îÇwî>ŽÀèî­ŁșĄ™C# ç'°Ô٘ZË<3Z»’YŚSÓÏœ7ûCÆZs”éśńŽŻjű?‘ÿ ­qăM2fXŻ žÓob;Ő.ą lŃ jjéș\đ9îe@ÏżÈ\mUÆzzZűšì/-”;8îí%Ybd2ŸÒŹùf‹XWč)ÀSÄg4ኊNϻȈ”gă>”`!©Æ2Ê@ă"Ï"űƒŻËbÖ¶öŽ,ŒÌ™ńĂăĂšä-Ž»›­>Bș|ç/ÿ p`1ĐŚ­ê-.çĘEs™YČÙç°ÏZżoᕮPŻ2 ł=1Î@©ł:cmíQ5±ÏJÚòE!€±†ŰúS čô­Ö€Tf۞”ˆmÏ„0ÀȚ•»ö_jOłJv‚aaښc"·šÔc„Bրö„`1JkFJ°=ë]­=Ș3iíE€óűHÉĘ[:ŁRÆ);G}§ź=«”ŽÂËíą ›;êłáÆ3‘žĘë۞Ì2•eʑ‚q\'ˆŽ(,măŠwÙ7ú<Ęő\śÇò€Đ\›B»đäv~DIż9)s€Iőæč/êš]ÎąFŸe * ŽŰÀcìóŹĘJ CNŒ–ĘäżV~|ĆìA­=/û-fŽ–Xlš8‘e•·Żč©ôCÒŚUÔă‚kˆá‹«ŒŽĐg©ŻnÓŹ­ì,b¶”P!EÂăżœrśZï„%òí~ÏÆŃ…EÓŰ+±±·†;HÒDQí Ć4‡} €"ŠŰi dvŠ",RbžVšE&hÍ!”»©„š"›@Í&i)1@‡fŠJNôŽž  z­ ŽĄy©•2)é(Č”*E\™S<ĐO ö§­»Ő}#Z” €wÙÈŠ\֗–­*ÀžÆ)V(ž©Öš™bU©æ€+ù)ë ô«# Ć!•’jy…}*ÚF )Ž€)»TrڌfŽ68€)šÇ6ēĆD``zVàP;S J{S";RǑV>À1Z 4âŽŒŠ±ŹOAÓïÜ}źÎ)ÈŚ8ŐŐ* -ĂâÏ0čű~Ö·-6ƒȘOŠćą_™3ô5Đс2ÉČOáÿÖ©ąű‹ŠBû5M?QӟÒx?Ïá^—˜š|¶1H›$]}dQšY5żü+q€5‹u'Ž™_æ+vÊóNœÁŽŸ¶žSÿ<„VțF«ëđîČń”֛ŽmœÂą2ȚÇEgÍđ—ÂłRÎ[rXfaüóEÇo3Š6{节çMó##‘Œ‘\Śü+ ítŸj֘è†Bëùfœ|=ńÍŃŒIgxAqæ(žYŽisĆ3”j‰4SÄE–ƒ œq@ €V^â‘bfíO6íŰŃ  %Hû”(ô«)^XŠtŠxÆhVQ(=*3”Z1ŸJnÚ»"eS=Șž„ŁYjÖmky’6üÁő†”JûRcړ@™çWŸžŽ†xà”‹T¶1Ÿ(Ü7ïb8è§žöźFđà“YmT¶x.ć Ń%{ò?^ę”NóN±Ô&‚kˆЁĂÇ(2{J‡ÓG=€űKŃîMÍŽ Òă€mÛ~•ŃÇdTŽ+Ed\u§pNE+XeT°ÉéšG°ÈàUŐrZvK:ĐAӛź*6±>•ŸŁÄ­š.9łfÇ”0Ű·jéÌw5 ÛíbÈ V9Ś”qÚĄ1Úș7¶=5ŰU#ô ,sț_”/—[­§&x“û>>€±ˆ"Í/}+kì>‚œ-8Æ(ŒA L–æ” Ÿ „•=(=mÛÒ§H9lDËÎ)ëŸJš-óÒ€H*ĐҜ‘éLdK*ÂÙ©vČԊÜs֐qR,kOȚ;ÔlŰ"Ž]NöÒ須Ă&۰ùg_R;U“sŹYźWò„i§Nkuhf”$ æ…ÜЌ)œ3EÖcŐ­MȍąC+GóˁÆ<țTʇsEŠSJ·?‹?ZRÉĐóF"'Š!Lìù›æïOòă?ÖŁšè(ț~G €I[,2)b™(©ËÇțÍFnž#©|”sžÔnŒóÀ4ŸjÔR7ŽVéPꔯ„XóÔŒR4Ê{Ńvpi|Ì RŸyȘìŻž9Ș”ÉLœIR€ Udțí&śĄ„aÜŐóTŠ_5{ÉódîšQ9iXw5Ö\ś§–ČVç2ʂ0iX.^.)Ÿ`ȘB|žŽŠ_J,.Ò0ÚÙíUÒOZyrôì&W©ŽÂPf+Kö† ғˆ\ČB砄ۇ°Ș>s±ÀïȚâæ‹ËL *źiȘXőj]€žŽÒ ‹ +9\üԌ§?)⁙Mgă4l$ôĄS ƒÈŠ”oÍCĘzTedhÆLP&5]Á˜d#”Ëë?œKç É2)żhaE‡s\Ê=iŠ|wŹÁrI憟4X.i‘M7#Öł ÇҘe4X.jęŻè7`śŹƒ)őŠ™ ›_iFnńȚČ<öőŠ™Ï­ šÿlś€ûZúÖ?šÇŠhȚç±§`čŽ.ÓÖ„qúÖÙvç€KèiXw7ŸŐ:Òˆpy±Ź]ȏÿ].%ôąÁr]GHŃuHœ> P¶ăòí9őÈæč­wÁp^Ém“„ŒAŐæ‘“Ì'oĘIĆtÈ;Rn~ôčù˜élÚxâÜѝÁ–č©ï”{M6=śłŹ+Đ8Ś'âêpĘÚź™jeŽ•ÔyđŸ^CŚhÇAÛ446r"šêâÉő·IwcÈÌBíÎF2­uâ-RŃmdw‹Í’Üî‘Pe•‰ŸÂâ NÏXł…tûi"[pCDäQÛńśśŹ–YìĄ•€ŒCŠƒï&>”,Hè,l“!ĐFÛC7FàÏ…Mș”±É·ŸZ7T[….êv ’KșŁÜ(ĘE…r]Ônš·Qș‹É7RnšśQș‹ÉwRf™șÔXWš3LĘFêv ÍzS7Ræ‹Ç`z 0=7u©X.;җ›șÔXw$©wQî„ĘJÁqćłÖ“4ÍÔnąÁqàÓ·fąĘKș‹ÉCRïš·Qș‹É7šȚŁĘFê,;’ù­ë@rZ‹uš°\”ÈM7qőŠn€Í ÏzkȚ”™€ĘJÁq<°}))ÜNÍ.h°\g’ž‚ƒ ÔìÒf‹âyiŒb—ɇ©Ł4f‹Ä0ÁčPŽvZ˜šniX.AäÆ?‚šcÜ©óHM0 Žđ”dá©· BE".ʅ&æïRd{Rn_Q@0î*C"c„FŁ"•‡qKFzŃș 0)‡o”!ŰN1E‚æ'Š|7eâm<Á3ŒR(&7SÀ=Č;ŠçŽĘUĐtéŽëšVöŐŰž‰ŠČ'ĐdƒôăȚș닕'jtęjž•Œ…>NĄÌxŒûÈS¶ XŒïÏLTڀ‚ąkvˆČb&1Ž0wgč$žž•òÜiàÙÍnć _^ăŚ" ”îòv‹€"F ś5Æ+Op‘¶TIàqíœW­Ùx§FÒŒ#ޚ]Ș KÇϻԌńÍp6„ŒWłÏ©[‰·nò~\ȘxúŽŐMncș3ÄȘGžÀ€[IÁŒc©çš| őV=_ÂZÜÉ ÓË;Ę+ȎW”Æ>ńśæ»?2žűqŹtŐžżAçI”ŁFZ,güzWn$śŹomH•źXó)ȚeU3ÔĐ'OZČKBCJdȘâPzJd_ZŸÌŁÌȘûÇ@hȚ(1”Ńæšźd›Ć0- /˜j°ozvÿz@OæZ7ûÔłȚ“œXȚ}hȚ}j ôž`Î2)ˆłæZMȚőăFê@NúÒï>”î(Ɗ.2ÀúÒù†«y‚ËŠ”æC-TósüTÒÔlϊőSqő„ߎÔoÎś€óÇ­Tó) ĐߎZC?œRȚ(Ț=hïÚ=é|ÿzŁæ{ÒùœQ`/yÇ֓ÍȘ>n;ÒÉï@LÀu4ÏŽ/śȘƒIêi†Oz,˜č_ïĐ.ÆqșČüÁëG˜3֋­ç“Ț›æŸZÍ‘ĐÒùŹ{šC4„€w§yÆČüӏŒi<śPphLÌi<ńY_i“ĄjgÚdĆùŃ`5ŒàwĆGöN2k,ܱëQŽÌ{ŃašŻr‘ő9>‚ąûlgûÂł7Òo§`4ĆägčZ>Óț!YEé7Ńaæuî7íięúÊßïFúv š zC|ŒŠ†K–“©âȘoŁu)zBüT[©„šĆąœ "M~|ŐD=žÈ#ŸÊȘËùeŒ¶ł‘­wăæ z‘Ć: ZK{Fˆ*ï=e Œ`/=Șœ3Ï+Â<çęk–æ„§Ìč!ÀùW€ŁąŽžșÏÙÛ EŻnæHRŰä  ÇŻJÀcHšÙeĂJă€3ÛőÆi¶ń5í۶„œò$óéFÌgčXߛ۞P„#ȚBsŒőp9°434:T0ÜÂ"’1·ŽŸ…i‰}ë„;Ł‹Ă-Î:pÖ©Łç„Iç`}ăEÀ¶ż/qO”DL\úț4ń!wM,ŁœUóHíMóžä⁷ŠiqëUÀï-DŚB˜üĐ;R™r:Vpž'Š?:<ö  țiŁÍoZą·=?Îhč=é7Uq2Ál}iÂEÏȚ⋁cÎaÜŃæ±êMWóï@}Ę(s%i=ńPŠ ÇZ°$'űš-ƒÍWĘÍ;pÇ!~“Ì#œBZ›ææ€, H<1©Çż5Ozÿ|~yž†ËŠuíQ™—Ț©4êœò}*5čÉĂ \É)Ąç-7Ï9àUmÙí@cœ w ŸŃíJ'ŻY™G黯rsEÂĆĂ(##ši•O”AœŽÀPß/EÉíJábbê{Ó ĄȘČ9Cę( rH§q #œ&Ș™Ž84yÇšwè8x„-èj—ÇȚ4ąoCHeœÔ…žëUŒÒi čí@ɏ4Æ85 —ŒÒ†3@‰rM!őXÜ`қĄFŁ'ÁŠ1ÁšŒđzœQæî8ƚb[Ț“uDZ“u0& Kș ĘK“@îÍ!p>”hÁÍ?}&êBŠ“oœ+Œđm㐠|ą;(cÆœïQ#†Á%{ŽŐnL‘°w'žNMr$jCčƒm>Ę+ĄĐ”ș’»‚FàpùïX0*ę |»ŁÜMzáË9-"žž‚?0ő r€~uQ‹oA7cʶčŽâ’)Ą09ÍXZŁg”Œ^MšEPrBúû՚è^fl¶źŰő§ UCő09Æ)0%Žÿ­(~ż1ž° ‚yőŠMĄ'žŐ*IŽÂyźżÄqKæőš@ïÎ)séT!ÄÓ qҔ¶ŃœÎĄk:iŠ"L‘JWûW<(üéćŰap)ˆ¶2{Óò{Ö`–@~ù©ŸÔȚ”0.Kó©ă5T^àc ßUÏăF \,ĘéÊÿTˆ;5K磀|ÀGĄ©d1ő4»Șș’Ă#&ŸƒŽ’Ű„Ì%ßFțzŐ]á{țŽ»đ:ŃÌ,–Ü1Ć4Ł0áxȘÆE‚ÄÔO3ă‡#ô„Ì>RybÉfÇ”BÓ»Œ'Jƒœ““ïO =i9ą;•“Ÿz]ùŠg'$æ“57‰ă›gšfVț5˜ÎAÈ9+œä6©2YźqÜ} 4É$šÏK€‡PO”H/qÊÆ ŽôÉ-yă<äSZlô&©<ìä’Gà)Ÿ{zŐŰW/ù™łűSdč,Ąp8öȘ>aÇSIæZ,-y™€Ü=j¶ÿzMŐVÖđ)D‚©î4o"•€Œ%㚊™óU#)Æ)<ĂE‡rŚ›ïM2 ­șÆ€”ÉšBőê P»©r{ŻșôcÌaÆM/œEUȚ}hßE€ș· Ü›| >VĂzšÌĘïFïzVÍÁáłôŠî#œóT„„t4Ÿ{zŃaüÖ XőLÌOZ çh°ÏÀA ägŠż ŹQ^æUH;ˆÆČŚÌ“ 1Çá[6ÊÊćÊv+· îôëÒčQ©kO–+Ęâ舕ĆžduêȚő”u}ušÂ–épDOŽčT/Ż#··ZÊđę֛ŒŃꃷàqő5ÖŰÇe 7qۀŽÔaœcžïÍT[dČö…f¶–Jw1fç.›[­veÄæŠèËžu€ËŹ@öš„™Kőü3ÎlȘ@XÂúdûš…üĂÈP”(vÇb~”I暄ì!ŰÄô5*ÄȚ SÀ ÓŻœÎzSsbć‰Aç,h0sœœSłèiW$à}ÍO3ˆÌ>†—Éă†Áô5)F^vàzÒ Èp9Íâ±_k‚ż•XôŹäšÛÓ…žì~Žę ùF'˜€m|~5`ÌÌż9úŠ„‚W#wòrF3RćpJÄæAŰęjșU?zȘŒÄ’ }Șc$íFGè]7Ł„5ŻAè Ș, Ósާí]š uŒăi©Æ8çČEÂÆŒšŠK·“Ÿ*_ŃŽO âĄyU[““YQLË*°<ŐÖaÜÒœžŁ,,ëÎOôÓ:;uȘFESƒÖž‘,­…‘AúŐhM˛ÔwÈô©’uRrŁȘ9€ Žçҗìò Îć╦q–CšÙXtÁȘë)RwsÚ„YrG#Óh,˜č4ż7çAqϧJoʓô«ç…9© TšC „ćHȃĄÜhç î4n5 1¶A4ò‘(ùČ?|â+äæ—ô§†‰QÀèj9ЏŽa8ïIș˜ÇÁÍéUqĘIŸ‘‘€Î 0‘z.1Ćš-Q’=OăI»Ț˜M)ӊ„HGÒïbő€śRđ{ÔJp~u4íèÜÏŸO\ 6ûю:ÔjÊ͜ŸĄ©Àà M€c§<Ò4ÜdJđҎ`†ŒúÔÜH,3@ÏzW|ŽßΗxèr*4sšVl9ö„qç?x❟/‘UËÙMBLÌ0~_z`jFŽç”8ő'šȚä)(©­TF€Lă„DîÉ9g<žj}Gf\{“łi;łQoă9ü1U~Ń·Đ ‚[Š|óEûƖžXÉÛËҘfȚĘóTLŒæˆçÜ{ŸjV –öś4ÇdqțĐ„1Í*ebüÏZŹŃÊçˆÈÇZi‰°-Ž?­&à*UȝŁfN=M;û:^;{|Ȓű=EN—$wŠ' î”/ŰóŒš9ő,Źđč!—ƒȚŻă}»qÇ„e 9”„zôĄepáTàqÏQIù1EʱÏB3SÛŸì‰6äuÇűVDsÈ.§$• o5‹$Œ8늋·ćEŒ‚qÈŠK§BĂtdÆȚ܏ʰË\ÂŒ8 œûĐ·—IĂ;…сȘńáÜ2ŽIÆ űä·+Ž:‚9ŹŻ”M1È8~^.NpG>ŐV}@ŐD·-Hühš(Èó“ô–gÈsÏ^*Hźž'#ćeÆx4YŒ¶QălȘ©'ńĄĘ™Æőv§GšG*áӞàÓŁœ·Ęć‚Ày"ŽgŐ Ä;íÝĐÇśM:CbÇe>ÔùâŠTó’@ŻcTDk)æMžűÍRi‰ąÇÙPä, žŒŒÔ3Bńò6ê§j-=$Üd‘Ői­§m9Y”ŽÙ⚚]BĆ??j€zśj# $ćIś­ËŁ1 …j ć8aö*ԐX„ì#;K €ś4í&w»€?ŰŠàtYę>`°ą™çҗÈQŚòÍ1ŒĆ`‡oŻ„#<‡ćź=Oč˜XF€rŹč„‘°ç·5T;‰Iè95:É2/úŒû†Ą¶„,§9+ìE€2Jˆ\>0ÈܜúԂńUpcä€Qv; ;‡z©}~–VòM4Q{ՆœYEŒëYz©Ôle·c‚ËÇŚ”vHć!ŚgŸbžKraâP@öâ«I{i$?fQ›dȶp=*ŹșuԐ™Ű#“‘‚~•’ò‡—ŠjÔ –Su&æföäuźÇCł”Šq"ïv;‘‡ÓŻÖčŐÓ'S—I22Pp1éšèS[·±Ä€Ż8ì,S”· ö7…Û©ù”{U”ÔßćČGâšAš[\ÆłG!ŽF1R4Ç;WcƒĐçšz>„ÜŒú‹Œ8['Š9€dšG·łDƒÀúšTžE?)'ڝ»x±ß†ŸóśGü«<ĘÊF üĆ;íLÍüéêâS±çĐÒ?ˆtéQÇtŹ›]Aś"œćă Qp&Ir>^”<€)''ŰUv—(⛻wŚÜÒO9AË)ŰóOŽäœN?ÚȘć1Ép}©Źă„+Üą.A8 žž 5ź6Ÿ˜qŽ0j€aü=HæšÌN}© ŽŚŒ`qïȚŁ7 zwȘEŠfÂFXûRí˜1 Áô§ îY9#ź*6bI;°9PìÜÇé“UȚBÍ”Hç”+Œ „ ŒőŽqÉâ™(tlŽxê68摰Êrzfú“qÀ7Tđɂž ń‘Ț š"ŹAMŁŚ”,2ùśÎ9Ą»­t·»”nP§·+Λ +ž3Íe ÎÔۃëëN{‡T, ‡Ò°ćÔf”w0+S·­쏛#Ec€†!șăĐÔÒFÛ~P:Qd~wœáB5J;[§jŠ#“~č'„Čđ$î3WèÙłÌŒóÈöš|Æa0:‘Py›ČœÓÔ1Á-í֍€—.ƒÇSȚ«–$Œ{ƒšŹrsô4đU—%3ő§{•/ąš&źĂ'z–7.č+žœàŐD‰7h>ê矄Uvj–Ś@#’/‘}Ș ._ilúZI§ä©Á횩$Ę ֑żQ2ôdłùÛê)Ăò†I>‡Ò©î!͊łȚvÊ(ïcšmwí6óc|xl}áÔԂ8ˆ`ŽI”RQ9‰”ŒvŠCr‘Éła`yĂb§ĐfÔtvˑÀțŠ|‰ ò.ÁÓ#ŠÊûSŒ#;\Œ€őö”Èÿ;Æ ž•<ČaĄrKIFŚ·f•Óš§$óÀĂ폟Gj§ł2áSfsÀŚSŽĆŐ6ä`źEV»1̖̅Ő$cž™Æ*4%páÆOvè*’4ì^ś©–)ZFĘÙäæ’€mìÔík‰Ł$}ӊŹRĘŹw_(êHçü*„Ż$Œ “ÆŰśȘÍ8A Žä_pčŹËo·âWőŠ©sșc˕œśÍ: ›BŸxŁ{”…K.OBŹ8úÓNÁbșÄN “ùSÊ>9sJD‘źŃÿhjߋW*q'źFió>a«ç#ïvÈ©vÉ ÙÔÿ»R.ŻRʧž›E+ʉFÖY@úb„Ê]‡d1l!lgyĆkúÎĄ§Ț4PĘC·BWë]l·‹±VÚžäs^g­êOšĘč!B)!H^—QÄԖ+™ls0‚ÛóÖČJì·ó>c#N8Ż]_@ùŠE!`R: ÔČ$’ˆv—*€có A,XhțUőÇÒŹ$á%»2e€đ=éúd^\ï°mWćrÎkhA9ÀĆ4ł(êW1°Vû„NyÏÿZ”lfŸ7K,«û’ŒxÏҘš}©ț úgœ}*Úf Łć vËŸyùŽÍ ”ƒĐàőÈ8&œ œ’3ŸÊ˜‹”€wg>Žči˜ˆ ê­œsK4’A^Ôź«0äž »™ùôŸœôÏÖ„GQêOœ6ƒ#3Î)†b9š^V9P)Vlg;Ż¶U•€`-íڟ‚>ö €XŐ2qœSC©o-Iÿ<Ôč 21Ê ïRF›‡ï0:Ö€†1–mő5·g™Č:Ôs_a“§—f±šć8ïU..^iUÎz§$0Ó2†ś< 6Ü IšL|„'ûF›ć%»#qÏÔČLÊ>Bżjî }æÏҒ»ßÚ!$őëéQ™ă.P·=zŐÒvFs֛†‚š\ŽŁ5\šEÙ%‚UÈxŠ,QÁ”š^”WŒ«ÜVü‘€r1IŽș€ós@2à±=3֛rK9vRŻÖ•QpęjÈ čŽ3ŒšŽd¶wÉw8èn 'h“ê)o]ŽÄŸÆž¶“*›ÏsJêځe/„ѶìŻl‘UŠ…%Ă#Ęœj)rȘ ŸwŠHÙł†_Ł kMP" -Ú9'=‡ąáрl(ïWUĄUÜÒRj­|Őób*êy«S]@Ż,ÜnC·ÔŠb^ÊŁîśŠșl})·=Z”aŸŰ[†b8珔^8=‰æ©™ùIś§ ç%F:c֋ ,ùÛî ŸćP€r<úSțM€Ș†#¶iÌëӜ“ü4\,F†ëÏ”[dd$ŠNxæšȚqő«Aò6XòAïCXh—kźÓÓR?(9ӊšă%FaVž<Ő X(?»‘sӜŠIÜ Öäź@HƒéL”4Î΀‘žxȘ†BÈr= >+‚ż, Ô`ŐíȘnX6ĘÌ9Ú{V”ZŒ y±"°öâ±€N6íÉ'šê)ʁJ€Î§@O8„$žăŰè@BŸ»HÀíH‚‚rLÖ*ßę™6ŁïÏœ2ëQ3&ŐfSê eÉ+…ËțLrć˜ÉœŐYą•dÚHőĆ6Ù­ÒՌo‰©=ȘS,ź·ÍÉâ­]1˜%î#úTl’Ż1«zš˜Êzôæ…žđ|€ö4ù˜yÒDƋ”~ÙŸö,ǓÉ8æŻ‡aWpĂ©ȘŒ1œ„nHÈÔÀdr ažûj+Ëł»JîćWžx„‘Ù[Vf©9û)V]Êä#zgœUú"­jn̄îS†SXțAK)''ž„úÔ/‘áŃĂ:Ž Șۓrț8ŹÜ›-#cę [e‚°‚yÉśő„·”óP`F_žúV*±-…9ô©Òi#܁œ18ąácź¶’/,"c ÆELOɐÜVŒŠ mŁ''=êĐœp1‘T‰hÒC‘ò“NőúŐŻX8$Á­ˆźíçUa)zNV Ž5‘łŽG^qSÇ,“o <ŠöŰ&„uÀă5,wÎű1űj\ŰXcććWRqSœ»žv*zcž*)Í«Ő[¶66–5>\ż/Ą„Í}‚Ă^?.M»ż:M’ŹH#”#Í39f#èM9'Qކ Žž5\Ì,^KHcA!ËŒÓȘÓ ŒtȘï3–2%›űzÔCraöǓ»ÊŠęÆHeß3 áI뚞'†'\)ÁÎO­g·œŹJ…BȚŸžÔDZg)&æUśÆh{­.k»íáV«Èß'!AÀúTH@“ƒ€0*_ŸĂ zÎöˆŚ €ÿ9š~ÊÓI'Ô”qmò[?Z•ćüÊIö„í±ŠEà‚GĄŠ}öü‹€+Z!oőÀ~ő klùoő SíZè·bFáűP¶ź•sÇJè~ÍÁÌ&fê(:h_šCÏšäQíÂÆTPʋ…QŸRzÔéjIĘ#~FŻn¶Ù±žă­T’ RLĆ"ÈžûčÁšçl,Gö4E'Ï=*Zx_xĈN6•ÁiK€7Œ7p SO™ő‹»È]C#śp*;«‘j›ŒÂȚÄTÛՇ`ă¶zŠ1ߚš9Z3śȘąËxàӕłßB5Öá$P =AàSțÓćG_„d«–èE[‚ć7(|z‡4üæTÜÌIîŽÔ»`?șEDrÌ N7Ț>•?Ê n;KqÈÀ©ș™a†U,9ÏJis22žȘ̒yP-ÁÈăéYŠòF,ŒFÖ9ÀĄj"CvÎà’F=;V„·b;Lčß+F{V;8ÁS’;sÍYIŁD@cŒš4ÚH’Pßڝ»«GZjJ$2íaùTĐ"†(nÀXÌeČő«șÈnŸŽĐÀ )†D/ĂúV7ž‹Fê/ŠvőnVCŠŹ‰Œk“ëR|»pPbĄ€­1Q†Ă Y>a‘ô5TśÇŠi‚CG Ëźpű=‰© ž’4 ævètòÍ DrŒśE|"EÈ,8Î)ò6€Ő—2¶|À>•^Hʰœ†^„ …obrš'jdڌPŒŸĄĄ)tvű/pIóŠGi“c șçœrk2KèçÉ(äœM7è†=ëNIurëSxœ zæ©=áž` nÈ”Eq<— śƒb ‹†V_^┌@_šéđSôäő€Yl[$őô5_pöçŠML¶Û·g©šŃuż|¶°'x5 –łDA!ŽFOÏҘ’}•Ô,›ûĘȘúȚ-Ä[N“Ő{Ô]­‡čI<Œ(ÆđAéBݚû[ G5bkx™űcžŽ„qN[2«ęăîió!cÊÀ/˜ËÔgò«6đ­äÏVÎ'“YÊłÛeƒ.Ob8ĆhÇqŠđ»Ś“ƒĐÖRșŰ“K*dęìÔv7 #±LłH«‚çÉV#ô­w$‘ČmnČЧ+Ü čǗ滞Ę#úÓmv6ó‚:|ŰĆiM4eUç qÇ­fÈ›ŸʚÖ2ș ȘŁ~ćÁ#–šZy:îÇ=ù©.Wlä IûĂœR.3Éú֑ÔD„ż˜A,Äú ޶höì·;Æă4èî|šăăŸG5ćńHńĆ!êJ—&Çc 9żČŠžYQ›zŒÏÆ~”œĄæl«ĆțRxščmzB&!۶ĐXs·­SšPŚRȘŸ•čûR(ĐĐä rć†ĐWć5бw3°«‹‡Í Ò6íyśÓC.ûxœ ©ŠĆ·m„ˆä8>ęj™YÉ,MI#…çùU)ő»w gĄ;‹áUL‚UäôF^In8 æ7ÈÀtŹ &i'#œĆmi ČMƒ ’x.>đ?áEÆm| ő ­FÉó``fŠk ŽÌĂȘ’*ł<±Ț!…5+ì ۞™8§$$Yڅű‚2"ùe8<úTștČ\Z‡“Œž3Ț«˜VúÍ/YÇï1€ŐÏ]Ùy(l„'ŸjêŸ+ś—úŐy– Ł`ŰÇz4gœúUˆąW čÀÇZź0§šx“kmSP3R8a,<ʌXw§ŠÓŚšïY)#FęXcSŒäHHlęiÜF Ă`gžÔ+1ăÍeĆpCe˜Š’;ÆW9ó‘G0ìjvÉÎ}šFÁéTțŃčŐŚ 8`=êa2“ÀSžŹ_†èÄ[kEX7ƒbì'8ÁSYHăž8Š™K9(ÏqE“êÜn“æ$«@íM")ŰIȘÄŽgb„ŽPĂaÆ;q֖À'™ÆzÔ±H™Ë©8èEB‘19+òŐÛ`‹{uéCvB'ŽD=Iû€TȘè䐩ȘȘ»d$Ùèiû•Cß©5›C,@@8àÓău/€7}ja‰—sIç„ZOćY¶„d<Ł}*1<ɀɿŚ(Ç4Ó ŽżJI ž\zPŚ1…ùĐ~â«Èê ƒQùà‚zPą4HڰĄ9SŸÊ«ÜĘ[–;r\Ž*œÛîPJƒïĐÖ{1àôô­ŁžźY3>ĘĄˆÎąȚ[Ąüé˜b:çÓ…{1ĆjŃ6~V©8èčÏ„IlĂn3S€ÈG9ÇnŐ.@C đ9ôÍXŠŰ—™yš–açEÛ«PŚJdŽ[8ĆK»SUbćr;Ż5Nx-êJvśíÀš€„»g gœ ˜ÀŃÊăxìM^òŐTaWÁ8Ź6]† ֌yŠž} )ź X3yJJČ śíRExČüÊ~ïU”EÂàă+Ï9ïUCœŒžSÉŁ•4ì—oȘdÏéT滆eu Ć'\â›ćŸk9 œœ*bÊëÊĄśëRŹ€«ҋ”HOĄíZŠ']ČGÎqÁŹűíAù߃ÁZzË,;ÖC•=§+=†‹ŃÜÁw€§gŚ”T†$iÂŽà*ž=jäŠ(%Ü仌š8>æČĂŒÒ*ÿ ÁȘŒ{65kƒö·%‹DŒ(ÏP=*…œìP Œ(áz©Ș\K-’ÆŠŻ°Ű0čűdg”rrN3ȚžÊÀz L€@b{ç4—wđÙù+6Iœà`pVȶÙé& 2číUő'†æÊfpA‚ÂëžN?JÉ­tTŐô±jL )FR=sëűV4ĐáPńŽò+kRՀ賒JŒsŰŹ Ï&՝žÀ&Ș7ê4HŚ- ˜FĘžĄÓ”șł«ÀÊ±ć]ò†P{Ț”§nšÊȘÈI8'>ŐvVĐ+ȐMaë"S8p?uŁźH N ÌÔÙX!gmŁšZV6G_*7ĘčÔàŽœv­•‚7˜/<œV[Ąa…êN>ąąf’1ŒsŒS©ĐőæËșBÏ#ägž;WJ̍ç`sšäŒ,Š–9SóÒ·u›Púd‹ Çpù‚îÆì•Œí{ ©Êk±ùŹÀàîÁ\v©ŁęžÖ8äÈfçŒç„dÜ\IstÒI—~˜5n8ü‰ÜpáAțUȘŃ šûU]íțékV_/•€HšÏZÛÔæțćmțʱ lïÇćÍbÊÒćáuWd<úf„î2șóÁ§0O,z€1Śż"“ž•B Ç” č€< =)êrœáHÀ§·UfR͌đ3ÏœF«€ÜóSE`Oӊ%˜ïòĐ?ˆ qOÎÁXí'űj8ĂŁ|ÀdńëV@‘Dč`Fś  ƒàÏ95zÊÎIžÆjłGżÈ8Tć•P(±ŠÓI\E22Cd`fą2Ą^œûR+gŠvu$w+€:`ő©Ł—ËŒ—ŠEGmćčÇ„/,@UÈîECłĐĘÜ«ŻAŰÒn2ÊëR.ʘÚ~”ŠêŒ@'֋?›ćFnœÔâ\»‚}ë.öAöf'#sŸz©©j;âXâÈ# Ô8PÖŸÌ|ŽäŸâÖyŚæ,„€ Ž” lŻgžŸ]8=êŒGśčÚ0x"š‚ŽŠ+ńqQlgo„PćæeeÁăš™œčXŰnqéTŁąmùÉ#ši$# YAÜś­*ٌ8ëíXÖ›ä!—Œ~UynBü٧”Vû+Ɛò„ú`Ô$ćțlgŚÖ’GcÀÆ=E3 ( †ôĆR.öŒü ‘G–ăÚąbŰÈ9֚[Ś­;Č]›î‘šp“xĂ)ÈêŐo$j]êŁçғ۝Bê}?ΌžXdôÍUÈhóÉŒä•Ï6C4óIVłȚĄ’ʐć{ÔqùĄÀyG'šźȘ^LŸj[0'i3Ì|üTÓ:”?($~•U„ÜÇi-ڌ“ŻŠi?˜Ìž,BTˆÒ0ęĐÎxÆqU0ŹBœăëS˜d‹Ž ^pi6†i[Ç<ȘIùqÆ3HmoA8d#9Ö|z”Á€<yśšâÖd[“žüÄT}zÔ>kè7›ČMò•ääöőŹŰË]9c’ qșŸȘêâá‘Bńő=›ŸćP_ß-Ë#€b7 -W3ê; q©M$=lBAûÂȕ‚HŹIăž*RÜq‚{š€%@ÎsȚšŹĐ.cŒŚÉs†'űF8ęMŠ[f“Í(ŠÄÛś…aŰÎöSòÁÏZÖčż{«Qˆź­€ö9Æ,Öm;Ü {‰vĊrÌ9=VùÒ@CáHÊâŹLšÌUd€#ÚȘÇ4Șœúę+T2ÄŹÀH„±Ôś­(.D…±Œś*+"7À'8ôő«Ú[çÌ,B:Đ&]U’V* šÉŒœ,ź™Ü1ƒéšÜvG¶oȱęć<šÁ»Ú_sŠôëš\ŚWVÜÚt€,ŒĂ ;zHÚ#ù€ČŽ^”y»“ÈPH/„0:ȘX«3[žčĆE«jpíló Ÿc,Ÿg‰RWőȘO+yśšćÖà1VČć3ïSÚ2=Ôk!Ï#5SpÉt«z|AźrÀp3ÍXȘOfO”vdóÖčœnȚÒ2ÂI¶ÓȚȘÏt•ÁW €Ă­RčžIÜ§Ž*Tlî2o/Łe±ŠŒœ‘KÁÒŻ#>*ÄK€sڀÌe óÏŠčÿxóWą"Úœó@|¶óXH^۩ߜhŠ5#lŒő§ÚŻúNđŽMžXÒYhbyŹdFs nÙŠÇ*ŽÉÇšÄÀg ÍNâ, +’ă { cÆŹCçŐf”ŸÜőȘ·RIoő'Č(łuK•uÆpAæł%‘™ÔçQQÜLfrę ëŠj±'ny”‹·Æ€c·`m'żjPűÆ sQ(i$đ;PIÈóL ăˆÌß) óu'„^†HšȚEQž5ebó>PyëŒS% #mUŰĘ RžˆcŁQ”ˆ çŻööG‘˜ôÈ wȘȚK‚8ê2)•VÁ#=©€ŠÄĐÔđU‘†7pV› ö©`ÆÄ­ëژ’à”TÀàç5YÎâOsO•òŰÀÀôšI'“@9Ç^icvNAĆ0óéOXÙù< `)ëžsN8g~HéOHŐEG;tQĐPÿÙmediascanner2-0.115/test/media/image3.png000066400000000000000000001216371436755250000201650ustar00rootroot00000000000000‰PNG  IHDR€±‘Fr pHYs  šœtIMEȚ &€ŃltEXtCommentCreated with GIMPW IDATxÚìY{Md‹”Y$œÿÿWÎŰ4ʀ˜‹3œ/]È2Ö™B‡dʗ7óù|:~#á• /‡Š"9Ó)###ű* —,±žæő„™ttź‰\TÄLÂK™ßCŠŒą% bvúkđ pŸșźCùÌńxMÓđ*śęWÖËĄą›©œäœé”‘‘‘|É5ÚJ­äU8Vj_\—ęYVyZi-Š2È/6óh§LaUTŁĄÖæZŰ0Žl<țÜÀŹl)…ƒÿ„8TÏ7ß}A\źŚ…-苫WəN À‰*o̊ÌMN8źË"4UȚŠòJ|qpzÛ¶=ü~~!ŽłÙ B O§ÓÁ`0›ÍÀ<€9Ÿ üȞőSP±æłł é»p_ź#ŚwÆK°âsM§ÓÙlVrŠSFFFđI.Œș>±"ïś{ è`0`ufeŐÄĘ1@\Œ”™€[%ąhÏHƒÍfsʃûŸ')ê+žVˆ?š8Sóù|·ÛńȚÄű&cÙ;ÊŻäÁ’{Èń·;럌Sd¶„WÇâß9YÆ+ÔöôëQÁÍWNoĆz…ŐXĘ óÏk…Đât$Íy„2€odex@PL ÀWžQl“ćûM5À›ÍHöMŚuH±Ê±HLٖ3ș.^ /‰ĐŸïÇă1[vłÙ 8™ÏçÏÏÏöÿ| K9PÊ:7HÌiᛯÙúÊD:„ŽłùgśŠÌmÂźÆ:%⚈Š<ŃTŃ"g»„–śS*ÌFaÿïæhI‹ă{q:^ž„áŒà!ÁÀ•BŰćOtrżđBœYÍqŒ"ĘuːyZ«¶€—ăȘäof›iê,Ă­Ł|ڟ”uőęûHpÙFHvMMś}ï^ùÂ/Đ+ę”ĆY›âóœéTNÚDY«q‹Ł‚ŚÑóy“Ka픋‰hćWQzÜ'ńj-&a8#űŁŻ* “`š'tv<ƒÇ–xum–¶Ûí`0 œ `„¶öČd°±TéÁ }ߏF#SÁ ŸtƐ"Œƒ­±¶KÙČ<NՎ‘T—_çÚ§—H^‘Ő`+Ă ÎX'4nXzé…òÏźébmćᶄš…wo”DTœ|UA—7hŁ \Œ«Mq‡ œv„łEțŽS@2€3J rM”Ź2È*áÛïśMÓP êá7W97œXXâ§Ó©Ìś(ÈՔhQ~†F ‡C蔘’]śțï\}ôŠ8UEr·.ÄŚ !rß§§'xœ^łgbsŁł&Uöûû{0X#”p5E\Ux¶äEùBŐ.oă»)w–€‹Æăńn·FŸŸL&tźČáÈtE_żúČOòڕÎÈHțź X Țív&˜…Łi±'¶ÉFEdśp8°ˆË±Æă1Ÿ&9cšTà—ƒ"‡‰]Ú,jŠžîęßŹÆŠÚo>yu?;3ŹÂ‘*]m2*ßś=Ú+j W9NŒušN§Űjțł#ĘeF%?w xì­^qÔúĆrŹûOočò«æy2™Űś$Ç[Nß ú"K”^›ÍÎŐ,#ű›mü#sČXLŠ‹Êš‹$•%À,™›tRdžŰȘK°ÌpŸH8(X’ëF“Çć篘Ü~f˜R¶ey”Z‘¶ő/–P̋"Ű뀿ą/§qłÙ<==ĂöŻ\ŠJD_ë0™Y™t:ƒshŸGd­p^ĆéR&œŁhKô5í4(”D”łßŐ|łMz6ÈUêč\Đ2€żS€.Ša៫ŐÊN$žùźëą?łšmĄÌ9Ăt KŒtôúț]Ö,èoŚuóùäà/źV+ŠÁ,7,ˆülŽ«”pż €E_eJ*ÀăÆÂŐÓĂŸB 1—ú’yƒ©ŻsšŠqŠ“ÖÿúF}»Ö1”!Öëuât~T”eQSç•ôhꆔú»ńś”c›@ ČçűőXx†ĂčKp.‹éë\Ó2€żG˜7cőQ§S Ą»źƒżÂ8Ć]8ź K‘X…Đ;âÏ⚐öû=YžŠi¶Û-|„»ŁŃh”Z•ĐÔś}9€ÉÿmćÒb3«-IZÉąF]§ȘÔ*mpU—’]ŚQÙ3ćè“Éd6›-‹ĆbÁtEș«YÄÿ)âK '–ÿčë8Ș|FÌ1ÈMŁ‹ˆ»ç#­2(ș»D– ôșœŁą*¶@2UŽišI`ú—e$łˆ†"°ÚQȚn·,Ù֙J)€.Œ܍‚Z]«”öT‹5‹üÛóó3+‹Xüx•Šf©âPÁi8œôîŻR”‘țêPĄCČE5”/͍œ(WVćI`ŹŚk“Ïd2ʱôËéƒaÀ ț”[öéŒ-ČIüè^Țu]Źé „Ž.ÁÎ8‹/ć8ADo ™äŸQù_Í"Si-Ùì7X»ÙlŰłșÁș~k¶ŒàŒÿy$‘Ś„Yćńń‘€%źœzÄ"ô. Á˜ŐÇ5œz­V™„ÉÀét !ŠȚlvzłÙà™E–r CٝżGô՞‚ßvêŽezV+ÁrM"ŹžŸšž/ Ílžàćry?ŸÏÉ"°=úŚrÎ:–ŻŚk6^ŃBŐún9é ;čuceè53}6âŠS(UŐ/6ÇMđ8ÒšÄ.„î–ŁÛ.•+[Fđ”Gœz ­V«ÇÇGÖna8N€íì)Â’W2™0œ”ȘHÓTȘ«F ęE‘Ű­Śk(€é AćXaê@lž2Ùn•”ÿ•ŻĂ†ĄżżË5I±*Ùóv»”˜ Ă3ó<NI8/—KśOQ”~ó›NeS&úŸ_­V$~űJ4*)Á©ŠIÉńf/K%(} MÓpWÓ/oŽĐ+ÖÆšv ]ŚĆ”=GS¶jÆCFFđ52€ű`kÔ°Z­~țüùăÇ`Ű”I+Ö ˆïrčD4;ŸÏYÊUń€ ±úő†öD4ëé5țîd2iÛ¶”/ŒYƒúŸ_,ŃëàHX·K0(Ö5–N~§Ł-ƒečËfS•ïbű,•lžF#ž/{&źŚż șŠ~UŃíÜ}””AKeڀłÊŃÓ*Ö$ dÈȘMUŒšĐŽJJûżîšŒRl+W?c.q ÀWœ E{B·ÿČȚ˜‰Ïód2òR5|xx€ûÆțQSÍć# ę‘e±–Y§Œjäš 1țG2§.À:y±>FÛ? čȘDd-—E/û˜‚+HՁ°© űæYoÌśò±Xîc+VŽăF@\?ÈàSÛ)ÛvăO±ƒ°5Óšr'Ő‡S@eÚ¶- î_€/źș™“ûf$_5śćY•íA|-‚’·d%Č4 P`”X,–ËćÿęGèŐżđc蔉S»,@‹Űˆ© ÂÂf Бś(Ő.ÇÊ\ “$tă’”\*…[%Tč ìąÍČvÛòKêPčmô­ŒeÜl œĐ_Ù0Ú3ÌäçĘü-‹ê>QÍ WæÁ‰Ù 8ˆPŐU Ó*ïĘü‘Œ)a2D܌F–Ÿk]Fđ5ąŻéVœ MŸÙüȘÈÓXöțóùüáááżÿț{xxžżżWÂă’ô äJŠŰpçÉTÆŒgælQRń±Áâ•Q»Äf’hLxfȚûóè/Ź.⊹ۊąÛÛŠ%umGWÙbD\Íł„+©j˜-ÏÁĘÆœŃ©ÊŽł›ŚÓáŸŃ-.ö ùÚÖùę~ùÆt:5[SŽuè˜N!tFđuĄŻ Áçê&ąùG™ą7aÿȚ4ÍĘĘ©KX”EDÓΟśÀ„Źzѧ)ŽV-ÁŰïŻN‹HŹŰ•^d-‘yì0†S«ęË2`íÀ†ƒT“țRęuüÎíĘí1álÖG&”Ë5ƈŠ»HÖË ww s·Wžk]Ć6q‡8VáwŻ1łâ1ˆĐlÍsđYŰă,]Â\‡r4ÈÌVàŒà+JÄiÚLrè%ç  aÜœŐPłțûû{$<ű6|ö".ÊjoÉN§ÓjmŽÀ=“Ç.Źú;ŚÔ4^ ÖđâYCr.Ć9-<éÚ*ÇźßétŠhŽëh!ó&ŃW?gx­­íș‘ˆŸqŰÙ]›ëôÇF`h ÒVÏ^Ő 7…çßçՏįpl$fôĄ«nu…ĐqÔR.} ÀŠÊOÌč­ŚëŸ?ƒáÄ&ß ÎÉA‚öJĐ”\­Ÿ‡Áj^í”-±;èo‰x5ÊÆì<æXŹKšÏEÍjùĆÙ›Ń—‹…ëșsł“@oeĘśVčŻ6œ2EMUŐXńu7^âšÔ…?%Ž2óőçM0öÂĆÇA=DìHNôÍHŸŠőÈș/kĐăăŁJ+€ż {mV.AŽZŻÒY}Ÿæ9—a8…m:R‘^Ղu&ôFż!źJ)`06(`›’Z'A]$ïge!ްA‹ j*ŐBŠnM·w·Gyż+3óÈ-—rÛǑ}5é}Ęç]äŁœXs)aîgÔm]P‘‘œńżšŹ‘0˜ŒîHŽR`‰Q“Iß-ÈíšöŰ}; ?{û n ŁËăüÔßÙlœŸ5‰s^ŁNŐîOŸR-Ÿv}D%6›•KAYV?».\âVeÏVžŐBAb@îëH+wln%§Ç`—iš}ŰĆÓőJŒśJ𮼩8d+pFđ㠁}źfžŁì™„j)Eƒ Ök~°mÛH4ł”CCæ—}œ8BUĆ”Ż5Šjöˆu\U3P"đ-1.˜] Q„ČŹć%vë`ÚYÿ&»V"‡“ûȚ7âò9ƋÍćăă#EĘÍ,‘ZăŚÉ–`+…üÎÙlÖuî,ë,ęĂáp”Z‘ŽŒ.†ŸșV.Ö;ăśúòțÆJżż™$HkûÓÓGì«Ű bŹÁRȚ,ƒ…ȚkCŻÓi KÈ9G;ŽT`e$_ËÓ‘ù‰MŹ‘‹Ž’űÜB)4°-!aĄë+óÏî!ŽŃG>7úÿù;írQ+G1—M)b0†D ™h ŒF#lĆü~ ?}U>CÁ)#żâÌ8°YżÉ[Ȝ4óL¶Šë:3Ïș:Kçóùp8€ÂÂ*Ÿqnft¶șˆSÊŃxR6Çç%ÎHŸüskśÛ| €+ܗQì ęŻhŠÍfcÿc°Mꐛźët„„űcòĐMIĆ( zÆÊŃâC?μ뚩ńÊQÖußF1XčÖ' (ŒÎ*öv“ƒŐ|ƒ+ûŽÜÜÀ]míÀ6_Đw”ZÙ‹Ć‰2ĂL'4E ÀQgwępwÀ\e2.<›\Y3©ÀÊHŸ.z ŽĂ;Úgăhr[„¶âĂ_y1~ÍŁ.[•öɀáŹf+șđú źˆ”śŃ’O*kŃgł”+æ«YòhôBONÙűóœ.â8ÛXúó ^©nȚŒïUdÿd›c晫ĂmÌçȚŽە ü»0EQ–gčrá{Œćƒ3€/ÿž*nšZ 3›L&TÂÌÈ9vŸ{ÓY,jq ï—íúăš'êšPMđęă’Ș†«ąÔ–uăb§k#UŐù|§”ădÂÜFNéfła}Ę?<(ęć°uX€™±ƒp#aÆæšÈIșarzæzœ~||ä=À 29Œ Üœżż§à‍ïČ#©ŠŒpÌV‘L8»;‰mÁ™…ÎHŸđÓë@=*ad)MGË ÌOêoćæ:*°ÊŻĂ^Ÿà zwËâ˧8|Ž.° ‹æÉqÊMÔ3S͝Íf»Ęnč\’»F~V„%$űIPęžK ʍ…ÄŚîŁ/s ęlôMímÛb”Aæ™+ÈÇ䯀‘śÇ6w6(ß:•Ezæt«Z‚h+§!e$_˜Ăw1·Q4\Ô]’GšÇŐyąZmĄs5-æŸ>oÍŐ0’#Š›‘&“‰2é3;€ŐsQ/ʁd2{%$±«ùKóù\cm'Xè¶­sçv»…8ïäœsđ@%O˜Ÿ ŰŰzsâUƒű>==ęüùÓ!ƒšáĐöóÁ™œšÄÁœÈ·;±Ù=f€xœÙ(«äWœ• 8#ű*°ű\ńȚ&ŚAB«EÍ7*$._Òìoș€qÎ<Ś8 .€+=Ć`sÎq&]ćĆhŐx†ív;ŸÏ9šŽÊQ(nyÒ ŸÜGٶα^ ­q41țî ŰM˜;°Őjőűűˆ»ȘÙ „Ihû—Çš$Wßń D ôȘńĘMłèëî9ÀŒà “àhXAŃšu:–y˜c»‘ŠŒŁŃˆ‰„•čÄ<ȚŹč,»h\Yp”ԗț–` ô:)ĂÍBËâC$Ś>EŃ\`XEŽnˆ€"—ÏDôF˜*c5VtƒuÚ+7˜<ÿűńCÙ3•źŚpßûû{țyiíQŁ+Ș4ŚĄ ÚRf:#ű*žÛhŻ(Ç&ÚžÌE«w‡źHFß3rü Üâkł è öĐČl:ïÌŚÓnúù]€ìàŸzGïI+…JŁśûœŒÙlxćë¶$Íçs+ÁäG­ƒîì`Žźdrß8°ùäW’ț8VD)j¶0~êŸôûÆșï·> nŠ#ęŐ,Ó śĂ©kzFFđž[)]NQEKž%wĘeKš«ßù©Ënćs€Ç‚ĘGL4üŁž&zêFG{hYŁ=Hmâ„È2ićr›źG©ö‡{ Eqq Ł…ȚŠiő­Ăë„ć$ìđ >> pÊœ±è{ùŰ8țË$NÏŹ%%úf$_ŸBæ‹uÜ 0lΑˆ8tèSòXOĐk…UYŠƒöÀà?ZQjŽÍŻòT˜oŻ<°ȘßH;QWÀ[J›•ù[ŃB$šŒr]Ű€7|Šhÿ}q’+äWQ»ŽüÛ9Ÿßœî[=Ć<›Ł_ƒ!˜§ĘwòàœK˜‘|E0üÊW"æ5.^‘;Žàc—ZőÌ‡Ă„3Țúű‘xŒ“nĄzŽDÍŚ*:ë–Rą‘țZ=­R—¶kń8NíG*ĄQŠN?ÒGUă$‚ÜÌăGÙÎ)șőW+ŽèHêĐLeêŽUˆ“7ƒ@qJ&ë:Ž~æ,—äą—‘|í0|șˎZ€(§ŹjÀń|ËQEÂR«Í>ŁÓë,zW‘űô7ö5Yś-/ÎUçR°k}äÊ ŹiRÆ6ô}O‚šúôG%l8¶đlȘÜI·ÎÔ»Ôkü±fł5l‰1âVìÆűŸ—S6·bŃvôöæ=g$ÿa+mt¶Ă»ăw űü^CY”?nBUńHœńG«Éϟ?ĄżTțœoĂZúÎçóŰgòÊ1(+‹ƒ• P„ŁŃòh4}ŠfȘán#“ĂÒŐy™$|?șoˆúž(CsŒŽß7ӋbVż‘8›+&0Ü6ĘùIq”ăÖ”"ăÔÒC¶!e$ż5.:ZTSbżé9 8vDž€Æ‰i@y`•Ă€q8Âh^O‹ärčtšœ“m^‡IȘÏŠæ$éì+Òźƒ|WČĆA‡”Ùl0Ïrúò{źŽ›Š zOrN, ZŸ țgöEá7ŸcqrsŒq‚ä9QŰqźôo8R”)ÇÙ*v ê32€ż ¶2Z‚ŰJ”ŃpTW”%ąČJ"đUgfúŽVÇ úËaÀ8”72ÿüGŻ 5ąžVEßàr|}ŒêÙÓ%,6U›íœN§ŽŽ` •ŸÏçNnxó‚È‘G«K7+%ŽŸ7 âëCEæQl©ôUw“üÏqąÜ< qûèàçŹg$Ś,ŸÀăúÎ7ÄéC%x_TPQŽăâcD$VŻd*UÜ5tÛ•ù~Jł˜ĘßßC‚AÁ?ź8Fê©iŸąȘSîûâ:nÊ.‚ŚQÊŃi‹,ôûkÀž(Í(űCQAŠ*-¶Š~śĄW-š†òE=d܁ĆÉ7ù„: )ÊîÜAzßLț##űŸcÀźz>đŐó/Žž2`S»À›ƒƒôœ€áWJQ-ìĐĐ«m[­"ćŸŰ,ÀžцtŽ·Ÿțqšƒ)ÜrljBÉ„ćĐtZrłêÆ9‰$ۖĄ7wmĆmŠŸÇtà˜ŠP|ÄB|K°’oyîÏìź€•Á“}5}{5àxû•R_æŁêlă„ȚŒàïʀăÔ3m°*ì|€j`°#”j¶ŹăŁKŒVšh‚1țS›xĂń9șżżW~őGŸŁHm,‰©„dvïđ _ù–]í\RăD ±CâȚEœI' évÉkžxĂcG”¶\5n'śF€\•Û&Áűo`Áł2Ń95ތàoɀm b™s-CŹsČžÉ +& ÁI­^ ŽĘn0ž•¶Ś IDATĘźëbî‘=~Ó4‡Ă–äóÿęwbùƒ- ‚^źæ«ŐÊYÂV‘掯cXôlšŠm[żâ'őłË€ß òÜȁÀțÉ_kSČìđ~Šn_LÜl6t„óńí~†û"`è­6 bp Ć  ›s)ËHțȚ ű©& €ČC%©NȘA__ĐYÊÛ¶e†­MÛí–OGVœ`΃S5mêăßn·H ÍBż«Ü6©Ó°ćXT;Šç›.ÄqȚ"wŃj”zzzzzzbW§čYLŻÀ`[Èț…&œÓë›Đ›‘|S ŰéôćšÀŒ ˜:.đàXV”SÎ -lZć§b{ŽĘŃĂlJ˜!·Cö|wwgÒÁ2}aœpÛ¶ždŒÇăjìÄ9+žčzÁŰ}I)…?m)”KüÛëbï2çÓQĕVdÀß~"úČabâ/SŽ#ćlSô%ùÌVÌ6€Št3€oŠ›JE"æŸ"Ív»‡àK$»’`'E`ș"ŽûG#ȘÉ/‘;‘fÁ}œSšZÓ„ż?~üűńăÇzœțùó'lƒĐęęœÛÏÆOWLD[ł̙a#bkĐ_ÙóVÉXÇ<Ű[Źk&ÙW Àßw]ŽS°BŁÍŸg[nšŠY,śśśśśśnÈR‚”‘‘üpìٍ žŐżź€ˆáŸúfàĄĄđȘÊi“ìő}3gJ‚`‡üü•ÓŻĐŐ¶-†rńp€­OÛë\Î0ï”û:9ŠßƒIšĂîû^nú·,¶}ÙˆX$`ż/ę­ÒÔ}čșźășÀ}هQ†@ęï$Ÿ32€oœó ,Ą*Qv ś„âûăÇMˆšâ&űŠ‚a}šmFé“ĆT{twЉÖ8§è.Č «ćiÛÖM†òìòś#žìęˆÉp5eL źLzÏÄ`kFLk—Łÿ† NűŸŐ_«ĘQŹÎ?IÀ° ƒń#ÇŁlő7 (22€ż=¶&*„mÛÂb7› ùg2„öŠhpát<§ÄCd*aX”u @b‚·ČZ<cT>ŁĄ}|||zzÂßCUvÿÿšXŽŁ+\Žézx”ZEeu”zÁ SŐśgß@ś‘ ÜWd&4ŸÏïîîtC“ț&úfd${ìjˆìÉuGœ'“ €]3u_a[x`qtFșsĐsyNŐuÌxœDä«6@gn&š•’!zzČ/Yś+țtdIœÉxÎI‹;6…àÁmÛZê¶ÆL”ò|Xr̃ŽĐüŚl63‘`ÎàûąŻû ÇęjÊŠÖ ú*ƒÏ@ À7€e!qĄŒä“E_xp)…Š/Żä{­æâ-—ËXè-G=—ìP_ßœ%ˆ¶Îÿ,±)™#€îœ'`Ž]p)Í€țŰ[\B8Îg…òÚ Œű™zy̜ óŻă„¶Ș#êŁöŠîß„ [Ę`…x‹ĆâááÁû*Kż À7€5Ú<Àà YJéșn±Xl·ÛŠi@bh„’+p…TsÓ4ôì"UU?%ܖcŸŚś1Șû[.e#)Q Żšdž‡Ă!D [it=MӜƒŸŸ…Ú$ș\š*N[X‹ ƒőzÓ •Žíw—ĂŸ&»żtù@ŽäàïîAęXôSC…à.‡ęÜęęœȚ“‰Ÿ À7ƀYŁ›2ÔRŠvÍ TùuBm9ÎąMHè}”‰0ÿ­€Ł‰Ï;Q€r1Dù ś…MÚ,;™Lp”üïżÿțûï?xŐ_yً|ÎHG$Ć‡ÒźyłÙTMϱëwźĆѱÄb°˜mŐYbę»é+čęÜdž…â9pÜŐô*ëŸ À·z6ÁnĄżT‚‡CßśłÙ ŁM±•€UŻfÈ%Ég'ՀŸŃˆăGÇKĘChÈ.RB†Ł# ĆŐk-—ő3F™śáphšÆüŸˆfhrȘ¶6E0çLŒˆÁÒ_‹ŸVąŠÍĂűŸĐënCW5‡O+”CÊGEÁ|>ł À7€%”€ŁĂ3ÿÛśœ©NW@đƒĆqč\B%ÿûï? «ćK+#â~`òP%göŃÇęŽP-Ą »;†—ăż*ŠFʓè -Ç~$_ÉD$–țjțU1`eς.5QkÏ"ń·Šż„#łJ0AŹ'èFÇ«|f32€o$by’śŽÒ‹eH“ź Y9ș!FT{xx b*?~trpœ0òlè/;ÒĆÿęśbZT”jT_ŸÒTMÏçóJ5v8ùà §hägI1_=nPÄéraEę3§]őőÇ7}l‘›Q?±Íđ(Đ»ÚÏR~~ÒŃÆœŠÖ1ŸęG3€3>ćaŽ«eΩ;1Û\Ïçs2Ïrß8+śłŸrȚ yĘžìHf‹SgŁ€Âöm+»ù€HC]É%DAo9vĆÄ2Ż;Á2pŽč„Ș_ë6r0l€*łRă:ÛŁ‰șOû3\\ qžŰߟș-SÿbÔ1$ g$Ìó,€đ†tč”zAsAÙ8Z/ ,ÇÓ~Ù`€j$"ź NB„QÁŃ9~rÎvÉ ßvŽŹ€t^čêÉÈMéWƒ«?ç“«¶Æ:œŃƒÔäÄ+ő­ŚY ĐĆyˆ»h”}m\_ÖëĆ2i ĂK0o)ĄŁűńüWŃWq- f§”é a8#űÚÁ(ŻŐuûȘRŠÜÓ^M…^M›‡Ż<~ő;q1úm=¶tđ'íìP‡ś§ìXéÈÉ»"ë)ÁÌŽé«v1§]ˆí,‚%ë„ĆR /Œ]Ô±·űû.Č՜J‡dœnVź­Ú]A/ÚȘż”ÔfȘš›+ż*!*Ž{Š»n‘c˖#ăˆLLĒ•jFFđ5†O)XߝÏçąšaeĂĐGm(ŹËÌŸ}MՒ‚fmČô.—Kè»6çXBžÁ„”ÉdɜKdœNÁ¶€đDqæńBĄoćXôu^$›†7§ÍŻ0û!-vôČș#č¶vg…śNääöăÿl(aìŠ=xÂí“ÏQ+ŰăÿȘa»c’éŚz~~Šs!»„3€Ż…Dk'ŸdÖLˆ€Ț‡‡‡ÿțûO#À8šțü&ڏ=r’rv*ł<©s€’Ćéϛ+‹…—,—K–cSĐXsà&ő)ÇÙSt;HHŒÛíl~uTÔ·öÀŠû§x)ٌ|<ǙӣŃh±X”+˜†é/ČæL ŠB,đ>,Ń]U֊šĂ+y%ĂL1‚›ÜNwțșĂłu,±xÁă\r~pFđ•cpN6,k”\.MpĆ©|\ÁŐlÇ:œóÛ!ôdç>”•…_K0‰ÿ‹5ŻŹ›ăńűéé©i†ĆŚę[ Ž6ŹĘöU߆ 2·gƒOϊŰ4 éî9 vOŒÇZÎqOOOL›æÒó‰xRì+Á/œÂĆßIźJđȘӆ™Q€± =ĆJ)Ëć’cű㈑ŒŒà Żƒö~°êÀ ŻÓßxÚòŠÓŚ#~b=;Nțl%3B€‡ĂáăăŁe] :<ȧ§'y-^(|źŸŃŸ#Í(.’uűŒF4G«öR’Ć˜ö:^TN«żLœD~Ż} Ҋ8ł2îY•Fó逿țê•Ź °ŠĂ l[Ł7n€ćréóùœ"•‰Á ÀŚ› ƒE‘rœ[@. úhJížg}üŐ;¶/*Są@ìSRkN€)Gę*óK)PȚápžZ­(T·m {ÆB KKšŒk·C/nĂ YdUƒfa•í”ț”P=ź//"ÜĆśzio.ĐËEœN§ÏÏÏČFq™Ś±RœE•8œm:œG°ç±”țBÁ‚»żć’ÄàŒà+]#nčŽÁ·bߋܶJö .Ź>ćXŐvN<ț/8l»ÈÛTW 'ƒÁ ïûńxŹ„‡ H‹Ÿ¶–ș™ˆŁ„Ÿ5WÍŸ±.łFÉ.ńb±ÀTáÂU}"îLżM͜ûì—ź`Xq€ÀŻ,Kć~ÈŸžC…ò}]K3€o9ąû’ÿ‹= §ßyÙcÖÚBS‚h˜\J‘ű~%\Ùę)îFkòÏđ€ÉdgÚl6èšéf’•čjæśC7@hÜíĆF>5n*nGP¶[áZÓÿ&țâłG2łÛăđÄNRÍĐP Àïé§ȘœIŹƉăńűçϟœÒĆbĄgŽè\î2€ŻŽ‹Œ2‚·rč»’Eß «ÒP'Ș.Vóőss”ű@ž „kÛÖL „;šGÛétJźŽ^"pđ&JĐż5{G€+Ű[Hve~1`˜fčȘ(ț‡ÍÉg Ìžœ ś[ôáf°žßżrNą8Üû™ôIŐÎSśŁŃ]ôzœnšæéé ĘFßśH±R•‘üÍ ùj)» Ű9Č.‚æÒ”ŽúzSÓàûę~6›Áq‰őz­ŒM 5`ô8h\‘Ű”Łń€6 Șjo`%•ë—ĆJT‚ËqBƒÀÇn·[À v†ÒŚÀ0Ɲ…Șq9Îß&«“đș‹š.§‚Ü¶-…đÍfƒôZ泍Â}ß?==рÇÿ~ÙÉÉHÎxôFsăëôZȘäČ8óɌY cĄô"‚aêÁûę^.îցƒ§7i>Ÿ·mëZŒ€–L5WD{2Őd7Đ·)ćÜĂȚèçŃ C161~vHpßśX›čقȚMB«łăűő!/żŽL8­țžî)_iCâ+Ö2Ű·­V«őz=™LžžžTÎóûmFòÍwŸ‘üO ŻËbŐȘxm6ŒćhB©ž©•Ʊ;ÙĘ„Ž_ȘjnÜeq0 3"ÿ\Žąn”ź|(œŽ:’ç]đ}űnO~Ż8Ó$uÓÙ±”BĘČt°išĘn‡%*g©„rég“`GCb ŸX,žCT ‘”2ž|ƈï{“S#ż»»ûùó'i6pzfÁ$ Ž‹3”22€ŻwYôYuÎüu2àŠèÀ–ÔòTÚ«KQÆSJÉJ9z¶m o3­JS“  %ŠÓoŠ’Çg€ŒÍ„fN‚W‹û€Țt]ÃĂŹűî“ÎU4/SŒ­qŠ X |„^Œ3‘αąŒÈ­îáé鉏†RÏ”Ÿ:ȚöUN+WčŒà«^«7ŚIÖU]Ukœ5à膑ăŽGY#æ5!żB“ĆŠI–•ï$nŃÎɏ:yĘÒêźćréŽÊ șÀV‹ÉH»§aïEÖșišę~4z2™‹ț$q–âDęŒ(Rș—IU*űÌa ńÙy8ˆkh’%ÎÀV1P bÊÈHŸöˆ űj9ú!D5…FWŻÓ\úÓ_\ Išą·‚ź”mkčzœ^ŁČÁN!"Ż8ęFGĂËžàżÇËă&¶\.”űvôžæž+łúłÙŹïûĆb$ăŚń©[IaXՕÓäG$ŻqŸÂ_#ÔڧۍÖ4 §lÚTËߒsxFp2à«3xVŒt±àìÒ'èR™7ˁêc)dÚ]Ăą©ĘqÌÿëUû€oț“«É [źSœő{Š%as­jÙàÄpÁÙlfϒ”Oۅ?oqzwq7F@ùŰ꜎3\Já¶éșźÍ=ŰtÎçsĐz4aËlXݱäÁ ÀWQŻqâIQJ”Ébž-2à VČŁ6N'”ČCĘ€ŰŸïmYv8ôW+Đ[m#šÌș“UŠ"źĘ·ąÈBƒIž%ۛ€KsÌG¶ÿ+ÏȚi?ęߟFJ>Z}žZ­˜Â “E vÎè\ÜmOôÍHŸvè-G‰ęqÿ~U 8î°Ù‹+xUŸ öDąffc<©ț ÖDIą2HQú{Ăkh%*Țž”sà‡oˆHìkdÀ‘Û.ü-jąŃkڝŸœï{zĐhŰąžœŃŚËćòááĘ<—žŒàkŃą±ń2`5šKĆÿU„uAl”Ę %ŹđÁŰLAÛÇ[Ò»mc7YSr˒Z°gœ^3•NŒ~M€0ì|đ‰ qœ+UŽ–돳ÄI_Nzz}ŻcšÓɗÜĘĘ=<<ÜĘʱ‡‹șè\ß2€Żzßk#Ÿ¶ƒ4ŻK5rŽČȘR/ˀeŽG鹉D‡5ËÜ ™đș\.aÀÿÈD9qÂ.XRˆÔ8}ßóșÙl`~`0d1ڔš”Š6<›Íl#–tIûz»Žß†Ù4€ŻŰąéú:‹Đ(È€Lžm–Ë„%9Ž0#űzA7șÎúT» /żÖ2Ż„«v.„ őù§ÿ—bÀŃe§€Ùl†BŐɉ6PńO ClÿS·%lŰ-ŽT'…”'6șĄéaÉ Œ4xæ GŁ43%‘”d)>Y‘ĘÆ1Y%ô,©î&ëî>==aÏb[ŒŽA7nőw>ŸßdßZFđmrŽ?2à+ÉbÙéț fÇfńïR X&GbFûűűž^Ż­VF Šó•œ3<æîîŽȚ|űő›3AÉ?ÇcI°śl˜:qÙÛuùXČMىt5hššDr:4ìwêe 7U3’ÿ,AáÈÿÒhčŃ_°ÇÇGöv9+`ßFŰÛææË Àß;\"öż*| %aك‹Łă۰ Æ Ș A_ÖA–EŰ đ ‹ˆŠŒì@k$ʚ:ęËËhÄ`§ÀbŐÀ°™g њădIǎS˜xï/śW±sž Û>©j”«‹€V Vcóș[ĂĂŻQŽN“0è«78ô7 uIt៊Ÿó¶ÉHț!ćuzZdÀ6ûó Ś Xћ^™+‡ÇČÈȘyÆkÀöò.‹źëîïïńï„ÇFč| +ë)ô—2^ș(Q(.ńVù/.ôzœ.„ “6ŁË9ÇmÛbí(ĂèY!îÆŃL±<\Íő{‘ëÛìk5‡CÛ…Űæk[łž‹ä œ7&ŸWléëÁ}Ę·eĘ7#ű;1`Aí’kË‡ w |JĄŃcĆÚc ű‚.%Ì­3 AaŽLŽká;—Ë%œ°YĂûʖQŒcÎèÛujÇVjĄfóęîŰTfYcŽ©i„|àêG«ÈÓ$s|ˆȘæ(ĆwV‚Ù1P·ŠȘ ·X ,—KäWDßÜŽe$W Ü”1àšUŃŰlčĐ{ÊK.ÈÛXÓÉBS±‡°G“ïĄDô͕ôƝMìźqłUU‹I3ÄÁJœOH§w;Ż$NNSÓ&răHSD>51GMu_àšü8žˆr·gŸĆ4éư4ĘMC\à€4Iä•däX„Œ’àÙlÆÉd‘Ő«2%æ-z~Xv€§ˆćŐĂÜê]ŚáÚĄüXa”$5æœ+€|Qâ‡Ă­ÍNGŁÍșȘŰ@hNmZoUô% -ńœf;ëŒàŒ?ăY ÎïŃ%@ŽsZ,_êhc-*·#•4‘Í~/Ž Š­ĐŃЖÊÿ:ČÆ˜r5Ÿcß+Lé»›N§±!Ű[Ę Jßśóù`&Jš¶Ä.óRÊv»•`RD1cQՃc&ډD”¶ß;"›S)‘@[ŸÎ;$#ű{3`“]%t.FÀcéqàŸy}ŁJ»N&G2”‹ĘkX€$=š« /MÓĐî ËÁJo0©˜śçÛö:%”Q‡Ăt:­Č&l(eÆÔ‰ŐšŐŠćŸeÌ^\/™”—{’ŻT@}.«>fa›)Üän+ ‡*fd$ăˆN“-‹ˆÏżÿe=űâƒąÁźF .2Š(qșTà€•ÒD/zÖ@a/oÎwžp»ƒŒĆ`EX1íklXò§üAĘ3ŹéúÈ ÚȘĐôŽGYČëQ<Ž2 żßĘdtÊÌKœ‘| Xxˆù[–ΉfŚĐS[ŽŸŽ.€ûB€7`ìzúhM*F9+VÆ0`<Œ8ò8°6㝧=ˆi J)oìSą7)f›ĆàècłÍü~Ꞁ±„ű4ù\Bś”ݧŽÓń—GłëDߌà›â ŠÂȎ2⻔Â?qșM“«X;ŸÏY°šŠaAğÇ«j‹”!ÍçsÖ}Noßśób±xxx€ŸsÂSYó±û6›­Ë±¶ÂțòùڐéFQtőúâd€rÌ~ŸÖƒc 8:gùj9YC(Ę*ż–“ó‚f$ßÔÚ§DŠBë¶|Ö&  HäâŽ* èŽF#çú5!źÍĆÂ4Ăl6»żżÇŰÈł„‘ 8ŚÙ/ž‹Jđ·ȘܛOŚ*L5»Ê—“‰Â~1îȘd’?țąjȚ  À7KÉßRL],0 ăYȚŻÁÂܝ„˜©~>È”ù8Âià[zŠáTÌ'Šę)pœ—`ò\9DÚő-8Lû«N‘žȘéŸHÊOż’ž›‘ü/Üńűùù™z*]˜L/ČŠ‹kŻ]ÖJR”ZˆÀŒm·œ6­ŠeH?…ę֚dÙܙùç‹€‚Œaޠџ|uXayiR’ŚŒàŒżŚ”R ș*ąíîźA€Ző©G[ ÆčÎSíńÓxmRĘ“DßkƒäsĐ4ÉkFpÆŚ‘UeJ “ÏŻm*mԶĚ\%učÂĘÎ܎4–űŠ eFFÆŐ-_•ì0ăcCŹ8Ű “©+±”Šń»[âšŃKmŠŐ–"Ń7###űŸ†ażrĐû­#ŽÊ‰œÙˌŒŒà„á_8OÈŚĐś„ȚŒŒŒàŒŒŒŒŒŒŒ#ËS‘‘‘‘‘‘œ‘‘‘‘‘‘œ‘‘‘‘‘‘‘œ‘‘‘‘‘‘œ‘‘‘‘‘‘‘œ‘‘‘‘‘‘œ‘‘‘‘‘‘‘œ‘‘‘‘‘‘œ‘‘‘‘‘‘‘œ‘‘‘‘‘‘œ‘‘‘‘‘‘œ‘‘‘‘‘‘‘œ‘‘‘‘‘‘œ‘‘‘‘‘‘‘œ‘‘‘‘‘‘œ‘‘‘‘‘‘‘œ‘‘‘‘‘‘œ‘‘‘‘‘‘‘œ‘‘‘‘‘‘œ‘‘‘‘‘‘œ‘‘‘‘‘‘‘œ‘‘‘‘‘‘œ‘‘‘‘‘‘‘œ‘‘‘‘‘‘œ‘‘‘‘‘‘‘œ‘‘‘‘‘‘œ‘‘‘‘‘‘‘œ‘‘‘‘‘‘œ‘‘‘‘‘‘œ‘‘‘‘‘‘‘œ‘‘‘‘‘‘œ‘‘‘‘‘‘‘œ‘‘‘‘‘‘œ‘‘‘‘‘‘‘œ‘‘‘‘‘qœ1ÎS‘ń»8ŸÆ Ÿfddd$gd| èBïóóó`0à= ;}MÎÈÈHÎÈx#Á_ŸŸŸàçççççgŸè·ŁŃÈŚ„ጌŒàŒŒł"ÂȘ݃Á`żß‡ű ƒÇ@ïp8‡ŁŃh<???FŁĂᐜ‘‘‘|I"EüË«°çá"'á”Ξ~Cd· l)…ŚĘn???ïś{żòĄ`œŁŃh:îśûétÊŚùŁù ddd$_`ŃŻŸțO-ÇàS!°ê ÎPZ~-Öú:yő2U°ÊkDYۈ î;N·ÛmÓ4~œ€,+###űëĄW †ĂaÄĄțű±PÊ ąG߆Iç€Ì šńBŠ*§xőPŸŸŸw»]À„”ív{8ü/~gßś€ŠùPĂáôćă}322€ż~|-!) ßȚąìäłJ:„ż*ŽV鱓ŻdÉRUPóp8l·[èx$Áțl€ż»Ęn»Ęò†ńgę…ć˜”Țívà+Šïácv]7:Ɨ‘țošʓóœźĘÍSˆàïźæŹă0Ë4ïysËš@(b cPF:JM&“ńx\ÎëŐ©Ò §Ő\+”ĐP€3ŸÊ†ùqÀ2nà”ț“Ì»ĘÎ\șéhÿîp8Üív“É„ ~GÉt>ńRF_Œ[Œf\-ŻšۄáàëșM„_  ÄÊČßïÇăńd2±GćÛĘŸ§"ažIćÁûę~łÙ‡źëÄ3čït:N§“^߈Tô4*a±. 1‡,ÿæ˜=Žșćűûęńűł1bnèÆ IDAT/]ę!—©[ąì™Đ{Ș!í[Šë†/˜xꑶž7‰•0œ|]74°ïûŸïáC0đíź'§ôú#$ńZ5Í˄;îv»Ífăg~~†ï6MĂ? żž§>ęÓlh8„§äRnÊ_ßïś >hÊW*4-'Ê,OÌ^Ű}ÉźŒ­ƒyf¶VăńۻВODܘŸ(aóò&©đŚ\” YOaŰçƧƒû™ËDZ++ ÀWDY\úŸ„úŸ'5:YtÜ?~= ź’ą.§*ćű(Fö—ŃȘÎÊkŚu@oßś]Śùy§Ó©É—ÚŚSńźÚ$ŽùÍ»căç`,śò ’`ț˜sć]1]ŽŰólêœùٓɀe€4àśMÓÌçóÉdÒ4 ` QűǑ˜‹ć.ŠÛƒ›Çs;™LŠÓéh4ò|ŸˆÁQ=śțŚùtŻü‰(ś;óà?đŽżű'ܰș^ę†ùćf,Ìfńđ\äúŸ|E x»Ęn·Û¶m7›BÙŠi€–æŻ9±Ä7Č:çp8ŒF#··Qžk#Ź„P$ÁRÌRJßśŒ>??w]ÇòÊÒFŁ1'Aüæš~śôș@ž›iۖßJÁ„­ŒĘ1˜đ,żVŻ#ûŸW'ž–Çc^aóÂlúÛ4 0Ì?ŁëҌç/ń­yłú&.ôÜ›ÍÆíŚEú;›Í€áÉd2™LܻͱÏ{=SŹîƒóú/<íyăăőcéÌ‹M%nŁcf(*Ic‡ÿćöȚívÓét6›ń\|ǂZđ 2`ښźë6›Ífł{À9ëę~ÿ«ó©Y±p[y9UŐĐAˆ“‡jƒÁ]ÈX[UxE˜û„;„Òéß=“3m6›őzœÙlÀàšŻæ3FoHŚ”SÆêŒ!+ÁÌ^K¶YЅ“ù*7qÊžït:­rџšżă^ń›_iƒźŽßà ‰5‚·©oȘKÙu]Û¶mÛz“đ 4MłÙlÈPŹ©úžN_OO‹šóúœ~Ă98śÊWu ÆE bĂï?€s,æœOSY±–;ă}äÙdŁba‹č\.yș«zpFđe˜„8äBw]WJ™L&<‡l·Û­ć“ś'ÖȘșfĆ EJ…ÊŠž\ ܞ—_UEƒÁ@è_c^Śż?ò+„RÒʈsì̉ÀϒœZ­ÀàÍfmTcĆóàïa—yđžÿŐ-ÒB5…M=«?H îFž•ąYöœßóQĐ{Zźû Æ,b<'ŻŽAW7Ą§hŹö%ŸžĂqçsÁ%LDÇă¶m§ÓézœŸ»»ƒàÖ wț“RqÓ7€Ü+€çF!&uÍśÆóæë‡ÒïŒGÛÇß¶zËŻ–pÖq(ę˜É[,‹Ć‚{̛ŸÛ/Ipđg„pă"xšíń–%őșÛí€ Ű›%R’Ĉ?ńžÔY\…_€čQöâVí4Ö}ă>œJUùŹ*펙«È8cçF>vr 2đçh”Læ‹Álk<슫ù*f«NGàÓyÉâ‚pÚ-&‚ œâ«_4ùf|à0†J&sȘz‹©ŽȘ/+&˱AëĆ6èsÎŐÍ?”êčżjgHl ˜äÛ5NŠĆ °g»*‹Ä#ŽlïŽ!> /~œ*sV5Úßm,⏌˜M‰ęxƒÁŸ–Ÿï=ïÆńxÌžă‡ô·ŠX©„č:ڊ*Íλë:vN űd2Ą ÄSĐ4Ín·ûȂZđż…»Q=k&­Zéb†‡;u}ŒÍfłZ­6› ˜{ĘX~ ÙÛkqCP™`œŠ…OÁžzä*zZč"ś}ïWű ćWÛ©űłąOìt:Ïçóù|±XPáӋăw©YŹ)šS æŻË*Ź#rŰòQà_űŒšŻŰMqzÌČŰêG!üb•œÿ8 kŁûtŹ&ŒbŽ©â,jčŁz›ÍÔ(걜É=„+…úçrFÙWíbÍ1ˆ”“É„ŁćȚ˜0ô‹fž-ôšaŽPdjúw‰ôśteÄMR”•ĆM•”ƒÓ*ŁwfŒ%ą)ÛițE·êxZ@Ś6îȘ{í6ț¶*ÁÀsaâ„Æ>íŒFÔ6ÒÜńxÌÏVŻ‘Wț-/ÒÍÓ3^Ő­€UÜš čiùßÙl6 æóùápà4B‚ßvHu`æŸăîÍs7ń>őʐ]H0g˜Ă6)ęâM•‘üvâ+PńȘ5R †Jât…ÁÜČ_ —°OˆK?É·Ši*ƒèżJŹUVƱÜXIp9ê™+oäX.eĆȘOW]OŐ{:—vgE­łÙŒW`8șQŸžX‹el2Ï|"«ł(ž”"ûŚ5gVšÌ1ł‚ˆÁźk•„oȘÒ]ÌZŹŽ*:°Ï€è#~‰U§ČŐ(Yg‡änÏï‰ eU@9Ę~y•ù°$-ž‚±ć2ʘ=Aù=Ì«°œAò™ç Lâœ[Ÿ3룧YôòûÒWF€ü.çtZńyQćdó›ĆEF›Í†ŚȘĘù‡ôÇ{ń™Š*ŠÄőÄšIŻŸïm)Œ:ŒlyOțÈĐr”Ê‘èÎ/W °·^ŻÛ¶}||D7$óKű+šŽ@ˆ7'ÖbòÙG"Š,xűYĘbĆŚ•ZZ·V”c–Č*vFș)ôZćąÎ óȚŒb•țc*‚ Znj=>&ӞŸJŸ\čOŸ2àž„PqWćêiYîï=„ăH“Húé=bĄŽ7±„ÿ;TŒ«ïŻ2./źïlV€ 6OìȚbąț›kw6ph"łlFăŚcȘ©ÊŁŸ"{œ›¶ •“†Žsžæi­=‚œ_V Zę< ŒȘD’Ym/țöÎ<°Óÿő•ër*țˆ ÍÊ- NȚ4Q”ô7ű]“Žê3 –=˜Ș—êvśńŁf ÀĄžˆ]”ÁoHŹĆV“˜6­t*öPyÒx~âs%ąû "–%– «f„|ő„wŁET,ț}c*Bi.^.—6źDIGXAoÔxWă]nÊŻrßsÛï/yxăQçF»Ś¶mŐîĆEś5n˜Șčâz|óúżÙSÄ9ŽX Ó5ȚŚŻôpŸ’`d±ôźçFßś_r¶1‘țœl>cëŽbLçsxk)żúbÊèłïWȘڍ]ŚhćxFtŻă{b§û_í§3€_~f\ëA\ sœ^ë%©«b,ŸhM@É ›‚.Ą‡[ŒiĄݧò~—X«Ź‰+©vE|<ąŹ4ČÛű;«âź}±ŹĄ§ÍŻ>Ă|Ő_€Đü|ćŽWDù˜™ >Âd2Y,Ëćrč\. m§\ÙŁh9țŃj·[„#‰œ›ć3möÔ„Gé >IÔ^©Ę{QŁÓ­Uæ¶*7Œ˜đ1'äJÍî§ò;éś&™ÍfŃJ©išőzMo{P•Û§Ê +OAÇ"ElŽ;9¶#_“‚>=<öÍÊ5ÌlY†ăù•%“œÇ[ >ÿêV2`a’Ć‹r2-‰àê`bùt]«f0(nŠüŹê/úŒ;P§‘Ÿ?>==ĄĘ#ïMÊȘzD„„ÿډ‹o”ČWÙű*Yćć§§âüš_Üùy3ŒA&ƒÇÍ«Ï„ûaŃU‰°ž?Û¶…à*qûÂłłX,xEŁđbrű3DXŃÏ<’*M7ùvHĘûÎfł»»»»»»ćrÙ4 œô7ű]+ yă§§§öŃőđLęiÒ2’ăh1óQÒÁHRŁ2Hș Ț[„‹ 6•ŐbôCŽÛ‹˜ŠŒĂ\šăòZ-”o+ĆüłțÏ6B€Á8€ĆȘŸÛßęĘŰÒS l°»)~ȚòikŐxsïęüùóǏ oÛ¶pŽH›žŸŸUÏZ&ˆIĘ8J( ÍȘŸ^ŁêŽ­üLL“ÂV9Ï/N›8G]B›“”ùét ŃošFăt+±†ę"/<_-ńmHö5Žm•ƒJŰ/Îf3Ą+ڌŒùțx`§(îe^ȘÜölż6› _çȘQ†cU™N§śśśśśś|J? À Àï-ŰèZ„ćĄÂ«ܒ]łJP0Ɲ‡0vČSŽłŒÊ cSŻûæÄš»ZÓJ87H‘§ČnÂe-Üș"sä0†rìMŠìkÚÊ€bć——Ú7<–f Ą†±{*žqkQ\:c7ŽÆL"ÚĂYòŸÓOă\¶aïűj”zzzâűëÓé”ËĄœ%Ś"^Ao/œŻUőá”ZEoŐ°TŽVûPUŹÂ•™ÌùTXe5)M]…+ú{Úž|ćFÔ°ÍÜRxòŠćÔ=<<,—Ë»»»Ši‹E9șށG\ŸbJ,ê$,úN§S> Țd<&|&”H œ!/€ßXÖï”Ș­èèćsӛ/>*ÀśŽ‚ß{P@ÄCÈ&ô=‰5œcâT”ÙšUöÛNìȘÂmŠÚŃcÖ}ŁžëcEÂ6)Z˜—Áæy­>ÚëżPâ­ÊŒÖÜć85r·Û5M#]ˆÛ‘eÀ±Œ9†·ŸwșžąÓVœ-]UęUÒčږ_û^NçGVEu_é ÿ8èéos6% ”ț Čr/Z}]­%&ąï{ú}ĘËB|—ËćęęęĘĘxŹ ëł­(«p\,QE 4["kŐżżżW=TáǙ‰”Š|ÇôoÄ]6æ#©ćG [ćWÖ)ùŽk}Ćû?Š ÆfÊĂQÌâòôž“çĐwÉyM\&‰Ù-a2 ;aUę —;[8¶nz<Ö»8d©ÒšEËßŰAtšŸšrŚU)±œxÀ™\‚“űšeWÙ­5ÔÊ"æEDčÂa Ü] ©ù[…ű'#ڏϟ+É c(Áíź*ĆeÄÏ<É Ż—{Aû}€?‰«".Ć6ł.€àžÿÊŻs*æÇțqœ^GŽŠˆ‚7ș!T óù\cż7'Öą!FlčQ U”êÆŽț.{…ÊRŰGșjü‚çÍÍ>l§Ą ź)ŻĐ_·V’i* jʝ^ÌJ ęŐÜCqxöá3șœêêBI«öĘĘÌCđ«.eEm_ì§:ę¶Óț„*ŠS Á‡Ü U+j5˜öÌÙGh~%NïàÉÒ] fà{]Țü±ăă/ü]©ÈȘ“â+qîìăŰæ‚”ü{đh[óÀ܂,|*P"Ă8}B(YÙe·Â:™LHFĄ$„ÜXœ{sb-ê°NŁWŁžÿ‘Ÿ•ÏìÀyęOëk/%Č—æW)}Á޶ Ń–zż`Lʑî‚Xę|ȘțÍ+ šĆ99šÒ':ežzP{r"€~Ûï–ű(,/™BœÎ™űššš­_ùćïœöGżŽ·$4U}YL šÆˆČÁÏ;€ŚÏŐ+¶nŹćšß¶3ąÊ„%ú&À=j6ŻiR.êqĐÄœÓpŽȘ Ź‹”Ú?Çôê}Ț–X«rÂU7NĆìÿȘPwÙ°/š>Q LÊI‡«9g ‰Ž's>•ueőÏÚXVšđI Źh4fŸk ŸŹŚśśśwwwđ' À1‘x:æœ":Q~æ$æÊ4Șü:”đónsÊżëÏyÛë‡d2xÀŁ »ù •Æfï掟wHȚ%—rÛçÜ•ƒ‚źcčŸËxꟓ7Cđ“`ÿ vf‘…áh=ÿÇŃ ŠQéŠÁ ͊+oäË&Óźjn„“K>E š8J™léf03ÄW±Uô܈ž*žŽÖƒčLŸÇ€Y©to+…¶źeĂćuFÂmgč*woĄŰ”ÿ-ŒófKțŠ›,ô°:—Pÿ;5úăÚêąêÙàW9"đęm·œ=’1DÊ]­ÈÜ`“H™„^pWO'aè$EqN„Ł*O~z},›ŚțŠìÌ(ûŁWQ.ˆWˀ•ûÉ}Ő·[Î`?çz’|Fp]í°Ž+űTœ"ÔËŃŻźC•8qû^ȘU±4âsŽÍ}ß?==œLh¶œ;Ú>`@Iž 0Á^Ü}źÚ~ìƒČ&í_çžÁ€"sÂWH«ńő܍<ăq æ'eS2€ż=—_%'ć#Š#ÖŸ8ț9țPŹAævűw\9pč%*džQV=ۍŽŐÔąÊ9«ÈÔqUŒ…áJîțyc*ìÜî5'o6Źc*%а2.ˆŸ±ìűHu$”ó5*ΌŒà†Ë»«Șq›í4&ŹbĘ7đé ŹFî€ÁÚu933s@\ЗŸŁ˜sæB€ŹjŹîîîÈ âŻ‚\.}Òu‰­SŃQÄ}âČívÛ¶­ZšKˆőï$UÇà8ę7ÎVKŻ]č„“Œd ÀŻÁđ;Ù[œ*c#]9:7•`ű— űô*Ä©D–?MQĂ·ŃˆJ*ž‹ò™uĐ&ˆ#‚ŠXèŐQO±Uć0ő©»·Łœ6wjíőzí°UìŽç5‹țșXÆgŁŻ`“1ŃA,"Œmyê2€? ?ŹçéźEÎ#5ÂÍòtœbê• >i@đ*æœĘútšæ'ΰÖëôÆ/ó¶UđŹÍ8+”^Đ]Ś9!n±Xà0lPÀÆ9„Ś\ÙżțFs3A_K\Mî=úÇžFy™2€??J0‘ĐźœOÿ7đ‹;˜j–mä;ƒRH±`­TZ‘s&íl7-S„+W„ŻŒŃTHłƞ»ˆw]çš.f{9Gł=N7›MÓ40c·RáÍ7òû ŹÔ6'xÆțr»Y-âÆ©áȚ{ŠpòÔe$î“Ń7&Ÿ•JÆ&Â+IŠ^ÏÙłŽVûWh˜őֈÓ|ûfčś ‰oÀńđ”FŻV+ŠXö}?›ÍąÓ!“vôù Û°Ÿ&{ŽÂçmÚJ˜Ś„#‡3Œś4ŸÏźší—ŹŁŸ›JT`ƒVÜ*Č]j™I‚ÿ}=ù°Yèè”’ț Àà8üźëșŒ8@âŰy’Ïg\ŚÊQĄæ UWșKjő'}Tíòyăőęp fӆ­bo±xÓśę|>Śz7sâšI{țI*=Ólțű,æêÉ{çœś6î‹9yLȚp*Mˆź2yê2€?z٧żŁlx©űê58ŸÏcÿIž=Ś©ȘLîûh%͜ß<>ò› â%țŒżèž'©0iÿAĐÙlƘB­Čl†EáÈdč2‰h2Ûh§ÙÄ`o’Dí̚ڝÿáuÔK”(ŁÊ+șű vpV9&ûŸŸL&Ü0]ŚaâĄ,K,-q»źkۖ»‹/ȏa"2.'ÎxsžŁ‡yŽ‹łGó€e$ÖgߖÍY)æ5MژgÏsÛ-“»șiód óWŐjʍz%ĆíèŒHR‹g”^łÁȃăĄá*րI„ÆíLWéÍf#EăMŚuX`ú#)ú+ôê•ƒnĄ‚”ŽàÎHțʀađŽiŠ(1?„Œ%ȚSŸÔ|lÏ?íq?…ú1UûÊòčÎHțjź0˜DtLú$;s)ŒŰPBÛ4gŒ\=…óźë4Yr^$»%TìÊŃ"*š*ë3%ÖÂ}yŁe ei†: kŠ™Śvêüàqž–[úd逘vlˀí”c ;ŃmX7săꙧ.#ű‹"öC>xől֌:ÛÌV•cŠOÿç8l•ÖŐ§§'V7:ždEô%C‰où5=«»rŹÇ œd)`6öÛŐęÎ€­Őž$]ÊtSÿüW»I-Ż>ËțŻN‰Ÿ À{PŸçtnĆ(ôŐ20ŸU©Cì‚5Û뗻Ęn>ŸÇ5.žœ9ĂáĐNÍRJì/†}["ĆJü9.ŸU/rčJ“Łš±r'QœSyyÆ9Ûî+OośË.-n€ræwFđeHp îŸUG Ë}EűòŒU(RŽ2ą!0ڶm=±«7Qđ\Žœ:ŒzâűaĄ:6O&“ÙlÆ ĂÙlæÈ‡ëœu·jŒă©šUSŹûl"Ąr2a·çœá”€ŒŒà/ )ŻĆĄ kŁ“ûŚt”^ùÒVB:ÔőKÂȘ ùp8t]7›Í8«$ę°Zäœ_©èo$."SUÊe|Đ©§Ê|>ŸÍfË撑ș€7źV_ă'-A:îÀÚ<5Őmeü-všTŒ…šSXWJœ‘|8ŃC1*œŁ¶(râÜ)GöæéŠIŒív[JYŻŚŹzJÇ_”_UżłzUá8L)ÎS"áL˜Íf‹ĆÂALȜë<‡æêùgŁ•_›Ș“ÿ-ŽȘ«Ű‰Ä Àù\g$_l§”žüS„9ä%čS–túć±ĘnYìƒfÈqôÔę‰Og*”àkA©~0đk©Ù# sꃟfŒ`úp#̉$»ÊčKPrü)z&7ۂăÿ:Æ#ŸëŒàK.‚– ć[%ÌĘ€s§\Â(eÁo6›Ù'-ć_&Iß IDATußsNZÄăXŒWQ…Á$)e°âËaÄăa[Àw^-kŒrœFȘ<ł*èrlXÊöü[TçgsŃqà`J˜ 8#ű’;ćSŽë:šQQœæ*h‡(“éuhŁ=Ź"ŽRd{[_©ŁÇz|)Ć6beê‚îh4jš†Æ_m7@\Țÿ•đ*–ȘŸúŒkínÏÏ ›$ vîgIxÛsíŒê g$_˜ǖ|ÿÉ7Dœ;ćdzçó9‰SH§ƒ ©ć8óü–ÿ[°p ©5"ÍUienüÌËu:êìâ\Ćσá $"!./™odúŻÂíT ÉçÓ ŸHœ'6#ű2;ćÓZQéă;ćžr‘ïÏçz=N§SXv€ T>EŽÓ6€òkƒ”ÚfÿpW’őYç_šètŠ}fŹżúw?©8Ö;ĉȀ§˜ ű Ï”©{”ŁŃh·Û‘Wš,?óčÎHŸ Ž”"“Ï>äŻR-Y4­ 8]äąq‹ôdÆóöbòùԈZL‹?łÊVŁ~U‘Śß.ĐÛp˜0\ŽF êPeö±—^íUi…śÚ‡ćæïoOl9Ê5ąiŒ”^ÍŰʐç-#ű’ ž*Ő+ÇDtIôŻ0Lf8ҋȘq«ąŻ[QÆŻW$Ű7qé|ç CG]­ŚëŸïÛ¶eŽ0Æ$ŰgłÙn·ÓÓăĂePΌrr”1iÒ7D4òŽQ;'*äcŁWFFđWGÔhènè=‡ùdžê”—c‰—Ő )ĄĄ«ê«ță0†èŒZÓęš”R_­ív»^ŻÛ¶}zzjÛ¶m[dPúétș\.—Ëćóó3ÿtđÎ{ ŽY܆ЍD/Q$öRçíwÎęéȚ…âEÓ4œ^*#ä6Œšyb3€/ÀYò"¶Y0ŽŁIü"+ÖŐÔÉćŹJ>Ÿ9Ž0žáhŠÿ±…ËĘś}ßśOOOmۂ‚ô;ĘĘĘ9Qq»ĘÎçóh4íQč‡8ĘIDUœȚÈ&ŻëșÍfĂd§Íf#WGŐ>&oŒó°­kL„‡}ß#VÀŻÔMU2àŒà  o„[eéőÈȘ8”è[ap̔ƝÊë„WqEŽ?ül{ŃqžÙlÚ¶}||\­V«ŐŠJđp8€€ÍžĂív{żÛ횩Ùív§±öt·Q9hșÛÛl6ëőzœ^ŻV«¶m7›6XšYś †3ÎĄż*ŠÓé|>Ê|:Ś©î'g$_8œ>ë›Hæbí-Ô©†èr>Q‹űú»ŚOĘ{9p‰ÒoÛ¶ëőșë:šTŰ»b·Û- ‹Á1{é.Ä2mœhTŃn“Ï`żŒZ­ ÁDbłuŠIßv[‚žŠôÙ?‘°ƒ)öçœÇŒàË0àò«őżâŰ8“'Κ!?™§îLęŽY„ȌŽŽ•o@źŐ4Íb±° 9Î:ŒŽ>úFĂçrT]À]Ś”m+ęĘl6Îw!œ$‘,ímč™8€M•ê]K5+M ÎHŸŸŸJMźŠ ő–"JŹ AÎ~ȘŃhŽZ­@h`.č©ĐÈ‚Œ‚đhșŽßïItÀüÎźëŰùQęć7;Íéj폯ƒm–ăz©ĂòòćčÍHŸđ6č„ óq#GTĂ2ŸûE7?I°m[ìŹíHör‹Ä$?șźłùJÀÖi$òĘJœe˜ś†p#œàŐë2ÓÉśIŃȚsĄ”ʈvĐî·źÙ-<#űŸ`Àqf;eĄrÌž!e)î6è/„\Ü4ïîîHoŒF#RÁŽ”€ËRFÛí–Î!üZè/?nIĂ ÁXeuœ ,ŸF ;›Í Ćtw^žż K^G8jűóqÎHŸ0rŽCôű žçœÎÔËűŸë2Ż“Éd±XŰo6çóùfł!K 덭h”o·Û-ˆëú-œąșÚçĆJ00ŹÌ) {ww·\.çó9SæŒpo{Æ«ĘsùuÌT>Ë À^ŽE_T6ĂᔀN„ˆ5ò\ĘÒșʘ…tâŠED*Y,ßÏŰc\;â†ìô}tU+G…éhgęêœ%#_.—qȘqâÄ;1űt“§4#űòè«ń/Ÿƒèh¶Û- û±ùÄ!”yȚn`Qæș›:f5ŸÏ»źC!“(&Wʇ8ȘAé)Ù=œIb–ÔjK2œÉ0àĆb1ŸÏçóyîü>†32€Ż€qRd•€1”z0œžišżs—qę+2ŰfÛæÈŁąI$C&űŠŽiZ–Fp=u9í Ž›L‹bÒâl6óf›Íf‹ĆBú›ù猌à›Ć`ÊœźPŸR ‰AVÆlŰżÉPßä€a,‚žB/ޘśV‚˱šJËŻ ÁqÜí€äh”ˆ»9ïS{•‘q՛ű˜ûÊx[D L§ ëŁ\$1űVoy-7ÊgŃöïjĄEÊč@”Ś–`ÌćWțŻÈ Dž| Æăc€ùFFFđż‚Á0'ł–ă\XéQąï?r3(ŒŠSÁ`pw"*Șf ["˒‘€Ùç69FÄĘì;ÏÈHț·–]ĂŻkù›èûȚȚŃ:nŚDßSCJû”" —ă„iösΰÙ7ŒàtÍ-Gkèÿâ#É5ńß -$ÍQ;ö˜Ś8ńô>‰ÖйȚ1„%SFFpF9=„č&fŒÆüÓáƒń~XŐœd8nïò6ËÈHÎÈÈűó.-úVśÔșÚÆéź• ›‘‘œ‘‘‘‘‘‘ń‘"ɌŒŒŒŒŒàŒŒŒŒŒŒàŒŒŒŒŒŒŒàŒŒŒŒŒŒàŒŒŒŒŒŒŒàŒŒŒŒŒŒàŒŒŒŒŒŒŒàŒŒŒŒŒŒàŒŒŒŒŒŒŒàŒŒŒŒŒŒàŒŒŒŒŒŒàŒŒŒŒŒŒŒàŒŒŒŒŒŒàŒŒŒŒŒŒŒàŒŒŒŒŒŒàŒŒŒŒŒŒŒàŒŒŒŒŒŒ«Œqž‚Œ+‰çççÿm ‡č5ÌÈÈHÎÈű荜0|Uq8|=Á`àkFFFpÆ7ƒȚçççĘnśżûr<}†/…žn‰üç`08…a.ń8Q9##8ăJŃWèő•ęùùy8 ÉÁ_șíápà•ŰïśƒÁ€«s8€Uß ‡ĂĂá0|‡ƒÁ ^»Á``œ‘‘œqEèö}ßś}ŚupÓ4Ó锶ŒÉű<Ük…^ź‹!*ŸÒßAq—Śâńh4Êsž‘‘œqaú»Ûív»]ßśëőșïûÍfłßïYŁ·Ûít:],Óé4RáŒÄʘ„šòûꞶĐ_y°?H.ZîËVÉà"^Aż?ÙpFFpƅ1Ü]­Vëőz”Z ÀËćrč\ȔÇßő.|ńȘ GXn·Ûíț}ßó†/ŠÍ1;?ŽfđUÄe'“É`0˜N§ŁŃˆŚńxŒÛíÆă±\9„ŒàŒŒŻŠż€ŸžžțüčÙlșź+„Ìf3`€őòôœH°ŐÓ zŻoÄÔívKb»Ę·Œéû~»ĘŸSÓU ÚÊî`0 ú âÊzÁăét: 1śûœü8ËĂqŻ–§"8#ă+bżßw]ڶíϟ?ÿÏÿù?«ŐȘïûRÊt:œżż MÓl6›Ùlöœö|ŽX1• ^|‘%œìïÁch1șt? Ș+ôqÏÏÏŁŃˆŚę~?ŸŸŸÇă1\Ž:vÊù“Éd45MĂ{Tæ7@…ÿAì‰5EææŒ€3ŸÁźùcŚôŚÛ=? ¶ÇX­VOOOđàÍfSJY.—‡Ăa<Ïçóù|Țś=•àëgÀ&ra™Ź­,Šæf«Š/><Î<™ÿ¶mÙőǐƒÓ–~AÜê6èșŽšî`0èû~8v]7 Ú¶uĂ1W«ž;=Æ|>ŸL&»ĘN0&5­PëyxœCL3À$(Ÿ$'g\#ôŸ(Ię-yL6Æ4㼏d8­“…Țn·„`>ŸCËXžŸĄ7‹»Ûíű'šŁáp(۔ )ÁTț͆ąûzœȚl6r_›ï$Œ„[/GtPÙívQ™ć>c2™l·Ûńx,ś}?™Læóùt:ÍfûęĄ;úæÁÆ|>©ˆš9w»FöȚDœ:ó\ú€3źz]#LŸ“]ń;ÁÚ€ŠŠ|PKźœ.Ûí0  P‡ĂĄ;†0vęlƏCF0nÇăńt:mšŠÛgż^Ó}ÙńŹV«¶m7› G^wjș€čâúŽŒù˜eȚ«Źæ+ ČșźFłÙl6›u]7›Í¶Û-Ì5q}“™*™źÚ7Ă]Ż™łôTÍ/˜8ÉHÎű=­D1qAÏâź6ÇpE˜N§lÉɉ}ÈúÈö_ŸnQ ]Śő};aź}ÍëČ„°†:ÇăńrčäłÌçóŻŻđIÍ9¶§§'2ÿìrű/ó&?U5—ĐeÄ'2GîZ-æë ._ÔÇăp8ô}?žÜHí,9ómMÓ|à=v dŚ'«»Țčó9 ȘȚž@ž|őŠ"âȚ7188ăÂË=ŒPë(@Œ|ÛâÎï3Ú¶eQšl1fł™›śŻ$*Ą‰,ÁÔ Ë1imŒ%éjŚ© ÌÒèû^* É»»»ół|`:á̋kË5‡gÊĄmÛxQ€_&B͂ZĂf[Š1–Äވ€jN›k= ű'ÌX¶›ŽV–oęș#wașÀ-è[ez83“É€i‡ćr9ű5r LΞęőI¶_SˆÚïś$ŹȚ°žł.ô}ß¶-[ –ƒźë‹uÙH”ßóY$Ù± FȘdU,ˆ+ wBmÛl?ț|zzÂZhĄȚÉûŃhDF‘„ö‹ șì&ÍÀ†ìB@›Š±hMÚC*Źßd9Șv«šEì!6Éá6 Țn·àw•ŻŽȚ–ŸșAùŒțl¶ž oò±äüÛ¶…À&œh›žÏç>Úî­Ł#ŚĂàŒ ­ŰA«@ÉŽU %șó1RlÛvœ^ÿűńÁX~•Ί5lIĐUăU €pŻpŃaK“ÏOOO?ț$»k^Ć/„\Âv»L&_PŰö%[pßőz ú‚…ŽêąU^,lfłÉŃ7’0^-…xs À xá›L&f€ȚÍf tÔ7PțđæïOR/*žˆÉçì?ąÎœsîR|Ńê„|ŸZè(WNŸ+apÆÖzžsĐ—ç–€»fwĐæ ÏYVXY) p?~ü #h·Û ‡C”26ÒL§S˜ń›99I>‘šÌBSBïŹEÄk»"2<ŃWe ˜aśÀ©›Íf°a€I_łiă8៎qçpYčİźĆb1›Í@âŠiFżF5_!J #üűÊME­w:ąÀlžÆă1gfłcòƒ;m2™|HÉü|őąßé.öw[%s„uïŃÌÄjÎiƒ5JCćolMʱț‚đ"êÒO]P2€3ŸšëŸÌąC%yC„áüß ú¶mûűűűăǏŸ?€Y"Á?H’ìíÍYh[MáČ~EImŹg›ùŒ67G1ÆÆsŒÒČ͛ÍČ㖂ú§&ą„eb€ĆÒ'äÀÚć1Ű%šż•űțNÍ„4™«$QmÄg‡yCÇ)ÓCÌŻlŒ-›ŠƒÍKż! Ç:™%ŽèëÆBł‘űêx.^«., œ +§šlĄ—ĘŸTí$śŒŸșrLűc䘜qáŸ'œ§ LšX4w»Ęù hșS`Àđp8\, oÛ¶wwwŹïYäńrțP(#Ë.šŻóś_|-ÌÛ–,éIűb,ˆN§ÓÏ^U%”@Y ê‘|Lk±XÌçó‡‡0 0śN§Û. 8§*Á_ù°ôțv]7™LÀàápÈ-Çś“°•kn·Ûûû{îs0űw,üü»ÂœHÔ]óÛȘßiœ0\âèŒí>ƒs˃‰œ‰{țbüU-Ïét:”ăË­OÌ<瘜qaú«šT”+ìd2™(ô8sËŹŒ‹_HƒŠBYć6“É$v‹Ÿs?Ń”[^‚őUl€č*Űl„íżT|K)Q<ìêoQp»ĘÚäóyÇÉIVo 'ă@sɊ?<<,—ËĆbA•Z)ŸÄśwÂÙG±u8Ș«pŒâwòșÙlÈŹ0öŠș$n¶•ä`I_S–¶NÈ?†}j(đÊŸ„ßàE“Č(ŹÒёúÇ;èk#8}h6©pŠ¶íŒ > âćè|n©Âr‰ŠDâàŒË„O>łșkœ•$Sg€íÿ!‘G…țČtn6›ĆbœȚI­ûB \ËXUYžÊŃżâ:-â!6.Vû,°Úą‚fÁ•-IÉ^|ÒG‹vKUÏ` NJ„_Wü7Źő13ŒzŽp"Ź()RQïì^ùN\H›ŠÁșR^n‘Œęöp ➕ț+vźžäß\8ÒžšêŸÌÉä°Í6»'łîkŸé°^Đ7ÎpDŹÇuáąüËvÙ ÀWŽîÛĘáȘêzêê`™êőÇ5ö5Ć%Ctg…'Üïż3k*càïjž!\±Ò‘‹öo]ö€‘lä#«qE`,,ÉĂű2«Ä§Î[Œ©WdÉnڐ:Ă·XńĄżU–Ž­ĂÖòÙ]ÙalÖ*©-Âü“.8ô0[L5ÿŻßńb!q@(g—}·l Jè}Ò\3ȚóțłȘăšȚà°+;ROÛŒÀŽÈ–Ó{f8$šŽâÄ\ €3Ÿ:ÿӞVò°R!òâŒ3óšëXćőśTè('xÛ!é ”Ł+–énӌ‘;^UțY•/L°ĐKÙMEđÊ~B aÁû3ŃŃ4MšĐYÉ,(ŰFéWĂяMȚ˜„7ïÊÀ†èœ%§€ Ă,òÀđìj©Œ+~'ËȘ©«1T†æ”Ï@KWąhțț_~§ƒ=ÉözìCŁèźżMuÈĆáúï1ŰÉHÎűHÖe]-úđ ‘mœIbűURjQœšŒÍ„ÁœáÛvâČ1ù )\Ś8SpdAęÁ«Â`>…Sü€;°7NŸ5laü„ő{ûnYm?i'Ö^ÛèŁâúîÿyUF«¶tU G•țČcS85śš,Ê_ż7bőáÿężÿáÿkï\ÛÒܖ%:cŒpuíÿÿ/w5Éù06ă”óE‚‚B\=?űš!òȚ˜ŐŐ]]Ìl°WtŐ_“ĘWÜòż ch›n:~ä]H,“aöMÁéü”ySœU\\ëdœ†vȘ.uo·†Žÿg5«ƒ¶*B…s Z7†ápę3ïh€ ó< °Á~g„‚î «*ŸUŐÒS ČòČőzíMäőPç,ˆ~(_—rs%Xș|›țĐK'œtȘjäoߟŃČć\ŹïßżŻŚë49ŚȘËcwAaWa±Uìîî„!Xźè‰öÏȈ_ĂĘÛń:ôÒjÔZ;<NŻźźŠÓ©ôŚç\Íc‹’p œwĄop­S2à€S]iÊčzÎĘ{z 8”eÈűĘœ#7Íl‚|‡ĘŽhä<ÁIAĐëŠvV 8%”ùÚOĆٰ:™LäsßżWy«-iU蝍XGf”%s%r—ÙOîuÉBŠOZŠÄA\ÈeÚD#™F‘óûțj5CșEŐ:”p ö‰pš©·êŰ|’x?,’Ś4fÇMŒ­d[ęĆhL{pm=șroAop­oúlCGęêűđg"qŸ«ph=Rm”ÎÒț掷|7 f‡ì9s(—ò)LœN‹mE’x]„0HÀi7AÌA9đááÁÒăŃ[’ÜĐSžž‚ ź«ç“18‘ŻEí_ûqô 9 ŸKVïyż4$QeÛÍX ib¶œŸœŽ§›=<Ű[©–ÊzŠń1Y‡ĄŸ#NîD-l8;UÛXgŽȚ*m ›S“QókÿĆlțÓęĆ|WçYżc_V@„&…žÈ6èÉ,™őËÜÊś\ê_żđvŒcVÚ ]vŽĘóL°…CjșŠȘÒÆK§p"ç„Xš_rC%©Āԑé_TævȘ+,Őƒ'œ_<'-úŽÌíïÓ ›^.Äv ±Z?Noooçóùd2!MêžÚ}S‡•<ۃ|­Ą ·Vđ_™Ö R=”ŸsX$9ŻÔÙì©$Ò-Ź”orÒ\焰ăàĆŐ§jPÉ‚ŸNaSœ’€w&5`.»=H‹t,N`ÁšŃç+«Ó„9S G9͹ǔuƒÈá3sr‹„LŒ«B@ •łł„·=Ÿöör śˆäÿśïßÇă1Üw>ŸÏf3.V?œ ĄÂ Š“Ï!;šł̂$…m/Sèûžg*…ßóÁQđ5Žeü—ÙæQŐtk}NȘ» „`x·–U*m-Çąy†É1}„”Òű‹©©s‹ÏwÉq9‚îëô…¶™Ÿ8™L:AśÖłbÀĂô{U¶šœYDìî_sŒŁśśˆÏŒ—?Ë=MŚÏìS:+łÏÚá!íy™7ʇVWją„źš<œÀXűZ«űß œ©ĂҏMSéÊ>ő°_±r`‹ÓlˆDÿÂßwjzN”˂â0IkĘŚù†Š»NNa’éo€űûC›:Ț3©e0€©Č 8Ʊ.ä„uoÈ<đqŐÇC*™Ó™ÒŠùŹÌ>B.E_#ÂtΒúSPÌUĘA” €kmgÀn(9›eÏsqꚀSÇK§‡EDĐQüΎä4PÌWˆ/cÉYxVhśAĐžÿeúÎí©O ŰűƒsT:Ț6rt鯠«h.ËźN Ű™Òq°ś„…‰J ŸEÿőÜFNë"Ž—ŠoæÙ5AúÖ*ź”eQœ†ÌĂlíž@žłêZL!„$f+8Íw SŠ•{\g”ë],6;aX!ńrÄ)qq±Z­Ò9áŹàĄCßT/§·”~ Ùó“êÙŹXênŃ ttČź\šëąÉßtéôŻń‘Iìcœ7‹L–9j«©U\ëĆJAržê‡»}s·ZÄĘVÊk9‡•êĄÈkÙæöÄÏFšCm4Z.—è­ì8’|Hȅ|öG™Ê0ÎÒ?’26g€dÊŹÒ{ÁÆßÛKîđn…ûŽù c2Ùù—ÄàĄ‚ÁžzĂ,ԞS«žVKŽ›ÚpgŃŒ» ŠHgrhÈ(žÈđÚUmm2Pó!rÌíj”bÖÛb± çŒäÊ <Œ—0Êúââ" źsrÀ9ÀCŽPÄQFBćőáväp_QЃŚÔłE Űđ舧™`? Ëșiö_wń+ßäÇÄAçTU«Vp­;Hf)e]m“u[ÉF‹ 8ÓŒ9FÒïd2&[ûšdÀż^.8ńóó3”Țćryww·^Ż‹Œ ”čYmyGő|öh“ÖŸŒ3”•gŰ»„=Ísșœ0Ü^¶Óäž@ śè žœŹ_Ž—ŸWy‘żd ŰOGŠ‘òă`üúÉĘD9ą•ÊșžÖ93àźˆŰQź%ÒĘQŒ‚gś#pńŚŻ_ÙœÊ7Ęôü V|Őp„ÏÆb± mȘ\z úŽÇc0ԙDśŐՕ$žŒ“Ăƒ|YIÌްN.• XœKäóǏ úĂöbŁcÀíšúŻsX9:°œœő›ăò ÿÜ}úÚËĄÂĂÀ”ÎnÉȘĄ4)Ôț‰ÄTá&-wI>“ˆN2'Ûna/ʰ@zMÏ:J=í‘8ĂÌv[sWŸüć áÓ„ț.eyÚ IDAT Î1g›[6“ŸŐD{pćŁ ±bäŃpȚîìNSÆ44ț Űá #?/]œàŁóX„€ÙÂŚ„*6\\ëŒVGĄÚFF›ÛhŠÔvP(—”wȚٌĄÏ­ŒA—NÈžY‘©^.—űl@™5„ìœŁŃh:"őâŻęțę[ïûaŰŃ>@üîK?ig[™9xŒĂhžNûÉŹ»ÿ:?Ç"úÖïŁlÁé#ЃŠwŒ f0|(üœz;O+ăË|Áû.~Šń‡ÿ”ă )X˜:Ç,G2Ś:GÜ1*?ąțž…2ś˜œĄìj’ùÚ}ŽĐ°Ž‹-ƒü3Égx!a>ÈJ]y<;ró?Ț‚wȘ` 1žŻĆüƒO™Ç™"Œ‚Öcæ%żoë(Ÿă2mĐ©~2)š0é}tƱžl6ËŠí čŸȘ©»ÔŹ>ÉĘGàM釡`0ÿÉçYÜŐĂU¶€ÿkpÖ*źu^ X݌Œ»%Ä»pnÄ٘ÁŠă$"8kșÏw‰J‡à:3˜YÂJóœ}œ_ÎÌf3Ű±‰yvÀóĄÀ'_vÈ _ u7ĘGȘÆšj“TwR*Ś’bŠóT‹Ö&NÓüÄá'›·Ûšœ·n”^âÓŚûì$"z5RëĐökÙŁSŸ;UŚĐ—4?eŽŽƒÀ!úbz“,ŒHpp­3bÀ-ìźČîŰb@òÔʛrÚJûûLÇAO;ĂKbùÌ9ŻŚkÒÈüšË•فÈ9;i•Yo,Őiö‰ȚRŒ)ęKéwńi7'‹©ˆ]U‹Ć‚ß(‡ßëæo"ZTêôˆÎ=:ws»«uš]ÈĆeÆ”+=|œ?„û©>ëRîLCô5ߓ_»€Š|e{9Û4=l|ïŹŽ››?ŃYȘ­ŻžÖčDń™4îèQÚ îđLÎm=7zKbÒËHŁu…4eGéŚwI9ŽŽőÀÒl6cÊÊgqÈÎàŽńDpKvób17ûGî‹ç—FÖwwwwwwšŠpŃŰ=95ŸÀ%Y7 Țóűűèˆ M”Č»ú@œ·[„ í”œțx`_ƒ‡ùžź•Ś'­iRÁžj M”2„”­€à&}ÚčÔi\* æ™çù “RòđŠŚ:Ł(Ÿ*—/È-u·gr7z6œ$ÓN=[ƒ‹ß/—KÇŠČËȞQ]QôÏçdžù„mÇ)=Í)Cš\âLč\.ĄŃnLŸ°+RèŠùăǏ?~PęĆ,óśïßDhÊfł»cànŸ ga䜄ٜEçĆ=J’Í—T`ùhć7ùŻùyńúÛ<ê„/|;ùjè)$ÓSg”cő—̐Â=Țćûśï6‘?>>âüʟ-ú[\ëì°ń{7+mÿččYwLțjjÎ>`XŹVőOOO0TJ•ì2Ș’"Ăq<3áœïIŐvJŸĄŒ- yŚĂĂ%äŐj5™LÖë”äòCݶŽWÖ}QwÓè̞ˑ˜yŠŒí€ŽJv«l‰vû¶ ÌÎNSÖQB·..];rΐΖ€łzòÛ@cü&‘T€űgęŸœ48sțŠÉœĐ ™"èúł*Üą”Ì~ÿ;oê§L=|mtÀ”ΝóĄ5ŸöÓŸçÜ܎țæ$[2܎łTfČ4;_”žűöíÛh4‚ûRśL&Brł}‡:Û4blQye»ŒččYo–âj]G>”țjä< FJpŃ[œdŚ 2:ú›)h ~ȘŠÛŠɐˆőôô<Îőóv›ùL S•}sÌž€aˆGŐyhì9 ۜJrߌ&ŽHâŽ<>>:#9őS©œÊ1”6wefšEuż_–‚ćÎ äkwcŚ*ț žk^DÀŒ6čjnêLí±ĘËËK˜nLù”F‹zóúúúçϟĐAëŸłÙ X‚oEŠn–b„8 Pó)țÔőőőÇ]j=7œ'Ł”ÉC‡țj*2Náś¶‘ ۰Æ)YöMÙè© À©-o›2ț˜ÀöiÏ€°;űkŠâS»4tÊtuI ?’`Ł:łÇ—ëőš”ző.oŻáhΏ‡*§˜‹żé‹ű$Xݱ:SźXÀ”Ύ§6Ò.[^À†ÒŻ1àźæśòłœ"ĐM’țm˜ŃEH3öĐw>ŸƒÄÀÒk=ŠÙ~*ATĂ"–ë™őA,Á7MÏgÄȚ†ôÉòooo ;ŒzŻĘÄï›E8ąș-wv„X‡ŸföčrŁœĘCBÙąQçžt¶œ”ƒu.Tê›ò±T ÀÇR_ZÄìŠżț 7ćʏÀȚj”úùóçrčä±ïL3’C{lVpZ4p;Xł»)Ÿk›ȚqŒh4„Q<_3! €k–%ŽÍ8dÀI#vš ĘŒ @fàĂ€“eJ“·†Ëł›Đq„ê @}mCÉÔ·.î°]eDœL&Šß?‚ˆ‚ŒÖWdŐRĄœBx5ŸÏ‰9đïܱoÊÛ8ӛ›$lzełĆŻV«étzœł#[ŽĐt05YGčȘ™IîzÍMÏpSBï ,"Ëôù}Ż7kŸéŚ-êž~”Œö˜łŻÌ€±aŽß>ù^«Ž ·0ć3kŻŒ!ȶ|Fއ3ƒRô·žÖ™2`Ÿp^cÀYÄŐ“Öœž Eă{i1ˆš[žˆÂo°žÎf3¶•™çŽJ”\AV2ûíî̶űŠĄÌ±Ó€äE¶]P–Ș6gȘòÙ­sǙʁôȘÌÛúŽÇăÔő"ˆ•~ÙĂÖŃ_€0-3‚Ÿç çFû2O»•€›jÓLçrш~fÈk{EĐ”ym4òyیA…ȚĄ|Ú‚û"芇sŚcmÁˆ#0|ÓđƒKô-ú[\돰iśYjÀ»°û 5-Óqv=Š 6ó„d\ɏF#»mö%ŚëŠmewæčƒ„ËËËńx Á„Ö+iă\HÌ3“‘ƒćÁ^"Ńw>ŸÏçsB ŒœvI ł j•Z;Uag<gÈD=#Țš«”ŠJ{9«àX—ŃŒzj 9_Ÿ}Ț™7m.Œ‘e!óùŃ1æ”Kș1Ûö“óK4gÎ6t3öIpS±hb9l$úÜß.Š%ÖäăCüšU‰VíżÀ”ΜçÌ>¶ïĘ 87 k«:ËÌägÊt…^ŰÊùxP°ŠÓéíí­:ŹÎòiÇ©±=‘‘›ÍfšźôÛÊt\N/8zÚt7„_úŽÔ»’öO_¶ÎÌŠú& „-ü’ì(5! äđČëëk€_:$ÔHnNY†œé3š„ëŻgê§L$èę$ő]8qbA*ÒÎEC4Ž“€ęt:őDÄłŚą‡Î‰SJšĄO‹Y]JYWWÈ1Á@ęÒM ŽxŚä[8"šćśț3O•y.źuŸ ŰT˜ȘšÿĘżPAoeÀ2aFŽĆ~aôíBw+‹UłÙŒ“Ê%{*€ÛÛ[ž/tp·i˜‚æ]FŁ$x:rÀ4M?îàdV N‡Ò/)h: ôßVłÊŃȁrÁ±KvEŒb HŁgRC}TlCŽăHJț`jî vmŁÚ=ÖÖßŃ_ÜI‰šl`ÓM%“œI|ÓaÊĂ[.—ŁŃHɱ•‹­;iłjŹ“pkĐ)væç.Ë]¶EȚÿâèI—áŻVêç+łÛŸ ·Vđ_À€3O˜rV ~wôwÔÄ­-#}ȘčjtÙ 0â@]ĆŒŒŒD2Ê+UO§ÓDßęI››)ć=x6Û·čÙ,~ĐÌŸççgό4Ÿ»»»żż_­V°Uó‡~#`›àíșWŽ˜pÀ#źày ŽԟN§æÛÛ#}Óź(«ÚNčgŽŽ'ȚÛóCœÁ™B…Ó™ëââĘSÂp ùUÊÇÌ(đ„XOOOŽížB—‚N”è›Ț)> ÆOÆyŒ€đ4EÖY:/O?ŒùO~ŹlgŰł™ȘVp­S2`őąéL›Ò\Qù”>`C5ûŸ*FÎ;i<»ibŽÁßG ô:gđò(6”_ż~iîNƒ‘ ŽÈ€el€]żśśśÿęïzÔ6Zb°PtùöíŠ(Œ~Č„U€æŚëőb±ÀÜôŐšk-Ë]ÂöçĆ7ZI`Ù'“cŽőžfƅ€:$8őeVÒęŃžÊÔBŠ‘}Z, ’đĄĘšŐH7 ;‚xìy€©˜˜ÈśjŃ)—e?#]ăP›x…;xź6ßZÀ%Îșobç nE˜“vűȚń°§˜RËʁßÛ¶‹MÉb„vúçÈçà%WWWNè˘CjxÄäłùÒĆbíób± }ê~J❅ŽùL8Őáߙ\Ÿ!­Í€+9Ą(GèÌÜÍAJ#ˆ6˜ŸĘ±GŒ[ 0{$ČRăŒÔëH‹ęzqq±\.onnV«±ÚkoșšœGÈï”L9dn† ÛÜĆàÔ'J;‹.SęÄÇ*ŽŚ*źő©KR•ÉÌÌȘč tà·u@n”ąo–”Œćùăöčbz„. €8ßœčÈ3T6ń”3'Êá6‡F΄’2òèțțžÁGXR[„Ö.Í1ÉïìżÚś“{tIA[ćÜ3éÊëß7*-„Ò,bۄ“„èŁäą»čO]ȚȚ3dɁŽŽxV—ËeÛLD0ĄZ\ŽJśŽk‘!ûijąƒ2Ę „đÇÌBs„»Ézęăíe›uí`” €ÿz æ#=ìèPՙÀ9dÀ™‹Ëj6WluᑑđOÂżŰÏŚĂÂfswk§ëƒŰ3CJ€Ì0ĐûăÇ,8ŰÙ9ÇŐju}}œ\.Ą¶x5*Ë%Ÿ™'OÛaÁđHّ},Vô™û”™’ŸYtCŁ â ą±ÌQż w[űByĄŒ‰ü2يŽbqî(>.‹Œ8ÜŁ|BșűŁóÙÎÁ –TŹżt ű”ëY«VđżŽțșĘXúr‹L‡=‘CT!șČ֎&2Ç%iŃ^VԒƒv©ĘÇšŐ̔f癰Ûöaž_§GóB›±wNeŚ\3…Ó‡ ïÿ>Ű™A.onnZžŽXÒæpöæ`ÔŒU- n/{Ì:+òöäąsDmlÔwAâőz­&À0ÔÌAvćčÌ”š©gÊ'”‡ærjĂ©U\ë2čs '(Xˆ5ęÚ&òûćÊęę­ Ïą)êÎΓôdp&ë±T»¶çȘEâM hìNŠeE~¶»č›ôgʐ‚>««««ćr9,ĂgŠ:™â[Ó :ucâ˜FJČ­ŻëJáöŚŻ_°aMÍötVi1˜Kźß™B«|6dÌ©3%yrÒĆ*Ɖ:™•ác2`MH2mbÜ(Ą¶šZÀ”¶äŸmáH;! ›#ôî<ùr8yçÉwÂ8ŁEćț6“#Í3€80È!}éeV ]Àèx!{ ƒÔv",ĘKűšźę Ș[këőz</‹‹‹ ÈwÛ8šuŠĄCą<Ícò{}» ˜b^†ß5™Uû&öß^1#ȘŒw©`Èٔ€"}çÎ*˜u|Ûá֙9ü.ŁĐAțÉțZÀ”Ά%Á)‚Š)ĄJ€pžäÀȘäÖâÏ_I_l±íŰj¶—rÀ9úWb j2o‡6Fa6Â|§Ą§+Őł9K ș‰ïżùńă‡=9fĄ­ú§{â[0쓈ÁŠ)Űă4߈é“äáŚë5ÎŁPmŐöÓHK…·ș”tú&KĂđ1iŽÌ˜—eIÂàl(~vĐBVćó ÿ˜ÏšU«űß œúwœ~ÙiŽŃa‚[+s)ŒJßÿìÜxmąË©òÏNFÒQÙi€ș…rUŐQˆe> 1 & Œ9Š Œ)łÍ»żkœâ±ÉȚŃh„­ńzœŸŒŒTŻK˜ŻĐćì~~+·MŸON_eńö"û­Cž°‡J ›°ÇÇGB"&sđyüÇa°**Kč[?#;8w{©*° pòjK­ZÀç»ÔăŽPš8Â( v(9»șŰű„|W‡Ûę‡ű~æthűQ1Ä΋“„YkȚïŁż VÌ Ż“—dÛäŸńÄÀœ›››îúl„Â[ÉńP„„}Šƒ}iîòj”â—€Tżă”ÓV,%{ŚŚŚűk…pęĄÂʕIÎC…‰š+C…)ćGßljÒÙ!fÏt^ŰæNА3ŽRdž$»DX” €kő{3P3`••öűăky<'œ+©uș‘úÒôÒ:Ő);—0+Čl‘đ6'ĐeïÊ[їÒ/ƓÎäížăńX ô*żÚʱvĘj„N]ÌùŒ”úöwƒŒ\ŻÈ ïh4‚ “ˆŠ-ÊÄżÿfôSZ?>>>N§SŐŃűi˜Ž>ÊhaZĐ n DZ ©]ȇaX†'űprsgQYè[«žVżSÛ#”ƒŐRÿénŸ”‰š+čő°§źCÚé"í%ÓË©ÁMÙa!ŠșŸuŻŚÌ’Ì3Žïńń‘Ż0`àȚ_7í7ÇʝSkvœYT|íŒ2?œăß±Œ0+fŸ“*zK•ŁZk2{;fFV8òđTŠŠo„GX„*”KïŐÌ*w)ćî1Öa»óEïœąkÛ©U\뀥q’F?țÒĚcÚ¶N¶„&`“ÌL|8–ŽòŽg ÀäÀ8§ÀŠëÈ;èŻ©TЗ‰żš‹!Ì!û-&4ŃńžĄ†òu ŸĄ K@„Ő6؍vXîüŒ{Î TàÆńœÇïšÂpŚPKB˜€4)zÄê$'Hg0·ŸïEÄŒHÆc@…[LńÊ • Ô 8űŠóŽîŠE՞S«žÖìH€dÀDô†ÿĘ&’Ä·›ÏêîŻÜ7ŽN{ÊnŁŒę9ĘÁżőPí;RUDŽŒ“ 7MGȞ-p~DšA„Ąç”°Ô1ƒOxNŁÊCąŐÂč$›ça±XÀ;)Ew&ŠńžZ­&“ Ș1†t š/tÛf”‘ĐËmn~üű±Z­Č5Ëb„fÎĘßŰo5§…b‹SÈ9%'ìŰ‘t7źVp­0àE/›«ŚœĄrŚÓß ó”MꞶœìГMßàœ~űI°ùgß{ač ï|>}2êxž8 ĆP,î‚]čà12idïQއ ˆç3NŽÈ„& “ŠhUó™*G!E9Ùb9—Q~ï UČЕfÍ«Ng63ă™č”A™ (u5à$úü˜–gɀțüy}}}†Đ«ž,Ç[ŐȚX\ë3 ”uéFÔéì“tyˆŸNL"=ÈÊÆDțĐašç3FÁQ’ű6Èșÿ^Öőb©æÍ„0„ßT>wÛú‘?u——š»ŸŸŸíGzxx0Û4ÂQDóâă–êí±6  ú”•rCŽQWWWhÇ ëÈÔč’æ*|Ž€ĘfŠ˜ƒšˆäÄrčŒżż§LÀïyÀxnÉ%r]űhœÆ~w 79]›·æ—g2’Á’Sv¶Íl+ÏźvÈàZû94o&NÈ ÍÄüÙäóQ?ö<”,ŸZ NWŹÍÜéęt\ްÌS€,‹Ă#« 4:ÈA*ÜZƒ4“Ó† Ûa•cB2] Š˜ä}É??<<èPFˆiŠÂáTóùœ,4‡7 Îș)9“JŒ'ž±ćšiWGGÜ|ö€ęg›2W ­@ÛÆr‡» ÿIɶ €k}Fڍƒf>«ú2¶hIîËlCüߍ¶șSyOŸœ6h◜ ·gvùϑ€exŽ őŐŠÛ8 ‡ |èÁQŠœĆ ÔÙ­„qŽÖ⠖OOOËćRđ¶áÍh:k$AiV փSń?iî æȚme“ÆX>ÛÜSN ą‰‚,Óû™æ=Jۗ ĂF)îČÀÌńŹ7ëáá!›à™OŐ%-j“,źő± ۍŒmڄłąäő”°?2§¶œŽƒ°Vś…ŻdzM‹ŸjtÙÜM>kö ÇÆÌ­ìÇ-țÌ…œjÈę:*œ‚yè/_• ĂWWWúXueànް­tp>±\} -O“ÉÄÙvĐmœ,€ńyÓn€1U܈·‰*à—ïŸÎਗ਼ƒÔHÚW­ v0w=rśśś< Œ’ÉTÿüóOŠń‹ŚúŰ„ŒÄéé3Ì€Öô”]#§Ê°­ËȘOnțüÉlÓŹßX ‡û‹œ íFŁŃŻ_żæó97}2wYÿŻÏ,Őóìi^‘š>xJ“”†ûȘ.NiÂ0œ“žÚF„eŽÿšPœ‡|:Rś…kJúڝÊüłśÔ‰Â!5fț5MNȚ*5Oš Ž;ÌÊ._“¶—ĐwNôU€vww‡-(OùÂî§§'kRÀÀ”>v”À Žæ3Äà4zëÈń. ÿډŃŚéOÀ› 5`[K?S ›Yhá‡ŃV ûf¶ăÓv^wyÛȕVFŁ»»;ŽÊɆžBÒ;OÀsN­ żÊ™BX…ȚÌ{ŁN‡űŠ%űŽ8)ÍÒŐžĄë<ŃTeƒ…6Síx ș±˜Ùڔą9‹ÍɃÛ&=ȚąCZóŃÇÇGûŻ ž9æËËËétŠ8îܚŠjÁĆç“ ę…}Ź:nôÚ<à6Pÿ«æ«ƒ•ęk«€'É©&R8°ˆú‚Ć~s°VŠ5€üÌ#ÌÙő\ŸTW9Íič\&ۈgÒh±Y:Q q\z„i¶„=è»OŃDúËëGŁ‘†ș‹»»;ûŸŠÓ©ÁĘàK+ńÎSČ·-§G€îűWĂżÿ^ŻŚf›ÓŸ€ml2 €^<Úűż)ÈçgÓíÀ”>œ!± ÉzOZ–Û9vóżá¶š…ÛùÓΊž$L݉ôàÔ%ăXȚwx‡%ÔWíÉdâTG•Y  /ûÄ0Ôivæ@èŃ`Y#űcâƄűR_WHŒZ­t;É&šÛÛ[‡LTOž-gK›MÌêÈD_ÛŠm:ÏAąžŸé›Ækg„tBËJ;ŚúlzägïêêJv«r?úă'óßüŃíjáCƒ­“B¶ÿ‡Ăë<8SĐ{ÂGQÁANòFúzÖYÓc2gÙ5'ńm­‘ Sdęwƒ°öL{è(ΌKGs’ő”h­ôz2™Đ<aáCÖȅsżÓÜ ¶[-ŐđCê_ÎK€ZæôJ˞…BâàZ' GGŸ<ț"œ”đLžC-< N °ÎÇńŽĄ~šđ˧Í?Zk)fNÄâŒÄ˜Œ:°nI8 C$h%ąŒ8 €k}uă«> ‰•˜ÚƒÛyŚÂƒ€ÏíùŃŹ Â)fNœjnąĘIDATŽz`¶ÎáIÎŽâȚސ“ Țڈ•Òś››Í‰[I:!Áè,1N»úN6I|ù‘Œ1`ÒŃYêΊx‹É›]‹0j"eÒäțùçŸù|N4çË7îŚ:#đșunvœïƒáż9>óI8éòsíæˆU|o2¶”†à™ŻIU-xçzô&_o›*‡Ç;—].—–uÖë5ől$ÓŹn&·.ĄžČÚšŽ}ŐŻ„ȘÜź$è% A0˜†íăńűööV­~}ê €k †Ïy_țKažògÒ±]핡™)ËÊɆêăRá%m=JyE—ÍüÌêž8B(,4”ß;*;!3EXmèÇnÍLŐ 6ț”™çLäŰ(òÌÀ-…pGP(ûšrp­Ú—kŐúÿÄ~jÎarőû|˜łÙ)qśđ§]hÏ·ËhÀ4Č_sLĄĄ€Ÿ]†ž‹čî|Żôż$‹œ+8űłœ”"sGa’7ól+Z=rÀ”jŐȘ”%@"„•έŸÖv" ]G’5XNd5ۜëü±äîÎÍk 8PJê”éŒ 状pŹ4RᕶɁÇ"±ÿTÏXp­Z”jœaeWłë4hûhV—,¶E,Q6=ž đtS—ß[üNżŰdźÖłÉ$ ÏÙԔÉłMê䫳­žąàZ”jŐ:KțÌl*”U5ÉÉn™Ź€Lè~ –ë‘bctțèŒKĂ2QŸÙű4fïzŸKí\\«V­Z_deŸOÛÌèt3Šš‘5ŠîÊÛB#„Ő†]'[9–;É;5kš‚uéśÎ«ČVp­Z”j}ț bŒŽN&< ‘C;ƒčSo„Z*3Ƃź•Z«Â‰Šü»mČĐù—Û OœVp­Z”j}èêôÉ@'R„is{™!ŚÄăâć’Kyye—@ȚjBčőję«Öÿkâ’Öß¶sIENDźB`‚mediascanner2-0.115/test/media/krillin.jpg000066400000000000000000001346561436755250000204650ustar00rootroot00000000000000ÿŰÿàJFIFHHÿá‹àExifII*  â "*(1 22R !"#i‡fâ bq Aquaris E4.5 HHbq software2016:01:22 19:28:42š‚t‚|"ˆ'ˆW0220„˜‘’ Ź’’ÿ ’ ’Ž 0100     Ä€€€Œ€ †@B 2016:01:22 18:28:422016:01:22 18:28:42 ^drdR980100HP(X€ˆHHÿŰÿàJFIFÿÛC  $ $ , $((((( ,0,(0$(((ÿÛC (((((((((((((((((((((((((((((((((((((((((((((((((((ÿÀ !ÿÄ ÿÄ”}!1AQa"q2‘Ą#B±ÁRŃđ$3br‚ %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™šąŁ€„Чš©ȘČłŽ”¶·žčșÂĂÄĆÆÇÈÉÊÒÓÔŐÖŚŰÙÚáâăäćæçèéêńòóôőöśűùúÿÄ ÿÄ”w!1AQaq"2B‘Ą±Á #3RđbrŃ $4á%ń&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰Š’“”•–—˜™šąŁ€„Чš©ȘČłŽ”¶·žčșÂĂÄĆÆÇÈÉÊÒÓÔŐÖŚŰÙÚâăäćæçèéêòóôőöśűùúÿÚ ?Äv,i›Nkç=‡š»3HÀ&{œv­!ihLŸ*čìńüÇ<ôâ«]°™őÍz”©ò#Ì«79ŃăXŠžčïItȚlBQŐ~𭑓‘„áû捖@y+oÄ0»żÓÌÒȘ8•LW8én?Ò7/\ւßLń©žRÀ.ܞÔȚ„7#ː]F7n?9ś©­ĘŁĂŻF95BLцU~8ś$ŃFX2¶TŽȚŽ  ąčçž ”$E”Ì6«'æhBœö»XOś v©àˆËń‡\0'„P=HÖ7‘ü¶éü/—.VĂ/zRŰÙJ8Kû±Æ~njĆ€‡0¶ĂÁÒ°^ìÓŽžŽ8ö€„șdŽ1Dâ7ŁêÇç8äRR `$‰È{Šiń˜Šțń2ŰêOJcp‘±à Á>ŽÈ"—ÌmÜ7qBÜżo ši•ț/Z„~#äńÀ„pebÁłŠ ŠdIrŸčrw­’ÀàˆœNyÍE¶%ŽFSvœÒa±wfhÙé_=łßd77ÇZŹ­ —ÍbpkÒĂRćWgŸˆ«wd[)Ÿ-Ąww\Ő@ȘÎcqƒê{ì9w)Ï$9àzÒ@żÏœ7>•]LȚ€°@ÖÓ$gŽiŚo4çb),{ « dAm`Ń?Ú'_ű «.±·EàŽqëL’hÀ˜łÈ?E òÛ˗$™Ą /E:ìS‘ÔTÖòmAp»Č8>”śPŽ$ąm»óԊŠXü¶b_#žś ]Î>aŃșÓâ„Ç8Ü€†è=Áœ JLß±śM0GÄaܟ0`ś€ UÊ<`ŸzàxÉdPê3֎ĄĐ’Úò@<ŒĘâ€R&rY°G\š@ś^LyłńÍ$Șêù Î9țȘH<Ż•ĘAÏQïëCDd(Æ8R 4NÄ=łN6ۜ‰H]Ür(h:ÍŠ»Hw'̜«€}Ș ]ŠpÀęĐ( ŠÓ†<wâÇOjeŹ’Hæ cêy$ô€Äő5€Úƒ$Ô"t ÜŽq^6“›»=šő9bGȘ žUčˆu0«AșDò‰úfœH«#̔źËhÁ!1‘ó)àPÍä.?ÚdÜkŹw ƒ‚@äŐâ(Ù„4K-iï…!Žr «NŃÿŻU©ă«šŻ Éx,‹€ĘOZˆ[]ź2€đÇ”;jMő`­Ÿö‡5,ÖHÀˆä8­0Ü-¶ùd±ù—łSŐÚ@W$°èsBۗhÚ'~||ȚőÌ ÊĘŰóEœb†YÊ9ò*ߑ$x¶Î[)h' ŃŒ)†ûÄö©%fE4Ăó6ÚÇMÆ.cêĂÇJK•YŰ4Kòçż­,v‘ÏZÁ.qqžô‡}$>f\1é‘ÒžȘsć3ÇčXc‡ú)ܐGÌzóS€q°ó ńŒÓbkuûĂïgȘő"Ł_ße;œOjLd“F‘’†éÁàÔ°|țjž.1œœI čBôŒ‰ò'©ĆVŽ&yG̓žO­ČĆíÓHá#“žŽ‘2€‹( ƒÔŐÉF‘±ŚVneËl#˜@dîzUMFŐ­fqÏ#ŠĘ±šݐIçÿŚVG•4g/Ž*‰)n+ 8à}î(žȚ9ìžGQT– Æ(H;OL±"©ÇńQԛ€RČy~gÊyÇ­Lm’T1,€9âšV"û+…ÚO R+Ë †e$Z:†Áv’C‰ăQŽŒ”\Žń‘"6áŚ4n#B Lçy$@}©$€;ù‘Š=»SŰoT#€­óŸû”bk€ ČŸ•łÈ DÆÌȚÆóĄQ$cæPy>őČ3ć‰ä'ŠvpȘŠ<·8ÏÌJrDbęë6ć8'“ܱŒÈRxr ’§Ț©ß»Û|êHÁùMHȚĂl rfFÎûЧŽŰ5Ȕ1-œă֘ȚĆŰôĐb"V9aÈ4ßĘZIöv Š~VŁpBOrM‘ǓٜûTsDĆËĄ!‰ËŒ~ŽŹíU—nòˆúUy„W‰˜œUTr~Ž0čB&óȀnțàŠę˜Hwźxûăҋ ă.ÆôqÇ„%œŒä$€z}kš¶hiçvRNŁĄ4·h·1««êpEP'ĄMČæn3ùS%œżÈ­À=é’Fûæ1žj{EóSïüËÆÓԏJŃ!\lÖćÈTl’x§2Œàၥî!ÒáŰ1‚ Sك4žd’€áqÔĐ!ś ž‰c<€*?+11‘žô=iŰ.,kDZÆ{`óš‡oÙɁÆQŸé#Ą [„$Ć&ÆB=HïZÊ|–‰äqÍ1܊kF`šyÏ=&ŸûŻĘÉ8<ĐБČm^іö̟—ïïU+ë”}ź9k˜ĐŃžlF–Ń€Mæ@zSb2E@ÿsȘVÔ›™ăp |ĂIíP»§ÏyÁ=­+ ,T’OćWí!ùpÌz{QĐ:—ÚŠ…ćóFvăöÇZϒ Ă{%{zS@IjČKș N7sžö)_Č2”9`î=}h`WčÀ̈˜ôțïÖ©˜šWówžć±ÒuŸ›gŽ 0?6OZK‹a ąá[‡#Țžì,f&J ŒE"™# žNkËpÈW _é“SÆR)—śŸ{§@VÔ`ò˜–<ÁȘYǝ·ÀŸQ`'Cś©í­L©lžckA_ˆs˜Póܞț•”śndf=sÓڐžäᙣP§æƒžŐ$i(}Ù8ț,Ső/ÛĆć~öh‰Főâ„·”[ˆŒ^W,p€ž”ÂÄ/f蠆ÆÓŚ=}šžÍ'LăŻńcĄ D¶ĐĆ#ywźFžh–$ŽQœ©è‹†ÌTŒĘbÜ6“†'ŚÖ–{Űe‰±·±4„„ÜÇon‘>I##wAíUæxŁ‘–91§Š3Š=†Gm$dÜĄ'­C<.#òă±țÀˆ‡ r{ś5fŃu0˜0ÀsŸ­!=ĂP”ŽČž$è*%™ž@Yˆ°číI”‹öa]w*î#±ïTnxn B·ȚúQpnÄšŃ͍xÇÜ'ŚÒ’$i°%˜e#ЄYò-ĄùżÖáöő"šÍŐf‰2§îäÒ[í€ÉÚGÊsÒ„čŸś‚BÆ8ąĂêdËÆûŽrzšŻ,-Ű;ò9Ź‘Ob[E‰w ›?/„ZłxäV”ƒŽ3L]ùO©#,f!ÈĘéY“.ÂBź9瞕vž‰ ˆÄ7–àśÏéW„›vۃäĂ7§„P\źłî}˜ïÀ>”Vê۱k†yä g&#<ÂŻÛ[·˜CGÁ^ í@ș–d–d>MĐÊšÈÁíN‚ââFù[k``“Û֟Q‰=Ûą>}Í:v|žw‰Ęț˜ žŁï,Ï~pHe*«-˩ܜH焜%·Ÿ{‰Öl>yŁ-È24g•ëïL °MŒ}X#p U«[DÜ!č9VÇÍïIÔŻ}uyŽNàv.śÂdĄëŽIŠ;ŽdVŒ a”äőæ—{…ó-œ ;±-šWk‘’ßÄxÚjŠ"ûQFcł±§œ)3NčCG ăóŠ\—™D‚2K.7ú@%­…Ab±#­-ƑU ü|ĂژŹIÆÖąXłś¶ŽE,C=Œć’èOz‘•.‡Êâxpϓózv5HNàcŽ3Ć7v©Yc1¶âOGeÈ$:òVHČœăVúț•4R—ÚíËc­RqDpmš,澓ê¶łhË/›ł†h’ |șȁŁ‘ïR$ÂTÁ0éŠv,LÜ/CĂbŹQq1} Ž:Òê+êUXÍ»ćŚuçùŐÈź#‘AŒà·Łš1e1Gś‡Ț„YžP„UrŁž:Žœ)ŒG„™~Đ\”$n4°čŽfùJ©?/”1ùÍp6cžśőȘZ«A$;‘ÎUę)%±Ž(Ȑđ>•„ÜeNòpÀ‚iˆžȚáźìÒ°Ú3o„@nÛp‰@9ï@6F.>f”n“Ž) šhˆŁ?­·šČ8•ÍÁqĐ fąÌ†_6%ù_ƒ‘ҁ‘êĄ”2ƒ»=i˜UÀi^>fyĄ‚$ƒU¶†Y#Ź …^ęi.̒xăyï@.Jo»ŽȚŽ«dÈ»ŸLüüÓK2—ˆoÊç”XŽh >p‡ÌV©lQĐćH¶˜%`2ß!öéQ)š ćă=œéÆŸÚÈWw~ĘéloŹŸs&ÿcI”liúȘJ@țì)ïR]ÜIæy¶ÒžÎÿ:·k{ÁȚŠ;€>ÜŐSŠ$ߥn”è­Ț+©R2=jÄö‘jQ‘Ÿ0/ÊÏÆ}šêR3n<=Ă!k:çÂLèSíĆ|œ,K‹>†„$DaÖì†Ț]GfăëU€ÔnDčž'è8ŻZŽ&2<ÊŰgĄ„eÔ[äčÜWŽ:֌+ÄFæ<äžúWj’häqh‹X‚)nŁŸńœV…–hLlŒŻRZz ĄË"Șí#8çšœ›Ô7q„@ÜȘD–­ŁY`1ó»g\t>•ȘÂĄÀV'ĐúP êAÀGäd7 ő« i™ s”Đ›hšêßkKłg'>”"BŃ&ç-€pp:{P)ș‘OP98íSA;G—ŸÆ9Eô4ۖ„W—A9žu;wsț”Næć-æóá‚@í@=K0Í€\Fä9^y©-íÌšś[ÙžćIçŠ-ąùxCI=jÌÊżÀ ûčlÓQùcž’6Q‚0 íDrÉżg‘N}1ց6RK ŒŒ2äÜRy)u ‚GAÁî) ËčŒM‰Ôoż<ûU™Ô„•sŒŒbô-iłŹ,ƒAÉŽą•˜-ÀĄŰàwő€>…«{v±•ZL°æ GăV..Ą–&<6äűÍEkƒùhăRÀŹjYu…ü«˜Ï–ĘlsGRŻĄźm‡u§-š°éšű{łë>˜ÉŒ~UNëĂVłg1őö­!ZQ"PR3nŒŃ QUN‰źiÁ–ȚbÊĂ75éŃÆ”čĂW žÂÚß=Č4:–žäŒ§ ~ [I.Džç…'ëRŻŁÎ«BQ-Ț …|űÈ`ǰéVôÛŰŹ`RpÁÎp?„ŚJg;DW·łZ\2€‡kò=ê”ôsûĆP2sÀÍQ6)"ùNwžȚzÙV{rŃœ‚z‘Hd›‹bR8ÏÍŽŐ„q,rB&ŽȘ@ÈgęȘb)ÜÛÍ<­*ÆJ“òš9Ć$bTE–F*čÚž<äSfÜ}Ą^;…Ëc“ÉŹï./šÙ“‚NŚ DvûŹe+(Üđ֍œdä1*ÿă€dȘ耐…ș”$qôȘí<Źcgă>”ś'© · !ÉÎKdńÖ„ž–E‘HŁćæ€#”żžYJNç!x4’yöìžV\ő~8ÉíHĄśVï:y 0«Œ{V|¶ÜÈnG$Ÿ4Äô#û,P§ï\ząŹéòÂçìӝ Uę8€2ÔWRJ‚,%ç=ȘșȚ<2˜‘sęíÆôóˆúôÀąT–ìČJTíÁÏéALëšœ|К-ÙzWÄ5së„0:~4,dœ‘šÉèQ:Ú$E#iŃ·i);‰ąŒȚ·˜` ?ZĄ{àKiÉ" =ĆuÓÄÊ ÊtŁ#"óÀ·Đm.îžEg¶—­Ű·úE±‘}S­zű||dìÏ:¶Ș. ž1ŰhÜ”žè*Ä[fŐ$Yž'ÖœXTŒŃçΜ ő+j|­ûђ3Œăœ6ÔÉ Ł+ʎ˜ëZ™—•À—Î1(ŠNĆY†Qo7‘$9V(cĐmIĆŹ“ vddà|ĘGҐiȚwË(`@8ôÏ­d“Z‘,Šwc={v€ÔlôícRFwíLEHž'Iƒ€{ÔÖs ÀŽqž~P:ĐÄó"áòwràyJŻv'’U0BUŐàĂ©Æ3MŃVpä†Űg îjW°bÉÁ#żb{P.„{°Û„ń·ÌŸx ·§Ț PźÓł=}L‰ÊƄG»œśšäŽ{YüèÔŒž#+HLƒVӂĭm†G-jŰnŒ ÀPx$Đ2őœHû2d‚N3Ć6kQq3;.Ÿ˜(é@E PșîŽqVŃ ÈÂpXœ©ÆN(nÛW‘OÎjőŸź§·ç_(Ž}Zw/Awop>lU„…|Œ+6źŠ&Kw"ž gæR*%'ŽăŒ—ì˜^•“LMŰO±ÆĂ™ŠIŁZJò‡ÓJR‹%ÈËÔŒcqđ/>Ճ{đäŁù¶Č猥†ÇJ3*”!U/t~۱–/8{qYîÏâê ÇBVœÊÈTG—[ (= ș}͜‹ČVÊž<ѱ֭5ŽRŁùČáănyÍw)&qžŽO ęąÉUYLˆÙȚNúTÌn ČŽqfC‡cÚŹ†gÜ;íó‰ÜÄüț”–ű†o-[śr ę)“Ô­Șiïf ÌŸ'œš­m)p ƒÜĐ Ÿ%W_”Ûܝùù—֔–¶“À/m·+c çqŠICRł{IHăr[Œ`Őș-!Y$Ÿœé@șŒ;wî+Œ69Í6 uŠ}›ŰÇ!çm1ÜŚŽ8æ€+%xÈ„hțGłüßcÏJ@BmŸÍ'•q»aëÏZ«}o+NÛÎîP•ÆG­0Ÿ€g–Űy<2>Oœ^”žO,̀ŒHíț5#šKÈé•ÉÎ:àŐEŸ+ușY ńËcô .ZÈ5:É‘ÈŻ’q>© žDć\ę*í¶«p‡æČq.敊¶†8>ő~ J)ƒê*uL{ŁBŰÚÊ2ź*ȁ±òÓöq’ĐÆMŠ&R€Ž A$~5”©4fä1íùʜÓŚ'`âÓ)LôŰßædȘWŸłž\In§>Ő€*JBù”·0”/‡¶NKÀ†6ÿfłnŒ-źZ·™ ŠAßu{8l{ZHæŻ…Œ•ĐŰgó,¶F,’yÚ­Ę;\ÆĐÀŸ&s”è Żn•EQ]=ZnœŹÈŁ‹MÓűżˆÈČ©1€qŽûę*ŒÀ+ŸóÆ3ÇzÜƓ۫_ÛÈgŒ°QÈ&«Cd–óyWÊRG#šdôŹz{±Œn N§©Èíćžù†yôÍ=ÉkSć‹h‚°#q·^ÀV]픶’mqŒóœqŠmKvVËsˆáw­ț5~ÒßN¶Ęo0ó (ÚßĘ4„ŽÜXH7§Vê{ •íÖáB«|êÙVĘÇҀèE<-{l°;5{âśȘ“ÙM}ÛÎû^>'Ż”b4Čł¶‚HÀ‚ pO”@ŃlD”ƒžV“k ÊÀĄÙŒgȚš]•¶°Á_J,3F7ÆC~UaE|ÍJR‰ôĐš„±4|jdő°±Ș&‰ÛŁUˆn^#ĂÎJ쫗­”'RzÒ”Ś1–5–±bk™–ÚâÈ0à0«¶ŚZ}ÆÉSï]©i#–„9GbÓiqș7¶j7Č»ŒeĐûńNtWF §qŠßĂšš§”#&ž%ră=Jïmžć…DöŠx+Wît)‹aąiČßȘß[#ÆùxźgUÒ Ó5k*ÒèŹ%łǧôŻ/©ucÏÆ%'r­Łł”Æ0łĂćÉÔduéV"ò„¶2d+/ 㭞ǖçS’D˜Àì1íIÄvá`$%șP·ŒìŸi¶?4g2őš^o;śˆ™Ô§LPÁèęe…ŠÆòÀ <Ìqì+&ùś\ł"mÉçšZ‰Œ“ω˜Čp-6’H>Ë2ËedaÍ €6+àœŠ»fÆă择x9źZ”#4uSŻ(2XîeBVEéïV žG]ę9Ç5ćWÂJ.èôèâŁ=f9ŒŠk‚Qi±’cÂȶsVb•Śž ÍÆćܞ;ŠSÆź[ꌞłőʚ`ő4ŹőÏ/HWß5­eâ9žaïZÒ­(=NJÔŐ—Pł˜üĂëJÖÖҜ€Àț5șäȘqÚPb6ìő§ÛèÖì7I“[QĂ)ÎÁ*­";ëlŚÏVûŒ­qîÛ­IćàČ ț5ф‡łÄŽ*žÊÎ$T{Šù@l=Áą'xmĆńUVc„\ôˆúWŽ9”ïá’,’«+gÌœÿęTËy˜EćŽ IûȚ†š‚ˆ€a!Œ*w?ßÁïTź·Œß&9ä•=Ș„Ë1yw-Ôo”Đć—ÚźP.ЕڛYHàžônMÊș” "$‘!ÏŻmż…6ÍuА è Ć0êMö)àžŽȚHś7F sűT:ȘE/›’AÉÇLÿ…0 GiÔ rUXbBǏz5€r3$EËaU±ÆZL.6î&óRâá$t8ü*ŸĄ§#Âډ@ȘͅU9Áś„pEč)›gPÊAëڈȖ/ČŒ9|üŻ»éŚ4‡čZ€ óŒńW,€U‹tpÊr[=EIeë›đ‹ÍÏó•#­V`Ss”œM&“*- ”{„›o™·xâ”]î!‡Í%X/S»’=k†Ÿ3ŰíŁŠ”^€Ö7bćT€~nŸZ”V?#ŒękÉ­‡&z”ëFą%019Ć*ŠȚk•üFśĐUšHΚło~éüT„Ü»Ż"őcùŐšuźsæœYœ©©ÚüĆv‰łő«1k2ÄÖuśźșućs’t_WÖ⠑Èçë\UőɏPÊÇčźŒ$ùë\аćŁaæîȚűšĂ#Œš=>”fâ3 Â2& Ç85ížK(€đË;ZÉjźÛH. é@†ê0Rx¶ù,<”<’O@=jŹC%{dŐ­?B­‰€ôÏ\Vłkk—Ónś91í«DœˆVÖm6è3‘Ž r+LȘ@ÔG Ž7§Ż”ą6¶$I#•œ0±Ó&™t°Ăpp0c W4ÆIöÿô…bqÆ7Š#șó["xÊś€u)KŰ\•°ÇæÇJ·ŽŹۀ„üŠZbŻùddȘ.{Ő!pmä1ÊĆą'𖆶)Üm‘Ùb]«ŽęEU’ày^XÔRcCâ“k˞qÆjhȒ© TnÁ éQÔŁvÚ Ú5ŽȘĘr8=*’YÊÖÌÊJdÓ(«4·“6ߝÎÀu§ŰŒó)ŽY[ć道͉ôï&&bYvž†›wö›5Ì289æł©N3Vf°©(= ôÿȚ© ràV«mŹĂy8€G†#zó+à5ŒOB–3Ł-Eöy˜…o˜u!”=AŻ2tç ÏB5#% žS/­Húgóš±w,À’7 ‘S‡X.IÍW.—3{’KŒysjçuxŁ’ëdŽ3]XâűŸáČHŃć6„Ûpü\Ő»icŐí’DˆÇ€…ęr>júx„?ÙÀ+ùàęŁ8sžÿőąæÖFAyrH`~TĆY V»’ÎXïüł":æDÿ>ô·–Đß©uÚ»Á1°ÁÚ:íúŐ"Lš­fÔ-ńŸĘđäőÇź)°Kö`֒ł1$ìcëô H[e Ż™ČRÀ.Oo§­BđÏ0‘đ—vÉfš}AêC»“ć4›¶Ž”5šó#(v©ÁÏ<Đ&·łK‡1]­łśDś€·Ö‘ČœFš\/ĐÌæY™T’O랔Œ—–ìUFŃÓ#'8ëHe…°Ö8ÌŹp1ȚČ^"“ì, ‚Ț”šm_Ù[ęĄțɍ ’˜Š[Ăœ<Æo»Śž”™Ą„asq%źï61ć‘ò±Á?J[ÍŠćn•đ_ïȟÏ šê8tÛf0Lr:{Ő8[yD° 0öí@žÊőÔ\œóùU›«T’5Œs¶7N…śÿJchÊxäŠBzü”vȚ2š·1 ź€uläúĐMÙź“ĄPŒ&%źê±Ș^\[*ĘĆ ŽłXUĂÂȘÔڞ"pcl”Ł)),v2»GZ»i{euÔí9ï^]lĄ±êRĆF{›Zu‚HčQœÔú†˜ÖŹ„ŽąČ&©š*‰Ì«w‘Òč-râčò󾑦ĐŐà`ę©–.IS␠˜ĐÇ"(ÜIÆyÀĆ=Dvś ”<°¶ì‘^êÒDK€fŒ˜óóVČÛÏ–»zŻî‡CŸóš…±QsÙäڋ;˜ŽIíOș† #Yâ đÈ:ęh>—ö‚C*©S–|r=>ŽgŹ7kfïæŁsF:ô š†’Ò3,Ж’>\ŠúdS--mćŸÌžc .IvÖÖ/ipçä'ìjć…»ÜĂ=–„òàț”UÉhĄšZÿeÏäô(8p~đ§YÚ-ӉpTëëC”‡ÜômÊìۘšÜȚĆZŒ°†ô)#ĆsΜdŹtÆ€ąîrȚ1† 0ĆrKśsÍqw¶;yȘčBy>őtpńŠźŒ«W•Gb֗LpÈ ‘/†AïPĘù&òH|Đń–;yÀúVŠ-“é6áçkS&w)P3Æ ^™mtöû#@…ăDŰÈqïï@șź€EŒk~`§„ç~OÖ« t›Èxq"’Txö«%/ę‹*§ïç8È?Îłì-æ¶»óŸc|§i Ò>|ò/Ì7pĘpi‹=Ä2òÁYŚn æ˜ÜÙŽ%e‰@LȘ™ă=đ+2ú Ą”…pÀç;Aăœu€&ˆí‹Ć"Ćs`ǎ{ÓÌ-Ț# ©ïLo­Ą…ăŸ…ś+·ïž>•ĄmÔí1ž1/’0~^zʀ%Xš+—P‹L§æ#šöòȘ:Ÿ›\–ö矟=jJOBȚš~ŃgꞎUĂ}ô_YËnÏÌí<ûV&Ăb‚/-ŒË–û§§œ]·S(a‹>hôÏS@Ń+ÂmeûmłG60jtŽS"\îVRž%űÆ}hm­’Ed Júžki<àń«nÊŰSÄ7·î’ă*H y8ő^I„-öy-‚°c–=qô E€Ńá^üŒ{VŒsYĂ„aÀóÆr>ńô4‡uqwŻ_}ĄâsÀQ€nèšY]±ü42ÔNÎÒìÛLJùU'ÒČőŻ[y;,ź]XÿZ•ž3ŒÔîæ’ć˜č'’yÍC5ϟșÖ8r\|˜^A­șĘHm –kiY0’ÌÍÛĐzš†ÌDUąžÎÖRcńŸsJàś&2Ž‘/’Ș’ ûàő©e’kŠrÀí_—遟jrÔvÿ œ‚~|„>àúöȘš…ž"îÜŸ[•-ÆȘBd&Úâ7 í€Ę…-î‹#ČdPN|ÜóŸJ}IEkxŁA$Z”çr`"íëőą+š`•Ł˜­śíéL: -őЉí[k|ÙÁQšI!Kû)~äÆčˆ8êûŸęi1Y%š_)Ûç^„žŐ`ÎaÊnߐ0ÛjŹGÇomlcyŚÌ†oő©ÜcùTĘ\èw/ €„+6Tő·ő„ž2ăĆìFővòrB?ĘozmŒśFæ;wșì} €Ôłqi.x“˜‚‡9Q»<{ŐíJÚE+sŽ8 gÜbčú%(­S{À/lžąŹfÓÛʁćQ ÁsÆćôúSæ…œœRZ=»:óÈ9Î[҄ȚÚ=ƒ|yä {žô€DŽSkçK#sÏô©ìù€,ÛWïò?J`I ’»î1oèGæ›uc%ÄŠi—<łcôȘZ“qöškÍ2ŽÈ n…Žÿá’é<œ€!9Àî}iŰIąę—‚<šV=ÁFr}krĂK”°@"\‘üF„œ .îKwžĘâXW3{Š]ź,Ú<źIéI+Ą7c&}yÒ,»O\T2XÌ$’óŒ/ÊWĄ«fF-äÒntdùČCÒš6KmL@qžÿőÓ$œĆl±‡†\2ŒH u>Ôôheù ÜČ~ő1QÌńî ̑«őeüăL’9ÒUŽăxÉ”lcŽGăHfućń¶VY·ynJąöÆj=0]G)K™Jä^zÆ«ĄmjÙąUÔ„ó “) ž}sP^ž…n-‰? ÎWĄôÎ*‘Ä·“1TŒ™æ:M}=É{‰|”'ćțđ”ÀŠòÎÂXÍÄLævÀUëß5Z4/ÎÀ`á·zĐ ’ïIɕßÀ9äšÓû.âȚ«ü’Ï=Ÿ”–àEe~¶č±tA±‰,ßĆíWd”’òq{€ÎAőuB[YoŃ­WË.ƒw™ŒdNzÔö+ö‹Vȓ#o%‚eűW1Ò·+\ÙŹ#ŰwŸćAxî‘„–V ˜Âçš cȚ9­Ęf+'#r銶Ż5Š /Û+  TőĆ "šÔ+AŽ_•™Ś§·5RæáâÔ ĄWé lšćìšę§qČ0zÔ^NžșYHçæđMRdČćŸ€âuތ»r+oIńT«”d–'O„iL€tșvŻ ò€~Wô«•Œ•™Ž]ĐÉî"·C$π;ŐFŐŽÉc3ÈŠšĆîLćĐÍșșÓ#}ò°Ă’1žő—z4äIPN[tmÜûU”s4ʆÎÌÂłG[‹üÒ7P}}ëR”Ă-‡Fă·ăKš™ óŁćmŒpćzŐû7¶Eû2ÆK¶v1o»š`Ioyg)6Ăæml)ŸsÜS,>ÍwrZi%ùIä ę #Ô?łć…ËÈŻ+uÁSííTÒ$Qä+• rš!š1ïÔí^$“Ë…ÔʆNr#ŸZ©Çöu»i’Äț\ŁÄqÏQMMfhç;2ÈÜäŽÀv© žĂ-ì.z2“T*ÙZËmă %›9À>•ZóG‘•Żmäós–àŸoš ž$x1:wêwUšç[˜JDżÀȚąŽąylŚh.ą€ù‘àH9>ż„[Ò”ž·Ú@a Ś,O­&4t:”Źv—1ÍP’`0pPjź`ŽÔœFê8PyʟZä[c5o”Úʖ#xVœđER·‹eàLo Ő ÇZhOqÓ_Țź-|ÜȘ»O±­­eÔì<š%ćAeŠïJb*fbî€(!~pxü©ĆY‰• Žg –Îăôì1T¶IŸxíËl†ăiœÔ~t6·Ż °†Y » śŠMÇGk9|ł…R?­Oaq$MæÇśÇ ÈĆRdłkJÔȚ)$ƒÔWÿá2{TÚì”VæBRćdwȚ9łžȚDĂ•ƒUŽŻiÏzłMoû”BvšÎ­JM+'©BÿW¶Ôïçž(È?v§”C ML.q2gËă֛v3ê$w<™€t|1íYZœÁŒsrI,I à:Rê\NÏ!ŒœxÉèkfÆ9gҞDŒŹ°7ÎsŐHïTÚÆ‚ßí1Jƒsc Ć6țö{§SŠ«`šă'Ÿ1@Œ }† ÎA ‘’sŚ9šom%Č1I”ŒŐ]Òô<ЁąhnR4óáÚ!fęâ°éíK{l5K0-‚“.‡wđśÉFmÁ7îń9Vš5<«pțuŸhá.|©ÊzóT‰4á†;ăò,‘ȘGlà{Š™’8ĂÛdàïÖ YR<¶‘O€ł$ïï-·dßńÉŠO–EntùJ€«ó0íê(C; wJ»ßč.Ë1çnîęű>őŸž{Pî« `ŰŚ":Țć­>wÔaˆŚr°Ęžä“ëTçYeŸ&H6±;B Ć5 =‰”-NŽL­ăI#sÛb’Țy%òÜ\0q­P‡ß ‚țÙT‰žÎÖ?_zjùÚ|Ï ÜxóąÚHÆGâeI­%#iĘg'šőâ„șÓÚhĆÛ\yŽFí±Œăü*‰e»XÀ·Š…ĘFNOééQÜE{ ÊN°ó:1š fœw(!û1ڎ $ìÉ-žƒ Íș]Ç._çŒgƒÖź.ÄI\eź€/!•|ìžLȘç‘Æyü*…źÛ9C$„q•rŁ$֝î%žÚ r—ś;†Đ_„Z‚p"ŽH ü͂6äçźj^Ł"Ôm„‘P‰‰űeÏĘȘí\Zł[:‡#ŻȘúûR„Ę-mź$ŠS‚=ę*œƒ˜<ÇrÓv3VOSNk6¶˜]”żîd?"ÿv™’ÎőáxIÉ;Xž­eš,Ż,ŽÌâQĐï o/56ăcs±żˆç=š@ÈŻ'™Œ"–>jƒÆj}"sh$±•~@wÈóڟB:”őšâ‚ńu(cBČH6š5WWÓ፩dăÉ~€°Ü>ŽÁŽ·jv»țú5ùwc§”]Ž»qy2ÛyłŐì\ś }”k›vd@‰ÉbrżLőë{K˜ŰI"‘*òsŚŽ îKi,‘łłòČ.[ž«‹N«hÄXpüuŁš=N¶—WÒa~LŠ2›”c íLvI!8śźXŒ·§ŽëÂ-UÎ f_nÔÛű§Žì\üæNZF$čöíOšșcŒK'—vć"߆.Hś«łéÖ§@{váÙúăź)ˆŸLÛš™ŽŽ,7Öí·=3Śž3QÉçJŸ]ÀRńŒdŽIíL?™m|«<ëp6ž^čÇ\TzGÚVő-­1ÜHËŃph%Ž·ž{ òä)‰ś(mž ž”cSŒ†x<•f܃ä$`ț‚˜O*ÌńłÇż|ŐÛmE!žKèâdE8aŚ”—<ŐÓn|ûv+ÇĘæ=Ș-rÒ(g}BՃÇ&ŒiŠb1kuhm'ŒÀß!UûàÔcĘÛ]K:ą±É^يdšI XNŻșS”°őŹÙDš]Ù_'îqIn †ușžFˆ*HfMĂg}Ș·{b.ä7Î1TKÜÔČœItƙe.SÀȚ”ÎÆŽŒŒŽ# ëë@\u‘–űæYe<@zßò©šÖ(Š”Ą/R1țŹv>ôJú+Ï) Û[2#.6ú~b‹ËI.aÚąE•XykŒć{Ő!H޶6—6~Yw1:ێ2zŚ?,WVïęŸ>à’cjžôæšŐÓîŽĘGl‡iVă Ö”l3Ÿl‡”d7Ę4ÀŃșšÆêÀKEŠźéGQӟ§„dȚÆśA]dv)?0à/ țŽÙ!!”„ äûŐèlÁL›‰É+‘C74 čRá`…¶–#$Žqè*MJÆMqH€±xË/U'ük—©Ű”E-’ù!Öb0FFzâŽîš–œ.R5 çă<r„Pî†lÄĄ˜źCĂȚ«ÍjfÍÄIò—èi’Mcs-»­Ì#Ł€ê hjSęšâBÎ-ĄțgÜŁùìxŁš†2ęŒĂœ[!łœś•‘ɐc“w?)SŸÂ˜™§%„Öœ TÈÜßw‘TąŒIŹAč‹{ÀŰGUőÍ (^ív7(›C·ËÏ+Kc2JŸDò6ú”Qœšdő5ôҗ06™bżó©N Vß|Ú-À#€úQalkX[KkćjVóóÏë[šŁ[êÖŃ\"’#PfÈÚX{bč^Źí0ï-ż|ÖĄ¶†o”“œ—I»›N•ìź'xƒt;yÏjąz’êVČiò)ˆŽD+ó. úûÔvö¶)§‚ò‘;čÊ·Ę” •,§òï¶Ê§Ê/’œ?Ń{S$OŠ;Ê]Žè€ä~?…0èZÓlŒË/łLĄŠ’w•8n:T3Ác‚­Čțű9†ûȚâ‚X¶%›8ș± 9ÁôŠnm3PûLŃș«čY:}EĐn§cÖźÂIP6‡èG¶?Îł€0ZČM`ÒÎĘ=é’Y{ČŠ­`gŸSWŠž[;ÄŸ…ˆYÈíÇœ%Öt˜ŻîaÔ-†Ä‘aè՘±MmvÖ2Dă€wp 4&†Û$–wœąŁ$ óWÖDYŸÏu>èdŒ§ăGQt(X,6wć.7”@qȧ—JÇqj„”ĂќžŰü)ő›€č—ìBÙC†%qßńŠéæK đś:>OJĄŚö*Śx)—#ۧڛ±Šökç8V28 @3I.ìö펶ä·Èàg§_ 5m‰$€Šaò…^ÙéűRŸqbö74xŸ;CóÊ„ŽâfDÒçr‡rqűՙł+X{”č7±ź „ïóűÖmńkèÚę†AMșs[Ę@mäm“)È}ïj·ešæ#evŒ’ „ș(0ۚK+­jÒO2<Ëæ~óŒcŽ=ńY]âŰiÒFAŽC čŰ:3YËoŻ•â@$ŽÙă€œ]đĘÄ«Š5ÝÀœ’:g…rJș–•< 5ËÊd @.ǜöę+&êRfI™KtÎsT‰f”ȚFĄ§ęŁ$È=ÉŰv"Ë)«àœ`gžŠ&Jқ™țÒÄço˻۔[ÀžÓí¶à…č (xÙ,çŠáeÛÈAmęO­M/—op“AÉ&UÆFyś=(^xf”’Xb`Æ éžœiČæț߃@sœ·:b*Û^ßÌw=Ìd@»JIęÓÇ”^țȚ;9ِ) ęh>Ž-ź­^Ùq0ÌL͌ڭهu›N%LŠ ‡pĘűPÏȚ4öŰ]6ÒË1äžÀU F)lŻ–ĘçÓqښhOb=BS<࿯cæ$y‘“őÍ8ÇtŒ—Ś?tœE0&_$Âb”ŒÈä|Ù>ô¶–ᯍŸ Ç àçkҁ2-V;‡„Ü*šxËl菭eKšuœL0ĘryÀȘD2ę•Ážł}:(ł"KŠt6·Z™T3țőGÈ ì(NŃQâTŽÿʒ6êX`ԗxQ|Țëž89Ëń€QEȚ{ä-$;\Ê|·m>‡5V—ŠäÌï"lÈIÉ"š†Ou©„ìm5‰ùÀ&U(3·ß”s{ Òïž ’d‡ÈèŒn#đÎ)Ą‡ÚôĘHÜ.ä*s†àâŹÜȚÌîș„ŠÀùGš`hiææúĘd–W'űóÛëïMŐ œ–%môíČùÜ@Ąn ÙČč—yŽkf‘`Ë;mèœÇÿ^Ź[ÜĆi©E4váw‘»9ô<ŠćGeÍ]ZÊk°cAò2Œ2‚ÙŹâ·…ä·œ‘òŠDj9&€béòË ri2# ÎăûÒ^[ÄÛHaű8Ï­Q$qÚ4đ°óXŰ û ”§\ZĘ챖}„G9őÍX[˜.·Ú\_-~EU#§Qő©-ŸÍ5ŒÊ»¶ĄÈ hĐœȚžŰ|-łàóŽ?J€/–#öKw‘¶àžûÌ{cœ2Xš«AŚÚ|­Șç r(œdònÚcÌ.œ”Ìž~HĘÚ` WÔûVȘ”Üϧfۘ ïÜSŰœš_ÜFđ_A Ç&GAü^őcX¶ƒT”MU‚d'%sì?č .–_,ŽLß>üâŹ\Û}†Qč—'çUCœ~5D‹+Źw1œŁ™@JV·{Wل”‡ OçH=”¶ółÜÉí±•ŰǀsȚ±î-Ò9Z&ÎÀr‡z©ÆÁxËuŸȚ2Œ§={ Ö[żŽő 5 Ùù¶Ž„*berțUé]è\Œ'œjÜìžĘŻ"Ű0IăßéIn„Ùr&šb!òńœO$voéYRÜZEt·[4Š2n_źsM @óÉkpšœp ‚v #ń©5ËŒÓ6ÂXˆČđ•èO§Ö˜Œk{)”ˆäšyÍLŽÍ#vš"m%kY(ĘGĄőȘ$ÓŃà1_˜ă‘™rQńĆjÏa$°Žš{ŻÌ›_xûÀŸçS°ś$ÔŃ€ßD~IrIQúSc€yLÂWÜ"·\zŚ2Űëf֜E֖–’Êà“‡ 3Ééę+:kAĆą^;Ž 3ĐP2KûkÛËUÖ.$€üŒ=})·ÎfÓa1  ç%ò úțD”Ćă5ÒßOneò†Ò§ąÒNfž_”Z ŸLl˜‰Đm­ Ă,‘Țl–lőÇćRIääiC€Ręæ·”8^"›ÌŽ| Ă<úžÍUŸł–Êęíc˜ÊűÇ=ń@1/4éu+D[ §ë l ȚäŐ$­|§ ˜ț\fš%žP%,æ äSèłĆžž·iĄŹ{ĐÓŽ2ȝ"_»'Žyü+CAșYâ}TbÀ•ÁűûĐ4aêÖ·EÛÙMÎXŰšï.dčU’ć•]Wž€vȘșnąźČ€±š„goO„O„K&łk&˜ç/·śA†qŒÒb*éóęžp/S*‡dŠzę~”oVhVÙmÍÂ;/Ì»€ÈéLFŐÓFD»>|țBŻè·%6rO¶)†TÆîŐD“A{ŻÚ­' Ș‚€OćMÒnˆč‰l4EžzMÇššúw·œŰyőè1֛j艩dÜwęáNáčRVđóș4nÛŁy•?_„Mm+Ú§9 ćȄöô4Âæ5őć”Ă\oESœüàgȚŸ Ö±_GŐÓæł“NäŽXŃ/m‹5œÚ‘òŰÿ=«JÚú{,ÙŽ«ł`±?v‡š^[$O€É)ÛȚž8î*”}” ˜€QŐk•Œ“EŐRősÊ» –ÚGJÚÖ-à),ŃD6É̌z0úwĄ‚ŰÀ–,m6òńäÇŽÿJłáè„’Ííćr§'šä r•Q"jvÆŃě>\͟őƒ·ćTôÙ^ˆFÚÙêă…ô4ÄXӚȚææiźŠçË mîȚżZ’/`}4Ÿæ‰łb}©,vÒM[˜Ő—'ïښÍ-ć«\ÛÛaáP)q‚ßO§zb3^âgP‘M·Ì9Oô5ʅő——u,Ÿd3˜ƒœ}}ûÓDîMćíDve(8)ÜźxÍT1Ë錻fÁ>Ÿ”Ášsl°F„äA€c>țőrî]ĐÛêրù›±rTòOœ êXÔ4śŐŽśÔ 4ctlIÎ3Èț”ÏI*›Žy8oœ“ÖšXÖŽéŹä[ŰyRÊéIk©Ék$Văd›Hg~śђë:i 5˜ä—=G§œŒđ8»·ÔnŽrÔRŽŁgûŐ»(ŃÆÇ€œćT`Œ– PHt9F[’ѧwë*5Kqł$$€v8§Çcqogöćۄ $zŃp±cHHoËȘÇoËŹÒ}”X–ńźï vxG–2Â8ûŸáçÿŐKšÖÆ%ă-ŐÛÏMŒ9!G Ț­GrúÆd1ysF7;ÔŽ*Èê[ŒŽUÓ⾛ź»dŰb|W-9—Nč6ń’ÈwLR@ÉțÊ#œLš¶Ă‘ÖŽ!{=N9;ƒŒ#ÛÛڙ&ŸEŽĄÍĂ;ٟÛ`j=n>ÊäÂ7‘țgüŸąčNÖĂk,~e­ÄhŠŰIñíšĘÓ”îôŁ Á,ê~`*}E6ʞH­”“,ÎÛfyzóÎ*šÜIg©;ŹÙ•r­ÁÿëS ÒÔäYíÀGÌr ‚ÇîÿŸJʒy!Ă2ÊÂ;ăր#‰6ĘœČB;»ow=ŹKwo<ë2È#֘‹š”Źf[ˆCrÇżœR{h-$ȚČJđ–Àl@2-CO3:2~`3ÊśÇćM»3^AäXĄ(žù‰Ï$zž”ĆbM.à&“-čÍȚž3»5Łö› .§‹ku%Wúb€dpÏ {nź!Ț2r»±Î:֎‡}ÈöźXĂqÔĐúÓbF„›&‘pRS#! ‘śœ«/]Ò­ì. Š6) Ï=ÿź’Ül†Æ)ŻŹe°“"X†ćÜzŠŁ i[;*œ qT˜·6lVÛçÒïź ßČ<ÊȘȚéR[JóÜBC.Gj]BÄ:æȘ·6Đ»ó(ïšÁ}čn8Š„±-êièzĄÓCL±ïIΙïę*ÓOš[ŸŽ9W[€èNF}èê"(ź)~É$…76ìÇÙ» tó‹Éü‹w]źȘ%Ço^hêÜèj>ĐÏČ?șńç$œ*­,gZÚVÈ^3ééM;‰–#șšĂmĘșæ ™éëQk[ÏnÎۈ0ƒìyNŽ r…„ŐŒ¶FĘŸYÀ*>śŚšŻd—:lžjÈS'»ŠĄ\Ë5æšąvVxfÂß6;Pê‘ÜjlwM'úËôœsŒ‚ȚÊk›Xî]Ű|ûqțsZș?™§jfÙÂč~»O~ôȚÂÄzP†űÌÓFE žŁ8ȘąÚÖK"Í*yȘ2YéŽé~’^êČËç Ïę|ŐkË?ł<֒8r­” ńÇZbèRùëć üŒ‘žMIk=ł9†FhŚq …Ü~•B7tèÖăKm>iG˜Ä4yj«khȘdI. ”"(ÎìÒ@ÆÏ&Ÿufù.•ÿxÇ?0Ș±N!‰ąLdîTŒûÓK›Q K‹MÂ">vLjäTúŒó€1CÊɍÏòä©é„2§ˆĆu‰rIÉ=Ł)·Ê«’ùő  ‰ œÓæ-qaŽ;zš±mö•›ÙĘlyA&6ŒçqôÍ 2­‰ČÔV[Ź~êL0np=ꖫ oq-ê)1łá2j–âèMcukk»0”chaò­\Ô€șŐŽőÔazœL‘Žƒ±ĄîKjŃIfè6ÈčY6䩏ëę:3ŐáÓ9uŽŃ-\©m$ž[Ìż\šÜ‚î=CF‹Hv "IòŸzûSB4ÈË”»‚& €Ÿ§ÖŸ`ÒÁțom˜sæÍ $ Î3CÜ”,2]Ć)ži«ĆșČô{ŐHâ}R"Š›^ĘOÌOȚäuś lE·–ÒŽô‘Üü§‘Í:ȚŐŁk € Âč€cïŐ±›©Cm§jËaŒÊ‡Š­Ț[.šŠâŐÄźĂæ?€ś8ô EĘ*ìÛ\Oüu­ ‘é—ÆYÁ>9É#=»W1ÙĐĄșÍy%›]ù žäÔk/‘"0f,Œ‘ÓóȘ·5ŸÛôy"¶7v `¶H=N?Ք¶Ó€ȚI6űmĂE$ §ÀŽŒŠ,»Š±è«úœš)ŽäÜŠàŰTAŃûÇŚ5Bèf(kițс±·ƒ»J&`łù„€%śša’AśŠI”o6éàŸ7±țńÇßnsŽăҟ©cš­ć»8ŽBq"ó‘ÜÒ¶«kkńOnæUŒŻOĂ5·1ÈÇÉË+ć9î) ,Kö­E|ŰDJźw<`Œuă”V…Úí ŒhK“Ô/j`A} F’W;•ʃê1ĆO€ÜÚ]ŰIe$jŽá)Çì('©oIŸŠ ÓșMĐč*~\gȚ­Z*x\xgˆ‚@ˆäóĐsHe/ÛOó\ŹQÀó@9ÇÖȘÚ îéo(òđÍFrqM ڕ-öĂ>˙w!l0>‚”ŽÙځXV8%È1‡ÜYsCÔE[ë7ÓnÜś‹DsíLż’i,†©jD'“2qójiŒÌ–Î €°ÜčAÒ„°ŒXW%·É@EQÉŻ&»#\}ĄŒžÙrîjь(MA&BÀ“ł=Gœ&1VMBïP}DJpB;éR_Aœ«5ŒžZł?\ÿwë@2&°ŒŐíÚ8Č śˆî{t€ŠytŸjŽGÂăĄ_ZdČŐêÚMêVöŃłË YSčç“X7ïچv”Š˜q‚SéMj&\ib,ÄWW©ükBòí >;·y±œHò6ôĆshŻšEqwłWXc}à|w©'Ë6±ł.bŰr~И‡èr;H±Eœ\…$çčĆ%îŸ{ĆÄE±Ž3Áè„z†kTye(Ú6 ăȚźŰŽZ–šIæÆv¶óžz`eßÁ$†pȘ8ĆH­Ü"ÒÉ~6ä|ĆqŸ„2^ăŹeȈ4r)b~áÏĘ5„*Íy€ËjČńžt9꧊]DEonníŸ|čt@6ïUäW#œ Ä>]‹ś^iő{ ‹Hƒ}­ÙTà*äƒőíÍ%æË+TŽ—űŠŽ9€č_T\J%XČ,Àu4Ûímïc7ĐòăŠ:ÔÔ»MžG›N-…›=>`1À=ȘÄKę©șû;K,Hw(8lqÏ֑D¶qÛȚÙ”˜B“ÊÇz?c\ćâ]C+FÊRHŽU}Ș˜’4s[Źê ’ ™xÀŠÛŽ’ș›xŰÊ€r§  ”i]Ç6­i±Fe‰· žHĆUÓ.ąŽ@ŽAŠVÛ*ž™Ąl>ąj6ăwŰJ'–„ˆ]3ő'­b•xź<äƒÍZbkRüQÂcDO"^ 0û€ś«?eû«ná$ŚÂæ†I~+Ű4ąöĆÒX‰à§„Y‡Ëč†F–@«:ˆăî°?ڊ’ˆeiäźĘK•Î: Ô„­ĆŒ©}r ț„—żÇëM=Dö#ŽôJ Ąm‘NrÊŁ„튋SьśgóNčYYűțxȘّč6Żû»ÒbŒ1^]țTšxûO›3áƒI™8Ÿï^+œën—€ÚVü30mî1ÓòȘÖŚ7H‰fI!& Ą†p;ńT"ÄW­eȘíYy%pŁOcÍkßY”îŸ ]Ü‘òï'iìOLçë֗Pè`2Ć$f¶É챫:jC„_”–U٠۔9ÁŠ!òXčDóÆIfbKg xæ©LČ[ÜùA•Œ]Nr(&»‚qĄ !Ăç#éÚŻĂ$1jŸm’łÀŁkčsÈ#ùĐ!)€êŹÄ‰G, ;b[ÆÚœ/y–’Äő‖[©^ŸWPÇ·§çJÖ+6™!Ka;“óúÓ=6Òak6“u ‰ĐćL€ U]CM‘ŐnQ•û1ŒqôúĐ'±^+™Łvxn9áčÆG„kAr,”t—ÎƜ -‘Ûżz „æ‘vÚąOòK)pÍÉ\ÿVń=ȘȍB)Ą‘æ:Ž·-ÁìgiŚ ûO)í0XńŠŒĘI€ßì[ćQÓńŠ"ÓOê"xîqŒe㊋SÓ͖ɡbažäqĐĐŽGŽëP”KS$y‹,źx8ĆQÔ­€Ú—6Ń6ÜáÎ:7œ4Áì@amæ s»§5łgšŽœmÖŰŒ°ÆpËŰZ(ŒC őȘÇon±Ë‘łŽžŒÔöz ˜-Œ±*|Áe•s‘Fă4mš7–}êӏ.€uP8#Ó·œCfèaÍâč;¶ÈŒÙààț•e\Ù}‡P™”ç)ߌëWôÀúŽ,ŒŹČ/̅č;iœHê7\č–ćV[kO.=€9nn­æY ŁÖ1:YĄvV]—pÛÊŒîÜÀ ĂżOz‹W”ŠÛPŠàHï$ÆĂ8ôïÍ0cZÒi æ'MÊC«ŰŚGáéío4ńk.· ęę?úÔ0F~”Ì­%ÒDŠD»EÀ ;ƒűUY uòő ”ÄYùeQéëőŠ!ŚZƒȚX™m‚Ș–xQÏ9ÿëŐ+ۑąÿE$$č#š8 À¶łÏhXÍ ĂóĐv«zmÆ`679cžl@:ț9ŠI~HŁ’ŃŚ'ž}?JC4–‘„ŒGÎN0Œôî})$Aźí–yî[;ÂÊ«őăüûR,vvWŻł˜Ùòl`ț”oŠ Ÿz<ùłŸBiż!ȎïÏ ’äȁłú `f±LnŸË” W=șu«–śÖÍi“.X†Èń€#e^GÒäłTäTàčÎçôȘÚô±šÚA)*Èč–=ż6rèF=kÊ=ÂM2ïx‰ap ížô Ń.é,„‹ć”ùa_Ś‘Rê0-”(ƒiÈaOšžÄvûŐĘžŒ}~•‹™· €Ùéšd›śKKša·f*9ÏsÜ d–ÒÙ^nćK!ƒüŠ@OțÎżkk‘Č)pÁÚĆSž†A,a6—&&fÍÉ €ZŠ$0żÊàŹ›˜Û~•JÍ!ž; ۞☠{u%Țš<†Șْ˜ŸZ“Aò|Äóí‘ßËì:çéSWOšÍćwûQdLí}áôę*„Ä~“ Š6rÓÉż€°țtˆÇېßÛD2çÂò qÏŻÖłäŒB‘ÏŒWhÎ8©Ą06qÇÚȚç †ŽĄŸÎÓcŸŽ•â;e@€’=IĄ_R¶’îßûN° ĉžÔș]ÍĄ‰l&‘Ö)1EnsҀ&ÖÄvq‹KbČI~őJpĂŚ5‹äÚa2à2`^§>œ±T“éOÖÓźePJæ=ôąȚ$Œ dü̱€FzçúÓč%­"ÒÜ\[èщùQŸ‘Śź+Yï&”»Ù• ùh‹cGź8瞏e l-ă’Eœ]Țko†"O?sÖ±őD6—ąæčd̈­ÂJšČdŽŐü5œśö•¶šKœ†zæčÏxRÓ&óî#Ìlx‘Er'©ÔŐŃ_H•>ÙNr$Ê6FqïV–(tíEƒž ùGw^Ùúś­TżŠX.ȚȚ7ÀaÎzâžcxÜąçï Î1L–tvÀĘi~Tx36.sùț”Ź”Â_­ò! fç§l~t e}rÌ#ęčW`•CœçÔûUö†[ë[+»oœ”Úê{ăŻéLLĄpÆȚăʏï!ʟoJO!ź`Xܰ“?tœéŠ .h–†6’)ăɌ<ŻŻ”rú/b‘Ąžs$QŒ!țtu°Ä5-5‘Ű QrŰ_O_zŠü]_éń_șDŹ™]êŰ,šú ŽÉ¶> ĂG­» ێŐ ì‘ź§çtÛ[ćÎò  [ś’BT%_-nű«*á.tëö “ĐC@֗bÌEn“FŽçwšĘztÍ]‚ûûY<čìă’x۔Fsï@Y„€ROaš2Á{ę=ùȘ7ö.’g]ȅUÊくZ. Ršk!ąČ)@ä#ûŐh&h?ŃRáՋáN)‰šzMЄ:iÖC2àí= dŐmç‚śìłĄ$Æh^Ëû9íAțđysč8Âś$”Í_ZEos$qÎÍO—·Ąæš#žż‚áȚ+{QÊ7déš¶·I}VUB瞝Jv'©!YoÁ»¶ –.Ù$ś«AŁÔŒ„…‚KżË<ž{Ûé@u.”’Cj$ž•Ț@Ùă;șzwúŸZ-moMwŒ:…œ6ÓëŸé@™Ę-A#>`ÊNȚâ«Yê>d‹,ń©ŐX°č#§njĐžæŽ™xĐʄó#ˆżü”q–öUęR.Qí ‰Y‡ÌXt'n1íNhČ{ ’őÛÍV2 äóëV<=u"+Û`€ îÇ?„0#œ˜Ç/›%Ą$ ŻđäÔ p#u›,ÆvŒ·n‚XÛinmï|đû1˜ü ÿZœ ÂÏRP.a"òăŁCf+GÓź7e<ïő`dçŸZp”…594çmÂQò°LĂ·°4Ÿ{m4SÉnĆP«îŰxÈ”üÇ+É$ö»GˆÊ’@luŠô)"ÓYÊÈŒŁ ç9śȘÚÌ{ÓÈu”ÜG–‘ä’iuQˆ‰áòäŒmäœăÒ¶–ÙŽÒš€bBƒnÖr}è‡ßŹ†î ›TY#”ŒìŻ^jśˆVkë!r°ąìćßű1@Î~Ęć‡PK•Eonőő Óu»iŸÔ5n"YđTg¶:ț4ú‰‹o,PŸ2(|Ùś{đ”\Œč—Z±yűW‹<ăSHș·Œœ$ȘC‚xÏO©éa%ț̎@YFőpÙàŒț8ÀϖȚÆÒÒ9DLÒ y—9ÈôÛKe%֍š€ö…ă•A’7^ÇÓßÄOhđ[]‡ȔgŐhȖw‡ć 2ŹdgùQč%Ż”O1óąr<”_8ˆÉ“Èo^ÔÛë[»ì›pqóHĂ'ú æ„3ŒŁëX ’4™ r(*Ăæ5 -°Z‚ČÜ1ŃŚÒ…žúÚy‚ê;­.d9oÿëU)|‹r ”ápC‰ż„ÿN•Kp'œŠłMN ‹ lUéțy=ëfĘŠ“EK‘â Áč#úÿ*lžŠB–IšäÀÂȚYz`mÎ85Vű—_—ł1'9ê;S5”(nŻt„–2žS ËpŰÇQÜț>•‹gk+ÛÉŽ„DÿXÇűĆÔćŒ,ŽŃ†ëèj=źwù·&9f0zri‹©­s=Ćî™Ò»4–ăćFx51v©©Ù)Ś;‰äÒ;ÀśvńëMhŸ{kźî üjdŒ&*ëÊ>śAZ­­8·Ô ż€9ÁęăŒuZK„>|łJwmfRsŒĐt† kÉ ÌÌàÇˎ};Ôò_]GvWQțíÁd%ŸêóĆ4t[Š!ôŃ.ŚNcaÛŚô«:\¶ś7n—O#*# CßüúzĐ37ÄV‘i–í ۓ$ÏČÙÚ>œéÚM̚ŸžúeɍЧî·}ìûzÿJÇ·•loH˜àă'šŸ[ì7é$ó#Ź€üȘ8Pj˜ƒWҟOșdßœ$Æ0çÓéҋ)nź*Ș#!;ŸĄaŒ~?JŻ­ŰZ[À·1±%Á*AôíXƒP»š_*æfbǂNNi­DÍK;ő“J{ą»ăs‚W'_AÖŸcgkulȚhe“a°8ïŠ{ vixrö6ß§jr…Œ€Tހޙő«·Ò\yíoldàÄÒF2Oń{Í.ĄĐì(ʰg5Ÿâfżn¶\É$zŻqI;”Œá5Kp±słwP;}išŐ”D58:M.v•5 2YîWP·6±EDßUUÈ#ÓÖŹhw±ZۘîÈ ,kÔęhb#Ô4çĉq3*G…èAèÏ­CmeæšæĘöĘ)ÚÁ˜ ĂĐzšś,hú›œƒiQÀ«,h|ŰÆ;őïҚ˜űoĂFÇk8éLE›Žk[„xu1,„BœqZ˟MòźÉKòzŒP#oGò­Ę,ć|,ÊwÙëÚĄ?kÒîfŽâ70È0Fìô d7ÚY]F.ÚCEŒÆ‡vÖ?ÊąŒ»ó_ÍEXö(1GŽĘ cőgwm–wČË.ÍÒ+uĘU,䌞Í­ó,GćGăôîiˆčs§k_fțÒșœŽȚFr:ôȘ·‘̧íplđÌÙÀ€3Vtò-FÊdÀnU8íSjMo±e3ąKòÉ·¶;țtfëJ[ę:3Hê7n~Ł=kŸ¶»6—Fć#ÊGòčÇj4&ŒöÚ€żm°‰SʞŹ=p:SôÓm}Gw…(üìĘïT†xș…ŸözÆŹÈÄŠW?^żQóĂ2ŹLc1Ê㜞ô™+Ăe„q]NțLÀ’Ä “ŽĂëÜÖ¶a™Ąû6ÉWHȘBbÙm¶‰š70oȚÆâŽ.-â†5rÉ!ä/E>žô‰/b–)"Ő-”’F1Ł|Ê~:UÛmXČ_L&`ÛălœŸÔÓbHï(źsa +uĄ”0*Ă ŽA©JĂčÊkú]Ÿœò,€ȘHًh뚣h‚śB’)[wäŒżțșŃÜš·Ö1&b&?“aÀËÜÔÖÒĄ3 Ù"…V˜`šìj„jêŠ>'x<Èą“æ7tŹk«)`čŽi› +‚7Lò9úHOqĐÜĂkźLyaŠœ6F=\Ő4ìÇ=Ɲ«g?;vŸqôśŠ+0Êí!”à»Ż.Ü`J”"Í2ŰPÌyäÓƒZ‡Eyq{f‘AhX–r@"ŻÜ5Í’Ę.űűenwc„č^èK5ČĆ$‰;”`žôÙ-ąž+{­ÊČnی đ98šè.ì-­%>vdó8X”är}êêÿgê‘Ț4źćđìî:žô ’Kpî1o°Jß™ =œ*ŒRÜÙ\4sšÚăæs¶€ÜœĄí•dŽ;U,ïò<Èƒ·ăW-`‡2hóRAŸ,6p}(cIż»·ż63DŹŁƒŸíŸçYzț›ö=Eâ”…đxô=) €ÜŸžò~êB6ÊŰŸQíT/c67ȚNÜ)=Hç„IrȚ(tКœĆÊ6_¶ éW5oł‹x”ˆb#züùänż*c)ÙyîW1łn@ŻaOÔì՚;,‡Vƒ Š#™Ś-™ŠûBŁ phÓ„ ky˜ćNT“ҝôSDÉĄb©knÆH—20=9ÆZłaê6oö»ÇGÈf(żúÔç„Đyë\ÒWFąb‚9”’E\Yô=ÂæGÍzżdY¶Ł|ˌòG”^ł‰­,–EÊ}’±oœÍ CüClŹ<ËX°"#üŸÿZˊŃçčڄ€äì?ߥ îXŐ,âÓìQ$žWs'0Šxüit»šźۜđ!Oš(űç?­0êhjö—rÛÇšÛöUž€ŽŸäUY-Śìąí„É$(=`ęh j6æ95 HőGÉęfxäušíșE.<Ź—Œă?Q@u#Öæ];RMFÊVÉyíŸlu«zĘőĆćŠȚÜŽRFXăFö䚠Č\é—Im*˜:‡^y)c61\ {€Îí•Üh  ¶3>Ÿ©”ą°òäRźOö«1Ęh>dŻkșpLŽTôÿőP4KŹé‘\ Ö- (Hà çœcée>Ű!˜â9XóŽ‡Ö˜žäZä6šlČ[Aśs”“źjkE›R+Ș3Ą[uPȃæ*;àvś§ĐE­zæ-ZŐl ÆÎ21€œeiêDš— K'B3Í©kXÓϗ%„>P‚ ±ÆrOšûŐÍżÚĄȚ"Œź:ńïMj„É-/ŸÌËs„3će_nő±0’ȚafŒ6>`wm=čéŸjbG„Ń\͚… 98©ê1Mqz’ÏrÜ(FV“aV‚ő­"«€ŒČùúDxE•KŠîävȘJçOaœr–65P˜Űš%eŽÖ2űäăż5ÔAki{aö5Q„łwʎ(b0ŸÇqöoŽ,X!ŽÏ›”Ç_çMœuxÖ•ŰÇœuŠ–“yos€žVù™Ï\ń€:ț>”9»·ÊbÊĐŸűԏΗPĘÛÜ\ÜG#<™‘rÛ֛hž}уíAž$-€=iŃišû6æO”©+­ëPo”û[[AhÁĘ0…†Fîæ’^ˆ­ź„ÒŻSl,Ä4‡ôȘ+zöòyPă(ĆąÏ\zPK'’ÚÊț8gŒœeGcžíÈSŚóöš-.ɋû= ČĆ7ŻńS\ڕ¶VžßœN\“ŸĂŚ5 Žy’|Ą;Űôö v.î‹Ë63LdäłsÓ·ëZQčÖŽ…‹mŰí ”ŁMq{i“SùÇoóÍs·\[ÜKj«ÏPÄwő ee[íviŒńîhĄçÇŁÓȚK8ÚÌțIŠÖš’őÛi—?cžZT|…ă>Ł„VœŽ[}E­"Y$Ì~ƒž? žÊ)nż{«ș=ÆUʍȚŐGQ‚Y$ûTđù !á žžÔЊ1'Ù„g]„ Ç"ŻÙ\BK+™&e#0Șž7v☭IȚ蚀fŒO—PžW'źÆÿdž1męÜä¶z‘š€͒ÖXيkpç.A<ö?áRëÈ"Ôb’XvÁ · ú禚°eYí’±"NYŸû—$dAô«^œžŽâKîoù”ąäœęzS$žóN…ZYæ,7qÏ9őæ«ÁteIˆÔûìŁ#™éš@Èô«žeŒk(Ű*ËÀˆż †ê û\ȚN‹Ž9Ú{0Î @͑Ź›š<rčî O7ٚŚìńÜ!hÎCś  ֊ÿd‰ÒàîjdŒƒÔœzUœYŽ:»e±óÔ‘Śühê1u[Oí}<Ü[͍ż3Šłî-ąH។|°^˜HëÇaÍČŒ$É:#ćDˆ]ŒőĆ^žÓ`°TčI6OŸ7pÜŁŽ;~>ŽÀ5ežDÔfÛ$ĄÊÍŒn^Äț?ÊȘ^ Ę&F‰Ó;Àgû€ó@ś$Òî-ĄMï–É'FbdśăÒŽô™Kč<‚ÎJœŁùfąi#[-L(‘Š9 ò/đŽ†§ń.ž·B{7'æ Û'<šìr:ŠÉæ W(OïR’!ž2ÜËç# 3Ąț,Ją ĄłŽț Ș€\$€9ÜN*𶯧"ÁžŰœ’~đÿ@ÊzVŁö;„”ĄR݉qÏăVæƒÏŽi¶2Ł䏭Ôg ő<Ó{IŸ|>Q!;šțuŽá-±) Ą;=é’{&isšäʛ Eh ź?W’àœĘŒj™rFæ=8Ą_M·1IžČ<(ĄN@îE6ÁüÈĘ&E’K|Č«[?­Pt(Œši·’k…e™‹<č5nÊÖu–ëxü”ò ŹGMčéLF‚Ę5Ó”ÔÖ'u±x#ÓóéĆbj»™Ă vŠ)àŒrŰő€ Ż”Ń–č ”ą!ŽOAZ·Żoœ…1 Ő|9&•nZȚäJ ç~’3Àöź~kYÎ.ƒŠiŠ ÈNÏ5ĆlĆą”èHW#­óîctbä€òMTFQÓeč°ÔËnȟ-=LçŠj{€șfź8Ȝ±-»+êiő†NžČY^Œ’Z««ęŃíŰęjH.f†Î+YŠ]TuúûqTIĐhWvÆ ,CüÿÄ7aŽ=OaPêE”Ö‰jۈîgĂŻ'ü*zŒÄÚÍŠčd>ńœ”}ïoj~“æ^A&œüćK(=°?˜ˆæš[Xc–y7òČŸ“ßߊ§­œÛdf3FîÆČÆ›“q$ˆÙśëÏšÂeŽgč>r‘Ś‚žÇćÿÖ€Á «›Wu8źš!ˆ+Ë7z­š]Žó‰ ȘŹȘ™.ÁżZ[›{»‹1šHÀËË!AśTqÏ„gA!·–H”q“ÿ멄ÇɧąFod}źÍ…ŒęàGsè*Ô.îK•lȚÈ r{ę)Œ§Żiï§Ę­ÂL`źJ·+Ÿóš-/ő ”dŽçËVc»żÆ"í•ÛŰ_,Âà?+Č~U”&›§Grże•„“1‘è8ß41‹sßiți”+źCƒÉSę+œ·{Y|ÙXÆóüT!5šjÒiсuk»/ùIÏÔSt=JÚ&k{đRĘČË»'ŰzŐ[Bz—ôÍBÁjÎw+ûęk/RócżŸHùéڄ čos+[É~¶È»“cä’}=+=­V;æïò]I©Ző =)ȚőĂV„­n ÉßY\]^ÏtŒ =ÉU„wúU§`±‰vnlȚK6eâMϟQVő[¶ÖŁûR[”T(vÄLŐn ŠXąŐށ|d`Đ ’p1Òłè" ÈƑˆlôÛÚšF–š˜5%»UâSŒčî;ŚEŹGȘ.‹ł.ÒœnÈ<{óRś0.!ŸÓnÖȚ1”€”Ò ÔPZ\$ńĘŹ’(Œ±€1Û=èi ÒæŽk—ȚVuažSÜäÖ[ĆˆÒ^@AIBŁŒSMžFù э­Žàś€ČÔŸÌò›•,‘ăéűĐ»jÖWÖRéțhÉĂÆd瞇ò€Òd€1ȖA·$+sèOzG„ÚĘ=Ôș,źÀ2–ëÁ#§țžő ”ÂæUYĄW#œu€K/i“Á[čJùW3|ēÜ/~•ÚI̖Óm‹xĂc±Š>‚ĘiˆŠČ‡Ûw4ŹÜíČöśŹKXLSź€#hÍ0±~Ëv§`oĘFZY óŽÜv­;#uuŠ›xn?}hۑÁÏà=ÿôk‹­8}ŽćX çĘÉǰśŹZ ›MI„;Z"xŽr(CdV¶M,O+˜țá'ŒTÌMóìP€•-ÓÜSč,•g’IŁŸ’Ń€ cœ€§Ś!QÛ«' œHcŠ:ˆ­ò€ńł)œŽ@ăĐS"iőgšY]€1Î1Û4öÒüĆsòœÓș àOSVúšA nÍUÂÂO/‘jóśPœŚ)qr«äéÌ~r†GÿyŽkUžt1u]A x^Ń‹ęęž$zUŸ2ÊfȚê2ˆ‚]Äp=čéZ-„Ùœò&§&™#*ä1@ìyéőŹù§6/,~ń$È`­Ž{~”KqtjŹö"f”3‡ęÜJ9P:šëô›‹]gOòшrrÇű‡AÏ_ ©`ŒĘnÚe·û|ÖȚT›¶Č©ÈÀÀȘČlžÜI-€{%]9ÖÒèÎì"<Œ°IĄ‰Sœć§ÚmB™ÎHl`ozÎŐ,« ]Ç8ă#šÀ~Ž‘} –€m ;óÀ#u)ł‚çMž9ˆn©oaéL†SŃcžȚpeeÚà† z}júiăíŸÙĐLΎ™'=G­>ąFEƁłœkkŹì,vœô5©g%Ž:fë‰ÊɃW8=?*àŽæ&ÙÁ{ÔȘ䓑^zfíjk©-Óœ1v xúUB—Š”+}>À-ĘWa»éȚčK™œë ȘFŹËò¶@è”ŃԒXíș7Ê€€Ÿ0cÇRk=%F˜NòpŹŽ™iƒ5őˆ#¶x”+#Qž2InzsȚ«ű–ËíQŠŁfFÉ;‡9Ï=8ȘË[žŽűŒ±Ú°í\`7âŻxrHm5Ÿ.fۓ”à}è«źÂŃEöˆ”O±ÀÏź?•bO)Žž›OÜ#YŽ‚wqŸ­![OZšJšŃŻ ’{֎„ٱyÎ6ĆŃrü–=1ëIî#&ò‡Û†?,Ș­œțUoK[i-€”t&Oč=F}»žÔślLúmÙÓçC"Ȑè§ŻïčŐ-~ÆëćH‚‘òă­!”cČ»±’Ëò+±è3Œę*8‚Ę”€Ă2ńgŽżŻ©ÜEYÚ[[ƒoć‘">w{V޶áăŽî$ûSÜäcĐđ8ÿ`&ví©Oh-ö‚*ÎQžŐJć.€¶ž·Cæ1'©'Ž)Ąm>ÁkČȚW”JÎV@q”EZ¶Űú”Ó]¶€pÉoZÇCauj‹2m‘Ôï'š>§ü*Ì7)aȘ%ŒśkûäĘ*°è„.Ł3æŽțSCŃ3‚ù#<śŠORÍé‡PÓ~Ónăty%łYqÉ3ÄUÁÉ`{P3Ó4ë–č>†Ź€sŽŒŚžŽ‰-DžCÄ}ÍBćłOv.‡)ăžîuiC*1ŸNMf藱K€ŒM#Œüà ’”Śbgö‰/ž _JOł»%Ê(vˆê? ÇÁ‡Ú˜ì*@ G^Ô!ő64I-oìM„±|ŃÄIeêÜő$ú§ÓÄČiÓG1GXÏî`î9ç>ÔúGMșPŸdžŽ?•šDó<~gŽ)·š…ĆìI&A"ŸPƒÜòiŃhڌz–ž –ÎĐQwg-ÏnçôȘșŸš.­ăș[EC"ąăžùôíIî9żłŁ/őözÆŚmÚÊęíȚ"„N1œóIn0Ò/DzŒo)Àc”ù#—5« k=^7óÉ©sœÁȘž5í.çMż,ű$r6ô>• ĆȚą‘ęȘb‡íî„?„ ÜF݆źRÇQŽ9 Ș2ç,zds[ZŒ3yÆ{{s'ÚWĄàăË?ʓ‹g4vŻqc}އŠ2Ÿż=*­–ł>Ÿ*Á?äU=}(sV±q·JòMç}ÒFőʱp’ły7Ï$P;Žô-A–4ùă–33ÊæiXF2:{涌VPZy3·vőÈëÏlĐÆAeZƒZíÙ°™ëԊŻk}…Ìđd01`č z h]F[˜Ț7łLs6UF:ő&ł!k‹)YĘ\6èČ;Šhr;–č„Ș. źŚ™Ś9럧áP<ŠÏìíí2YŽ)Ąu4VÎ=BÙąÊ N6Eó8őÏ őŹ«!•&—óԂĄșîMÇéRĘßIę”.•Wyf2ôȏț”[±[ïÒîDŽ$ăi^sžÏNh_ĐÍΙJ' $”żxšïU|a€]€.cBwczr9€ìs¶ï<3`s™Î})·¶òÛșĆ(ŸYGlՒ”'Óu+«’4óbî+ś‡ÒŽ4æ–ù%”č;ł“áÎGj= gKHÚy$ (ź^‹_[/ží${WÇTWSŠ·LFr:ńK3âEP}iGr^çšű„†Ô€Œ“ƒœĄ?…Àßß1Ž#ćùÉĘÆxÎ+čhŒwlż ë_€lf< ’»çèGҙŹÀ5`·°6ęŒUäolőÇaÒĄèËèdHČE#ÆÊĂdž5»gyuäĆźȚDŒQLq(H}sOp-\ËțŽoźŒŒ‰ ›8àę+-CíCM[wwWÌMžá=N;Đ„Í BOĘĂrž 1œ‘‰úçȚ”ő;łȘi‘Će!gE3Ż@;Ÿé@Z€Vé„q2ÈÌŒ•<ݱȘš­„Žq]GsóIÖ6Ž3Ÿ„°5~Țu.2ډp99û9ŹgHíD ÁÁ&2§šô ił$Wi=ÚGçË©­3un!‘á&2ÿu$'=…ËI掑Œ!Çœ°oᄌ­6̚œ¶Ó€°đGLSê2­èž„%Í­«F#`ä–ÉŸšÍ6€±kKnć Ô±xțŽÍ7Q†ÚVł\˜€ćCrG©Ś­Cšhśé)|Ż9oÇöèъęLVÆ+f’P lPBăź95Bú9tÛÏí9ąóˆV_șê?„ő+Dnt‹•»4Y;Ł?ƃȚ­ȚêP]K–Č!IŽčï“Üÿ1.u±ŸŃdšäL/=*Üź§§% ž!¶… ő^sžÒ§?źìŽ»[xm|§Uá˜òOr~”^áfŐ-ćźÄŻ«Ę@ȘèHÏ œÌŚ~tPĘ&ælău­!;œÒê>ZB™À ÀúÒ«ńNŠQ~ÍÏ†Źž$7ŻÎüțç·ĄÛkD܀äàÇœC#âă9àsűSŽæo©ç·pÛÉ©°Ÿ/ć–$(^XJłeEÉô©ü/7™/Ùs–ÎŚ;đ1ëùSèI5K.ng„Ă€š ;#çoڰŹè­ŽÍB6ŽæfY•p źó&’á€vžšŐy!~@W<ävő«–ZeÖ”ȘŁË" Gmòìăœ©°2”X„Čœ’Û#—Êæź\Ë}wj&2 H…SË Èú})ˆlL@k‰.Is 3ۚł§ËöögőG+òúőç”-X-•ŃKw•ŠôۏŒĘ=k'íÒéw7‘ł,l<Hî?ęT-ÀŠ<•rŃóćźáÆ:ž•·i{qy§;Ę\D‘JHpä;éҘ‘…g=€î±\șOJ»ytgaąEŸf‘”#zr1őŠ##T7Mrńß\–xŸLŰqŠ·§êV6r[­’É$žÌźyQè(èŐșc6 L!Lô?֋pú6ąĄŻ Éæÿ«*zvoz›Ćšk\ÆŚŃFÁŐÿx]'ž5Ì/™d<ÂHI>űôŠÉh>ŚoÁ–ÒF1±ĂcŒŠŚf‹RÓĂÇ8UʆBxtĄèìŻl"č›|©ßŚ­]”ƒÉNOjóNç±b%Y`#šȘlć‹ČŸșŸÊȘ?—Fpʖ›íPF$`Ű0‘‘Æ€ËdÿnˆĂ’ČF€Ç|ŐÚőF1V‚x/ìdf‹Ž§œtI,i€I«#ÄźÀ:7ńg=œ*,ÏÖ,­^hŻ„ÀŁšfôÏ_­ejW à’ĘÆ#oĘÈ;ăœ q3[PœvȆőg.d Ï!ù»ŒúûTșíÌÒŰE$‚7@ƕs鞮úŒŻ#_jvÿj»»Ó8Àš#šM/SFŁ ĘqĐĐ# ÖGÚ4qo3îP ăpïŸZç|Ûœ6ć/c1è0=2;P…"+ż”é— |·&F|u<•Ï­lÜȚIuŁÛÍ›AùæAęîȘ˜#ćWíbiʙłȚŠŻ#\ù©„=àA#VŒ‘Î­©Zæ„Ț+è‹ÆČüùă5­c4cŒöš^Țc”—€żJĂDZì3Y\€Ör‹ƒœOŻz«©EČ(”(ˆeFÈbžOzk`!Ö.ìo'țĐÓČW‰L=é4ęB;b-î“tRÈb˜șŽčÓ`ˆŒł]"Ż—˜òż=1MÒăĘ©!I™H?3cn;ę(è]ÌG%ČQ»/űÓžŹSŠŒÏi DŠ\üÎ8” ÂĂ9·œLrSž§č'©‹v-ŸNMJ±™>LăȚŒĘÎÆÉ‚”ˆ©đŹÛˆ9%%ŸVCÏ„Rűˆ[3€°žčût–ĐF<°ä™ÁÀ«–ó]Nòê€ȘÛ]€lgœvœŒcžÙă¶șF¶”ćŽ< ß<OÓÎŃæŽč“_0ëȚąú șŸŃw v…öìÏDçőȘwz’ÿf„‡‘ÚÄ»„ù‰úĐ”%—Ž›ÄÔtżì·ț=Ô°=IçéOӚ9ŒÚL’`ęôŠGoʘÈd{ˆ­Œ˜QŠ$äNϔ뛩5}:4<ŐVÉPs€xąâ{›~čH–ÓHY…čŒEœsț{Ő+ƒMl»€˜ç(>êß4Żf2§Ù ż·M†yÌ ü­òŽÜûÓìƒÚȚI€OrÈžRÍòńïT”«[ˆZ)œł„Ú€…p9üčŹÙŒĆu™«e=E A2iäë9u'ô>”kBŸycm;ÊVS–%#±=*·5o.íőKckłË1 PäÖm”żm€é’òFîçŠ=© KxźÛJiY!U@;›z -JÈösÊCdđ;ÓžžĆÛk$ș-ÇÌXâË1lcńȘ·öséòGr’oY,Äò;b–šö9"ŠÂȚăd7Ț\ńÇR}kgj„B7 ~b)‰œM}SYÔu)ÖÒŐ:0Ìq󾁊e•Óéó›K„”‘&yĆčjè ö#k$Žź»·•ä Ԟ#†ÖæšÁLˆ«ž9<ž9ń€&Î^öK™ˆVÈČTt"ÙM5‰ž„năs}{U\•©ìRCŽE$JUčÀăiȚèsäsúUKĆ Œ}E.ىć··š&«"Łrźq‘šŐČŒ’áą•bÊÍIœnç­vIègÆÙC}m;ŰÀKyÄp}{fŁ[Yt­f8u,ˏ0+gÂłče¶DŃő`±œĆ8!xÏc8Ș>!ÒäÓe…l°ÉƊ}DTÒ.äČŸÎ:©äš­Č.t©TÎáˆö<óM‡Așšș2jvŽ@“€çŠœĄÜYEŠ}€@܍·NÍÉęj: ŽÚí4ÍhÏ ‹ć3‘žÎŽŻłdËȘ­?]zÛÓ41r@öŚÂòʝfî6œw>Őí€pÄ·žƒœŒüŒôśŠ˜™©s§C«]E8»òUă;‹Ž29Ź›ž€•ąX·*ŠcAí@Xą<ŚÊV_™Xő$mÛśqœñ Q64mä‚ÖèM4O‡ čőéÍ7T·JŐEÚÆ ;nUíƒIn2mRT„.­c!VbHzƒÜè*œő…ŐŐŹ”ax\ŸŒőś4ÁscȘ۔š8żÖ `ŒqïŠĄš\4lfčł 0CœM1lT·Žžűy‘E»h;4ę^8M”7’r Ï+ìjźM‰4ûô¶”Û'Í^L‚œìnÚYZÜ"šrsŠC{6ó OEi 4(qŸâöšô-DOg%”ÎÛ[ „wOz·35­"ăNQpĆLrŻÊAȘ»Z°‰X’NYsÁ\[ŚPÄÆ9š9dÉ<šöŹd•îh¶Ä`±< ÊœÔśLaòˆščțŃ€ŃÂxÇMĘȘJê@ ƒüY銋EŒžk)tá##©Ț€r?•u^ń"ڛ-3Æ|æ‹c<mßíÜúśŹû‹d’Ä\䌖çòš)—-€ł—EY.S|êvÛàò Iâ/6îÖÔKŐ)ó°Nr84úˆÂò-üőhłƒË7 ­quawgFbd‘Ï›ŒôÇCùUtĆ»X=“s&@*9úš„itś0>— łlŠ[ym/l1Ă$móbKžčÇjÛŃ/ą“Ht•.@Ú:œLĐÆWŒK‰Ź<čä/ćd©ÚFÒ}j…ÜŃ\i"W»#aÚ±Î{Ÿj‰tÛŚ»Ž{K†.ûG”OaéÿŚöȘśÌöè©„0äî?Šo"4d˜ÊrȘ@he‘Œńòœäź{И„{čŒńĆ…FÉÎ+Z„ŐídŽO-šÀ$|Ì=œ…1 ȂÖêá`A‰9N€Žæ™„"Im€!Վ! ì;šÁźŠ°\Ž^Fnb~ zV|7€O4æ%%ș)\š=3M •žőŹæó!›ppV\ `T"kkhȚÊêaÌrăJ{™¶}ž3†ƞąźxngžI4ęߌ˜c%sIì‹;čl”-ÆM±+8êȚĄmoȘ\Y[í*H•‡ńg‘EĈőÙ#ŐaX‘˜ÉJÏÊ=…sŚ2_+iĄÀÀëLMÛLš ćdț$<ÊJè€őŸ.:Ő;­*=†P2ĘMs8=Ía+3ű‡§Œ~Vą„‚ÒGŻjÆđÍÜș€čš•łò©îką:ÀOâ7fktžO¶‰ n„ÎOjźĐéżh1#âWLm#OsSÔdZ*âyíep ƒć1ułZZÜê6óÙÜɑgßőëőȘ$ÀŸą•`aŸEêχ€Ig6Ínž2«ÏF뜚}ŐÓㅼ<뙒2„ŽƒžŐ‘â/ì7òrAo“9€›žiæÎÚi ÂŻœŃmËśOŻč«mëéșȘ€ûvźHVçmVàlĘ][-À‘­ËC3d9-Ÿń5‰$_bż”¶ĐïŒ#sŒTˆKÉâƒQkíĂkQsÈüȘÖ„Ł­ĆÚI4žt—nxçéU}Éò„‚ÀexÎCž(°•¶ŒRoóƒoÓÜ:–ôś{œWÌò@‘†8àš·amŠž±K2ˆ;Ùúgu ă’Kma€‚F).pqś—?ęj_náŐ,‘P]"è€źą{ â8€”Cw 03ÜÖCî‚MȜ“ó*ƒÚĐ=ÂęĄčÓŁû4dJîT^ą™%ő„֖Ríæ#ßęj: ©Z{ȚÍnS8΀â`š‘5ÀžetûŹŽźĆÔ»h–ńÄłÍ+9gË)Ö”\–ê Ź-ÖaéńĄ+°E]3RžÍÚÊVʏ1ń–>«x–ŃlŠóeYNIÆ2iĄë”Tˆ@IëC ©Ő-{Ł[œçŒíÄșD„ŠJ`€kж’3r—–ê©ć}ęȘiü&s^ÆêIá7*Fûyw‚ç9ÙąˆăUŒ;xdx-“œûQÔcn­ïSQ…BBFé'Ž}ú֝Îč łżłó@òÊđqžțű FˆìÂÊ·qƒ±òUÇO„6ÏKžkEŸ„:/E'űœiÜ]MmjÆîöÒÚò8ÔAő5W]ŠSŠÇpŒlo|t"’`U‚Đ^ڇE‘ï üȘr;“VőŠÓă»K›5ÉHǘ„p3ßȚ©54ÛdÔt—ydDcƒ[ŽűíTő( 3^F^@Ł !ă§NԀ§{ _ÚÇvȘž…Béß·­\Đ”ŠIáG›xŒô4ś„ô‚Yšêçd!Wzbš™ÔIęâ¶ÏȚZkPd·w‚+TšȚU-żî*Ÿ—ń€”œYYËyŽy%Ș€łwo*Èće·&ӂTőȘ–·ÒŰ€Èч ›1'@qï@źK€BnVM6có·1ŸsÛ ‘4S †ŸhXàg–“Ô—Ș.Ár-ôó{dÛ% ŁçžżÊČâŽâ&óĂń‚}{ÓO@hčŠ@Ś ;›¶H1ÉgžőJê·;P‘ƒÁ=èiź,E‚D†Q6ïȚ±?.*DŒ‡íBæÎ}Ì0ŒńM ryRæ;èő]7[há}č«7—űŠ'ƒìùÚĄREÉ9śúÒ «E æWOL}'°ú˜^.%t»‚Ł€gÀ8‚Țgbžùńێ”=74ô˜-Ë„šș`$By‡§”hź— ôqùہ^6<?ˆšÈ5_ȶˆmÖLÉí IÉÓĐUM2DÔle…‹3Ä7·“O u,_ÂÓèër‚Qò±śçÿŐU,g’fHgșà|šž”3cMÔV|éwÛbòAhَ}ę}Ș„”ÈČ”•Ăe'?+c<öŠ#>đÏĄêBXŰ1Ă0Î})’Ę=ŐÓ^?ï7łŽM=„]đîź4ƒ"Ï8PFLžœúUù§Țź lÂ` ÆNёßè9  DŸč°’Căæ)ńùŃ„;[j Ă;d\eÇSUĐ ]M"šÎ;i?wćźâ’„‰éXMČndĂy?xÔR@3scÌHń6ćïŒv©^;WXőÏ2 FOȚçž{SEŸìZÒ”Ă`#âE#W[ ]íŁbbè=sLV\Ęi‚)À €„šš”â7"âĘpn;{{P­ÂDæ9X”—Ąi\%œúL·FĘËŁ*‡SòŽ?pÜą5†ŃmŰ.ęßx}à*ääê»c¶1 Ç»zdڕńșQ˜<ŽŰJŒf ÒÄI’<Àq‘ëGA-Í},™Ź€Č/»Ì»#±©ôžî4YMŽŹì±¶ Ș·ćJú–ÿÙÿÛC    #%$""!&+7/&)4)!"0A149;>>>%.DIC;ÿÛC  ;("(;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ÿÂ,©!ÿÄÿÄÿÚ È<^•—§Îș5ˆ=–"Š*[„î:BrYxöŻ9VÀl,+‡‚@ sr çS]CÖ9UžącB !AÍxŐácȚž‚ƒ ŹÊĘ’[JÂŐÎŁ*p–è,€ž?WWÓçJï=Ź‘}tJÌ-S/SHK%Ôö;ó%VŠ4KÁ;@’§;ÙĄŁÖȚ,”ÂΘô&ąXźP=“…_{či<œ#Š‘Ph&‡)bAêy‘ï^ßsŚy–©”Í xœ„4ҘŠjŚĐžzMȘÁ'ƕYą Jr„“Ï_QÜ”Ț>‡=nÌ*Ą.6ćI=#'čÜvžVâ.—rrƒb}]$ŰëŒXśO aËÒGEKeGąW늏eŽk,±ëŚŽüÈuÏP[ˆ€ŽbœBÉLśú8ôòጳKt«gG™PE©x€h{ǎü„é%æŰl"R)Jy> sŚŽ¶ńIR†ą Są7§GLK-ŸZ3pùóOC.€…©xƒS!Nr‘>„LȚFnìŠhHYDFN ‚kuÿÄ&!1"2AB #3ÿÚńôüK•ôĐ††/'ƒf‰gÆ<6HEe4!•çݚđĐü}ŁEÙŽ„QB(ŠÌ‘E‹kĆ êQBÂ41gĂŻQ™(y VhjÍ}<45R(ڊ,ëMHż2;u#;Ë+±GèfˆÌ€Đ•“X4¶Źù ^$±öłâÏŰu(ęIŸă§’'ŽÏQ@бȘ—}9ÜRȘ–Țu/o‰*dF‘ÒBBˆČȘϧr8ÿ©%ë>6É.ŸôĘ9íć XœŠ‘TäYąó§4áˉsF„Êw9`ńxyT•4ń(Ú>ûąivVăŠÚąŠžö”ŃČŹjT}1ܕ€ă’pÊ:Ä^ĐXrYWomgMjjž‰DX¶âšÔœhű—gÆ]ÈbSŽwŸđße44lD‘vFêF ,©hČ^đ§tlß”ÓZäV­MvŠtô9#$•À…WUăæwx”t¶Ę!+Œ^NŸéőmusQ{„‡7„ÎEBő(ÉŐ<„êKvÖeމo’]ă!;MZÊmf%2Źê~č\r™,ŁqfÉj6âIt’ő%čF…,ljÓÁ?eÿB^Čb‘É(Ńő)4䩯ɫ_KÜąK«țc†iö‰(‘ÂȘqö„°šńŽm€<7dšf$»xMTŃۗ d>'Æä± $æ©ÆŒFT†Rő”>KdŸIJ/ČłŽüJ=“^ÉYŸ->Eï$,Ɨjû ŠXäőrÄȘĐû'·ă>y =ńŒœŹĂ,Œ‡a;©Kä”]ą–cvÉEuŒ‹;ż2ÌĄŐG|‘©< čDJËN+źHŰÖ4ČIu”]ŚúîĆŁò~1—eđ–ÖąûĂâNș­+,H’±5Q—·*é9UŁ',šŽż%ûÇ&N;&œĄ.Ȓ ńĄżYd«ȎŸ$d»|âCăûN·ÉőÎÿèŸ“ĆKsÌ/3ź”ë’¶a7òqž+-“‹ÔÁCXæÇ6©àRìgŽ #qi§NMŽ"Ł·Q7Ź?Żê†=~Rȏő%é 7Bd•ł=‘gӖ.†ÈÈ^Ńćɟ?“ăăET“ï’SY^ĐÓíCfÒ2?Wß)Ÿź^#ÿ8î‘Ëç~Œ˜ižŸMqü„żO é7ŚúJáƟÿÄ!0 !1QP€ÿÚ?Pș·§*ł±bê/”OOäàû™đCĂI:ÒÄ~H˜ŹQÛŐOâIà[SÁ+“î?ڋš9-†IĂ€nNšŸr2ë=6F$đv­ű0G±äí0.Ș|“äđNćé+Z\ÛrÄĄń_”&wšà°Ò€èÉ#{ŻIààít’(Ïœ’IÁC”NôłÁäjK—ÉĘűÁaN+Câ ,ü“70`Ÿâ’5”ęŃ'’Kàkaü#CmÏŃÆÔûin9Bù;ŒÉ'ƒ}†‹Č>WM…ŐŁ ‡Ih‡”—”{…ŐčӔ\đ=‰-©-d—ùFŰ;^§(•©(g+Sî«Z^’6X’vۏÉMć#TwŁŒ{<„Ú0œ6ÁÛEĆ?cĘ„ppJĐkNąwœpœ{ąiœÓbv/äídô"n°Iàv!á–vőNŒƒÁrM.àćI'(yDAŸš±ŸGÒ'ŸEÎhÖæś%\ˆńI›Łțé:Ąą6ôÁæÇ„­.[Cr(żćÒy<’F6;·É$ą=ŒČéÒFű;…i‚™8hżÀúk;díéY;Xż‰:2Q ’ëÊ<àLÄîBêTc$%ù079#c2w©“čgS»I„sù?Ôያ܄>†yڜȚă^ĆÇ­&ëQô<—3Té/òcBqöž K$é­o†[=' •G$êŽíĄ‘&|Ë<ŁGì‰ÀÖćò©wžÒÓÏzăĐé<TđÄÖš•è~ŻtLńIŰÂö?ÿÄ'!1AQaq‘Ą±ÁŃđáń ÿÚ?!šŃ7b`œőoé(ö•Hś–„J»&_ÊU]fXÚĆ1]3Jí2`•Ë,?©^ @žqž±c‰QÇS,ÀäÂU˜ű–XcŒÊń™Ù05 »†s œ” Ă~§ËíŁÎ&ŒtBnmQû&eu©ƒfŠžeÖGù70{œÌ šiUĂG”ś(Éką^»žŒaă5.ò&ĐŻžánÍA&ł0`_siL!©pr±ń8gÔéĆw;GćLљVê>Xgű©Wf`ÖĄŻdô 2Ó*–ÙTœÇ  ȘŃt&ŠŒÇDožæńP?Üpâx8—V€è˜{ŹGw!+Ónû„“”ŸHwÏ3vł‚ â`ÇțăàRbòâ~„Ă(íîU—ą]áú…šj*ÿ2ʧrÆjćĂèŻQÛi^"…+Ț% _6KkÄԘê`ŒżÄŸA=ĆżŻ2Ț"~™”l1ž§6„^y"ÛPt|űŸä‹Ă˜rEzżæqB„ŹŰžWÚÏ1Z`f 0g„ p‡Ò2hÇȚŠSËű%ËqŠ&'˜4€æ{ƒó %^ŸÄi\őK 2Ÿć™ŽQüĂ䁓Ô}lj78#0W°šȚ”țTĄœûKŠ`ž`°°»~!+»–x–ìߎ„“é—âœJav&9–œbuXsó(舟FŒ\î=Ą(AQ٘AîQ–rśśÔKžq DÉßdܘráùžsÆI}’ëH›=M—ś jZX^jzŸólÉ2Qˆqë©j&^ážÔŹê oK†XXŐ·ó6Șó.[•œû”Úșf.cƒP ăŽjk|@4éŚčL†üb?l|B„˜”)ÓČŁx&ɗ[Ärđč&ńPcŠsRńÔR”đ€Xš–ź8–2Ń[…fł’ž—QÿȔg7XÔòœ˜aoLùy'‰ûBÔs˜(5‰LŰÉś”î6(FóÔșe­U @ú%ÀË·” `WIš~ D(ÂĄŻˆëkŒžfovŐ†DZˆEœ%đSґÄȚu/ź~„kÂËąĂx•ș5©E·Éë_Ł5ž­À*É„w•æ§ŒČâ{ ƒŠż-SNš…Xq-¶mnȚ‹fXˆCY1čœHń :œĆÊțĐÛ\f†e:b­X+ÊT©AĘW žÍC4üžă‰Z Ù_ÖQ.íœÉe­·@;‡ž5uÁž}\1Áä|LV–éš_eÀ{ń ŠWRŒ“›oÔӕ>èÛpÒ„·ËêPrœCˆ\,,E/"ŽÈŸMČżUÍqn XÏLBŸ€ŐŒŸÉEŹ[9ü„kŒț#)Û©I{%qô“ȘąŒ¶{ŚĐ\Óșée‡±W™\œ œu «ŰXč7ö3<'©úi]6Ț%FuûŁMWô,jàrȘ\)ž^§ò Ąz]À°›ŹLćïO$Wz­ÄbĘEś]™`oć—û̔l »}ńCŰÆć”_Ù(äö@ÀU™ń9†`Ïțą#òægGuÂ|€&ĘlęC –\ɏsÊÎoˆłA˜Ű _Ķ*<©Ș*ÌÇDQcÓ*Œ»ä†Ì#ƒČ „áÔ@đŹÌű†jőŹÊoàûƒhżșbûƒVDZ~„+«iœwy=Kš±Ś˜qHpŠč€KÚȝî\gŁö™bÔ°Pœ‘V7ù_Ä r"€ÈȚež’ç0q©cű?™†s.,Ą™óöęjüț‘ev.z`žÁÔwêŒĆE♊,ƁÜ?‰ÿ› (˜Æ{‹+g]Mđ‹t/Ț'ᙆłÁĄȘ8WpŸeű›1șÆtGe°˜á[ÒHĆY‹‡d%­Ő›Ž)@ßń+=x•IÏw/!Ö„€€©}bPàčQvșIr3©áÎ7O%*`wr€îĐ0òL(›ÉŠq‡ę‚Ÿmd°ÍĄq{ÂĐwÛ1 ·> P#xGŠWV„jùn1Ąź;žoÛèˆBÜÇÓ88ÈÎV0KZÄo‡¶ŁÔWAaÎÂ# J§Ì%œXE7*gôĂŹL–ìfω‰¶WÄŰÇŠwC=~”V/;šÍĆwäŠ&Pá!Ć«yš(òye)ꈚ§„‡DÛz̶Š‹)\șCŸÚ5»ÂJˆќË}ÌanśuoaÙ?Ü>ЃÔșl`šeŒó .œncC?€AQá^!"B±Îđ”˜pF%žäž7™e9Ń\6|Nc<žfmÈn.[!§‰űŁÌÿqę}\‰.)ÆetŠ˜ĆfjÌGcWÜČÍȘČ^ÌœG±Zûą`1ą(°ł5üAl+šB-pĐÒéw$(lîDŃíÇŽ3Gâh~ćű•ĄJ@ÔžłÄBŠ+ÊV«s2UŠÌœqÌœ3Gí09˶ȘSš ~ˆĘCö„Ȏe^šS:7ùE™ĂHCòĄŸ!ž?ț. _%L4ăęÌûG)6[ÂR-đ¶ßAœLù-ž*k9á4]çs1(žŠÒ dê0ÖÿŐ@ˆ1<Ż9ÜìČZŰțŸ˜1€XŐâšLœF’o7†ôPˆRb:‡5§}ÔbęÒ"đŐÊeÖȚ&ÛÚeŽ+QÚŁ'>&Ț§tz‹—Zs.G–ú‰Qč]KÛâ`±É=ij^æŰÄZș;”§K{ƒ§yEGd„/~à^ aÄŹ_;\É穇:±űqwRXśaƒŹPÒÖÙüÌ;ßÔ#V·hàፊáÉQ”Ą”BĆGŠ)$ –ŒËYŸĘëŠ*,ڐŠqîæ –3ˆ ûÎ<Ê]VI…șàeÏlU¶9P€2ÔUž˜sIÿšâ ›ó1Uá˜G’UŐwȚ„:KlșšS°­E–ê!w*V—ˆöŻJŠÀPhˆ$»Łæb‹iVf˜.ɟ‰™0üYQźsś0RÂŽąMțÏä…à”!Ł)8ÎH••æ™ț)˜MśŃ Yi‡ -Żâ4W%ÚKI¶Jń^'#cÈV1|@Č1êXœvKĘ[óUńùœ<&…©ŚšÓaÄekdUŃS$Öß2“qŐȘæO‡QVĄ_ÌŒ[xŽ4† śd·Ô«”ćźW[%IÔ0őUˆč9y•tEÈrÔ§’ߎźțˆŒ,›e|i섅ÏCUÜž{ŸQđ3^3c‚`ƛBŸpêŒÊpîăG üÄŃì#ôê" šą7îW†·qr6±‚XÊÓ;áZ9‚*Ëé©K=‚őÙ*€ŒÇ?XÛ?rQ((;.7ù˜[ž«ü ZÒ^ æh~bsœæbę\8l‡îL&î,‘ùYŒœäâOˆ}~’ÿÚ ÊûœšîÙźæŻőŚÄÊû+gBÜk_Ń*û@qoRÊNű?áÆžœÁ^ś‹@SZk@šrĂ„ì>æQÓfƒ©^łăł]>ôu)čïęćÁƒæI’ÙĄFiń2ąá>*Ü-¶ïu W8ÕeïŽ2-œr”à mçs§3…ś{Iś Țçę6čțźc„čr㠌pŸ—v°ŽĆöçUԌăÉ%Ș‹€KśFä¶ =Òxô© nŠ>R*Ł,€:'ïŽ)jŁ]Ķ€·éS@ ÿÄ 0!A1@qÿÚ?@¶2č|„ù_ńș<Żö{—‡îF="&]F\cę,c=€y‡ę‚0ZCMGUVB|€%Ą@Ÿ¶€ ~N‹žäÔòTž«ûŰ8<xȚ‡7–$Ź»1ŰŃфw4c«ąspÊË«„àâ”tpáîś‘ìćç^óû>đqò}äÏčtšágÖč1_‘ŽűÈh[žpœA­bEšČOΙ /1W”ś»NH„ÉA#Hs…Š +1“č.|ÿ̜Hœ'f$ oÉĕ€àyÏè1ĐÎńVòàcŸ?1+ٗ8Đ_GxÀÌ3•O»,ź)śÀž!h·‚ž+ą'gï"ĐHİFD#€ù” !kŸ›À:E]§X!eáęaÊëyzæÒL5Ž š‘ž VÁ6Ÿđł6b‡EéÜpzÖ);œ"—łœÌc¶žß|ëŚYĆ8Œ¶‰æe(‘ÜNÎŒDő­ăG‘Ă7n}òĄ”\ÎÌUL=Ê?­áˆ(2n;ämu!Ą„HˆŃŰÔ{ûâ&ˆmćNnr3‘ÿG%ÊőWxc†g§ĘŠÒGRpŸą~˜8 O«ćtxÉ8…@8ÆCrłVbä8È©!ËÛ hÆăêɚ „Ï­}đ°ADĄö›ĂZ…IÓ~ű%XáT—Óő ƒ 3\a"Ć@Î)wFoށDt*ă!7™ź}säœrÀh|üașj`†‡š(ÎŚ•‰0ôïŒŹ€MS À#Ÿ˜’‘êP?:br)gíÎIä—nńäŃŁæȘ›YëÔp„ ‚§ęÆ%E{țđ!DÄLĄł $‰zWàÁ–R-=rD‡bb/iăÛ$äBáìHgïœćTçç+Á$Ź]ńńÎÖ0Ird‰©aÎ $†œÏ˓zB܍à'@Á‘ûbś–ò·Cv~WàF2†Š[/Ś#BźïÇ&AܒˆöcŚ%# l<űĆbiB„qZáúă6bBŸ$‡ł«}ăÛ$‹”oY$Á2|>˜5tžŻ“’1YËÓ*°­šsŽ{ț˜Ż4’l•Ćńûʉ'€żł…=tC5•^Ï0âŠȘđ;}°šȘ@Ol%s\dlÚ©ç 6§c&)ëKï &B"žçŚ"â‡ûŠqFXlŒV;ŻoÆ̋=^†ˆ‰BRæ~tǒJ‰@?ÎÙÍțr`‚œ(ü>ŰÒ(ą™aF)żÁÏæđqCHÿŠBȘíȘŒLĐ:8#—%l’GŽ1" Š…č>ÿ $üᒷTêNEIÌ$ÿžŽĐ,źÏle€†Ž‘ëˆ9aC‘bÖč-L}_źÖŠGA»)c“Îvƒ sƒ‘ADD,߇ …_RńÇ&9(Š:˜hꘀž>žűC䌊­‘0—óûX҉Zt;çÂÀ,_3őÊ}E\`Q3/Żœ`rˆnKˆp’‚Sš"ő2œ€Ięc­lžĆÔ–ą­Œvă G~OŠ 3`(ŸLŠóXŚFœČ(Pf%ûúăbêWwX1f©ëùXÌQRw1ŸÏïä’ä…5‰ą ŻžqÈÁ'ĄMúäÄͰÏlkŠ jzmËf6Ń]OŚïŽÉdÙÉőúç4šÿ™,Pty0GCB–'zâ±&Rc@Wï™ęŰ,•4À©$ŠNžÒcb“‡ČFŠÀt<˜Mh‚ÒÔșs‹ 0šé?Ì ä‹Ew_Žțù/ƒˆ’%Sé{Ś­b€Ć '8:ĂÓLâA@ ïÚqàDĄÇÓöža @‰żÎ ŽĆG“éôïr2D »+œ`#)êôOYÇۇ…|V§ï…źÌÜĆÌÙ©éöĆ(D*’†œòHY#vû―Rùb°"3sМ7p‰ÍMÏnęđa"€ă<»ßœ$]OV6GR PcžĘ)ÈJOúÿ3ùž€=Ca‚Ÿ‘Ž‚AŁÇÛÎ űȚEëÏźJ]/Ž›ÂІd!›“Ö'`\LŃ>ó^Ù1ƒ"O›Ë ô0żż|B°Šp”ÓÛ-‚#ü^!áÏu„\ƒšó# \z—HŒ„ÌDqÓgÛ34O]sÇ$œœ~ÎOMVʧÓéo<™‡Ż\lœ‘Űń—łś€ ËšnęÈîe8„Ń2D_Űó–Ÿ6Êa{tĆ* P9ęśęćŠ0„±~ű0ԋVâźDƒnȚ~1Jn8‡“éŒ$€H2Łóï”Hd"ËïŠ„Ł™^¶*m]WŹăće?éy564<âŠőü8RÁÏIùX7cŽÈLV»aEA75nœŚZîuzŽč»’À38dï8çĂtő­çòqÌđ€&D¶=1„P „c/Bč|$“pÄtÄr6ÜûlúaRxù8`’ ©±ôS Ł“ŐÌKKŚ~pŽ‘x–ęŒzÁp‹b<ę̙D9 …đâ ŁgžĐŚL”ÏVJ/ÏœuÄw6ìőĂ„gáœi Ž d€Úbiúà$ÁÏG«Ô“}p3»,ŁżQùÎ GD†G3çYD†ážéë8 <1őûÙŰ©ń퇊A•˜3Zś^î^…±ŸMóóy<'‚ÆűcɀF"-‰ç·âČkʂY ż| =[RêÎśŠ’h”g| 5@t6ŚËˋÇ"ÔLč:"ȚŹ%€fUÔÉֆ$6Æ§Łöˑî<ăZ ÊE8ĄT—RŸhûà,e, òëL{`SŰí莊GúȂ’HŰś:a r“ETÍxŻlL2›_5ƒ5)C2ąxȘóąôC©ńùĆÖ©ś|śÉ€!B5WőúdžÆ zÇŻç î W©Ś Pr†·Ù±” $=Č@€Ń?Œ‘Ąëúțb>șM &w±ÒŒbą°D…a€ŒdTA"UŸ·‘<‚D*`^Œd˜”l GpČ Óđ}fæŸÜšÏáÎśĘÄ©y\=zd”$>ĆùŚÈŰ 8ędŁB‹,đŚ=pčA1” Ró_|љą,‡ÎŒ‡ •2qüŒh\S DđűăżL@4’ GnŽ•¶ Nśțbç3M˜„Ô=°„·Eyë…ŸRČĘïŽÏŚZ^æĘ·Ò1c2(±;L”@ †š"HÂ&œrcis­„5:D%Y;Zä‹››:û>Ž(o @|<ÏNcKÔ~ÿáVfÉëˆÀčš}k ‡€çźÿŸŰbDôúță„ˆXû8’òq&À•vżÎ ,ĐbÙŚ,x%ßgÎčqGĄżlȚšC«ż‰ûž€a5^ŰÀà0v›ç 0ÀuĂ^l ‹ΓÄkÎ0">#möë„Ü’ÉŰźw…08n:`RąŸˆí€N–>„GŒˆĐ’)3îzä|Ïü‹ÈU;œ8èe<ÇèàkRF»á¶îȚŽ”ÓÓ7WTÄssߊžÆŸ5돳6[Dued5&žùJ*ܕVncp‚’œ=cžùšDûk% „Tq˜d ÊžźZšè êé<Ćúâu«\‡ÎÙÂa 1(Š.MBd<†žÆˆ›đgwòïÿ/ź XV`±[ž~™RyDÏÒp‰H`s"Ś€kÛ$)š‘ç­ùË`ƒ€A&ńZl„Mْ`ÛS¶zÏß&Đ\âYęyÀlH­IŻkÈą$*P‘LțŻ$˜4“ŽmUPÙß-pA5óÎ@+ ryvĂ Pó·óE‘Ô`˜Â@šzč8n QUéŸÀÿĄî„dÖXy‡›ÄvU`,0±śÈVŽËŻjÂH§“ôæ14€ï‚O2ˆČFÜFœŸŰ ;Eš”_9Ç(<nC —EÖïèúc7z™1wß Ą*Pž ˜(0•lőùɀUj çéôÄU=^QO¶(^#e.›À”iKç ÓKrž{ôú rlOHϑțÿâKfUçÎ;ÉĄ u✰šÀ|íŽ/ ƒIŽ:xĘ뜔° t'ș{æŹ¶MËŚś•&抟xęcąK ;8Ÿb>ѓ’-›Ž;ă€áĄ;î^:b1C›az평DÙ&>xÈ2ˆÜ_ß‚S”}ț˜ÊÉȘ…GYó±jŽń=ógŽBQáń۟l0#$ÿ9Ü}póÿpŰ ŰšxĘᚠ8Z‘0`gcz }|™9y/ON…ÿ1`ҒûuÆS"ʇMśź»ÆR­Ł‡V}Ÿž Q"Hš9A"D1@˜ˆč s­u^&~œÚŰĄÂL"Íûă,KƒôÁúKTSʁaDëß#ĆÜŻS…DÆÊÆv_rB]âbŁŚšm:šĆ€–°ÜÔêyŒVFu)żź2 Z7ș29M„菑‘%8™űa„…8"Jùă‡;…ŁXŐÌc ٙɯ$ê1r`țpàŽőĄLûO1é ŐW‚űüeûăžŸlwöÈ#ÂebCÛ+¶ÈI•Í|Ț3da8dÀ «‰œ€|bÙîgg' śd‘7ŽZ™ê/őőÁpUxvî§ŚŃ`·Ă*ďŠ}p"V-ĐȚ«ÆLY%)}ò‰b ‡iúâ ÆŹ(1óŒ°älÄ>ó=đŒ„”î’{Ț#&8ü| ’ÎHFöV%^üôÆ…Á Ă5EćFë€Èâü[a0Ö-bÈîęę°%a“ó›ô@æQ–§#ęX„ĘÛ ĆłxJȚ4u—üÉÈڑ ÂŁŸÎk­ DŁÆ>A1=«©ì`w"ZÛää ҂Ž#Ÿlk.űp4ж|q°ûšöPúwĉEHŽȚŰL°ą$ûqęÈÚáûbK-ÎÆ§Őƒß!°ô>2KrE7Óó‘,•|ˆçˆôÄ`©qŰ(YI›lMN™ĐëïŸÂpi©§LY\f‚@RMœÄ5'造ĄƒVùç}7– 41œXK őÔóß'Œ“VŠâ1č#ĘŸxÇS† ™“_Ÿe†ÍœW§û’9Ę!ß~p†4Ô=1ʃŁGł0ńMœGæđˆ3PÄWwm bèńxX“1 MH{ä1­ ągÉÉ8RÁÉ~RBűđäőü°›»€Ÿ8"+‰–ńŽ *vŸOź:oyŸĂëï+$ żÜ˜ƒ)ÂŽč ’ ‘ GŠđ’ „ÌÌČW¶)P’±+獱™dÌGŸȚX”ÉíÀÔÈÄRԖ8JÂb)K]ëÛą„ŽÎ%ŰĄ$‘ìá+ê ;ô‘x[ÙśĆsÀÂÉÀ|çކșƒ>xÏ@·ÆJ‰ (SMŸÓ‰§Jùšœ\ŠU‡"ËM2çLTE4ÀŸuïę/űöHo_őŽPÚöą=Êá%éÌšWńŻèȘܶŠgûYÀ}9€T§Ís°cÊ`= *™ÁXˆ-l±KLaœĄżÚ•N›’™‚ê(šö1ôäG.Ԓ–±ŐtŐ .Uüæź”LŻ^9ÜáțMggäËNł&ű8æ6pŽ7lm럄cZ…&–CčT-n„FÆ0ŠŒŸ*ÌÒońuëíÄĐ­jوéϛšÓXČ&ń|#€Tä2y1,ÜŠAßłúò0›"ŸÎtŹÿh„V)ŸÒÔĐVč[J;ß՘čp© ëărŚÎîuN L;MPAmÆ$2ßlwŸȚ3yHÊ(’àCkRvFßäȘș ű;5ÆbżæÙјœșyÏ[hT(ŠléĆ܂;ż{,üsÇcAÀ˜yÛ`jMœ†~v5w|́hÜTíÂv㟱ZÈÜÉ'jF±ÚSW‹ 2I}"©ŠBÌVNnx8{‚òžYZ„ç żò”• ÍX€qĆPÀŻ—È€ê™Ął}GÁ*Sš –žĂXϕŽòÎ}îĂSfò$NO%Ś                                            šmoovlmvhdè @6trak\tkhd@ÈÈ$edtselstźmdia mdhd_UÄ-hdlrvideVideoHandlerYminfvmhd$dinfdref url stbl”stsd„avc1ÈÈHHÿÿ3avcCô4ÿágô4‘›(hß6@@ŻÈÆ e€hëăÄHDuuidkh@ò_$OĆș9„Ï#ósttsstscstszstcoAłtrak\tkhd @$edtselstö€+mdia mdhdŹDČ@UÄ-hdlrsounSoundHandlerÖminfsmhd$dinfdref url šstbljstsdZmp4aŹD6esds€€€%€€€@ôó€€€V怀€ stts,@(stsc,Èstsz-stco,V=udta5meta!hdlrmdirappl Úilst©namdataTitle©ARTdataArtist©albdataAlbum©gendataRock trkndata diskdata"©daydata2015-10-07%©toodataLavf56.36.100 ”covr ­data ÿŰÿàJFIFHHÿÛC   ")$+*($''-2@7-0=0''8L9=CEHIH+6OUNFT@GHEÿÛC !!E.'.EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEÿÂÈÈÿÄÿÄÿÚ ·*„iȚ[Žsy Űb ÌÎcq˜œŽ qÌo"‰ÓY 1\I8.Še8ŽsqZÊÉ0vÆ8ˆŽ“ ó8Ÿ+fD$@k8‰†&`Ź•CҍDQÎG—€ó‚púk6ĆŹÜŒ “„)2LHàvç0:ÈăaŒÚFÆàÿÄ&04@p"5PÿÚꝣ„aȚčź1 &àë4ÌDPâ$ȚőPŁ/W(ÇŐm[ÇRÀbkhŽ{iGæÒđŚnȚG'%™”äÙć8E(·vÙ6Ž…=“Ă1V>o„#ÜIc9ύ[EŃ\ĐÓ5ÔÄqFŠ‚[هqčsˆS X@15üÇűÚȚśZ•XùŸ•kățë/uŠ™.+Lć[EÜp]ŸtvK†uÏ«ËrÜäÒ ‰ˆ&™š lBŽRl…a¶ĄG‡ćÇÿÄ*!1AQaq‘±Áđ@Ą 0pŃáPÿÚ?!üœeÊ#AIUZ^M-ƒÇÈIÂhĘQ3fʄqš’‘-,MRŃ1Áčš˜ąqÖ§׈ą`Y1ÒxÀ,óĄŐᘄ§kZZ #w"ĘᇌǐèL8ÜLś1 $ Ȉ„›H&Oșđ됞@/ś @ŰïsêHT9Â8Pv Žß~»ČP.Aö]âMŠÙüP'„?7f$•Ï`y[őPCÜB=T9 ńpùżș ŸEš2€2gxmŠÆ \ćbńĐÛččuËĆ@Eš<è X9ȚÀ‚ ă ÎD80“H?čÙ4úÿ%‹)–>cèÈ™‚É $}·Qìz@ Șłcç9‹`Úerȗ 3”_Üb‰ÂÂx°ŚH4è]Bœ$ Z&€ €y8-æŃłŻ/šđC\«4b3Ą%:`UD23/gèj‡P°—“§Àšï<Ęűg™Șj=Ș;Ɲ ź_È pB>†ì3L:@ĂŸ…óàzőtșžÁaŸt˜ {8CÚUńšđf‘‡ńĂÿÚ $$€@ $@I H  € H€$ÿĐÿÚ?ÿĐÿÚ?ÿÄ*!1AQaq‘Áđ0@Ą p±ŃńPÿÚ?ęÎ+VJŸÉ»ęQ„ź )Ț(Łu…ÀÔʂcӑ«t™„ä Őt(+P€ kHN@KËj— `ÄĂl4ĐsgËDđtîR€«­qà]ń4=ĂOšf?P.ś=IșÂۋˆ‡™âŒìÌDäŃ€DëO!‘sig„–4ŽiŒB3V­đÍgÙÁKáE˜kű +›ÙSnhŒÜ‘Ńr- öhÁ,91P—ú$Ì#úRÛA KktšUqŰU‹ò)ÖitI(™Č{&t©)»0:ÓVŰő†|k^3>Šm8§„ł+ą|iIF]űoIÁŽÛÜz+čS'aÏiŁ@òˆÖ!Ó5|j 3«^ćÍldöOł%8t8WWm ÈJù©- S žü+?;Jφ”í$„rÛŻ¶+t`fÒLŠfĄ]ìĄk„6“œ=ő r% @™ž’ăV‚ß……nÚŽĆ5(‰?ĆkŠhRÿŻù&ɶ‹ҋ•ìD·èRw”€šKš`ÊÖ石XČnXï* ś‰è)]¶'äu;Va$ ŒLÉæ‡¶Cî­U(ÌXíGšąű!šœŐlˆ ™~ÊÓ/Bt&SÍìŰˆŸ/(±;Ű˜}hšŹ Xc„>Ą ËJĂЇˆ+W ÈJ­Őrżc AgWÜJÜK"ÀD™»= ùőłˆ;S‘ƒìV ۏkȘ6TŠE6TŠ>äˆĆăˆ{ęŒôƎäĆĐë@\±žÂbéĘô#“±’4€Ü †gpđŒ„ȘĘW+@€(„pÒĐÛ)áÀ~((Û1@»»ŒțÜÿÙ$aARTdataAlbum Artist.freemediascanner2-0.115/test/media/testfile.mp3000066400000000000000000000242001436755250000205360ustar00rootroot00000000000000ID3 3TPE1artist1TALBalbum1TIT2track1TYER2013TRCK1/10TCON(7)TDAT0306ÿû0ÀInfo(!C $$$**00066===CCIIIPPVVV\\bbbiiooouu|||‚‚ˆˆˆŽŽ•••››ĄĄĄššźźźŽŽșșșÁÁÇÇÇÍÍÔÔÔÚÚàààææíííóóùùùÿÿÿûPÄÀ€ 4€LAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄ]ƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUULAME3.99.5UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿûRÄĄƒÀ€ 4€UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTAGtrack1artist1album12013mediascanner2-0.115/test/media/testfile.ogg000066400000000000000000000347471436755250000206340ustar00rootroot00000000000000OggSÎFÍŰ-”vorbisDŹwžOggSÎFșIHĆÿÿÿÿÿÿÿÿÿÿÿÿÿÿÉvorbis-Xiph.Org libVorbis I 20101101 (Schaufenugget)GENRE=Progressive Rock TRACKNUMBER=1 ALBUM=album1 TITLE=track1 DATE=2013COMMENTS=This is a commentARTIST=artist1vorbis)BCV1L ƀАU`$)“fI)„”Ą(y˜”HI)„”Ć0‰˜”‰ĆcŒ1ÆcŒ1ÆcŒ 4d€( ŽŁæIjÎ9g'Žr 9iN8§ ŠQà9 Âő&cnŠŽŠknÎ)% Y@H!…RH!…bˆ!†bˆ!‡rÈ!§œr *š ‚ 2È ƒL2逓N:隣Ž:ê(ŽĐB -ŽÒJL1ŐVcźœ]|sÎ9çœsÎ9çœsÎ BCV BdB!…Rˆ)Иr 2È€ĐU €G‘I±˱ÍŃ$Oò,Q5Ń3ESTMUUUUu]WveŚvuŚv}Y˜…[ž}Yž…[ۅ]ś…a†a†a†aű}ßś}ßś} 4d  #9–ă)ą"ąâ9ą„†Źd ’")’ŁIŠfjźi›¶h«¶mËČ,ËČ „†Ź išŠišŠišŠišŠišŠišŠišfY–eY–eY–eY–eY–eY–eY–eY–eY–eY–eY–eY–eY@hÈ*@@ÇqÇq$ER$Çr, YÈ@R,Ćr4Gs4Çs<Çs=:<;<=:H14&,H;<<767:<=LŽ.H=;> JńŻF„ÿ«!*ź €€YiŹgf-?ș%#EQEccdzíGă ÔĂ ±ßF\ƒ›\2/ojŰŻétê`©ÿ‹~â-oĂÛź„T†ùWă*Ü9P‘ÍmE’ìętÀ.g,Ș­ƒÏ4‘om"úŽń"‚ <òÁ-áG†ŒöĄ‡ávw<$đO«M¶”țœ—æîb9T|d«™™3ANÁă`V§pź:âe"îîîsèî > N‹ êč\é+ŽĂG“~5ĐÏzP€ŚZX>ŚżïÏőŒțÿĆìXOê > Nś‹ êč]5ÒW\&ęj Ÿ ôŚÊX_jí`ÙÏŸ>ő„t?Ń~çÖ{-7> N‹ êčl#é+ŽëŁIżôà.Ő°~M–ƒ“žÈ \é¶ V X‡†ü> N‹ êč\ÒW,ŻhÒ/@?ëwj°~ «Î=ŽbŚț]2™́?ž> N‹ êč\Òw(ŽŁŠ&ęȘPe°NvXčŒÿős{źśYl3FmKT> N‹ êč\Òw(źàŠ&ęȘPe°žUX °òòńr֋<æ™x3Fwá> Nś‹`êč]ô•AŁIż €J:€w•%€őĄƝD‡ÁÙ'<cƖ> Nϋ`êč\é+ŽCŁÂ2* èo^#Ź›Wx‰Ăű‰rÔœ€`Jo\Ś Žà"R€ˆîÏHűÙäòBÛ]Ÿßč3ȘëÒ€„‹tÎïjìï@ßV?%–ßv[lèêȘȘXEQeîŁ5’MMM…Ę}D†ÉWź>ż–ra îm0żę˜ë^D†‘ßčúì[šŠ î~S€uÆÁwvfèĂT†ńÏ\yömäÀŚW,—ț1ŹąÉ<†ÁÏÆ5ű6r` îÊ`Œ<`I-L†ÙWă \K30Æ-W6@ûó/E+Ś$†éŸkđmä†1nÉČàżúæM5^$†©ï\ÿlí&a8ńŽ€kđ/O)'ŒŸQńŚĆćúÛŐJow]\‘5&-\^uś„».Xőmn#î<4yűËWŒé%:slŒë/yȘ#Öđ!pû—dćț‡<ÎőQç ŒœïžžŐĘ §Ś,nàéśĆ«}#VÛÿŸčƊÎę·ÔmÍjyKŸeê_ûŹ$nÍWăŻAÛLqÚR$aÿŐW^JêcŻ%V9Îz â ÚŽè%ącží)ăÉæùÍtžçyFQ Đévê'ÿ#ę^ Ę͗żżżèȚᏟ”R]vŠÁčĆĆżÿśWż> .—_°R/c$=E)c€_ Đ?] đŽ`”Ę~Ÿ>ÔÖ{r”;Űgú9> ź§PF/ăBzŠÚ˜~ĐàćËî{·ŒKwúĂ†Ś›žIŸ•.Ž> .—_°R/c$=E©c€_ ĐżSàfWË~öő©ŻK/ë耣z7> .ϗŸ©—VIO1Ž‘~ Àp%Ç}9ˆŒ"2ˆÌqÈ芠™;Ę > .—p©—M%=ĆÀ:FúUÖßÔP›ŽÚŸxkuùá‹'ûj”š> .—0R/cGzŠČ‘~U€u™V5rŚóçśő\ßî;ĄŰP9y„> .ś—ß©—M$=Ć#ęȘë[U€‡áȕ—·_—^šś:—6H&> .—Ÿ©—1“žąÔ1Ò/Ücy^ëÈ:Š; Â`îME”ó 4>Ț Žđ%RpaÎÇJyïrù6æu}çΚźkôy ° ÂđžvŻęôÓó=ĐÓś#ëȚ{ï§ßr4bŻ^œ€żÿ $†ÁGțđì,Ć cpçV€xgĐ[—†IDŠÙGź=?[?P™Áę5pțûëŸAC64ŠńGțđìlę@ew·•@öÁ©·z&T†ćgź>?ƒ{Œ‡Ÿ~zèĂD†Égź=»6șP•Á]o·\‡†TŠùGź>;_šÌàțjëü7gĂú°D†ÉGź=»6z 2ƒû[ ŹȚMÖĄ‡#D†í[ăzÔ7ba îÊjŁęś–aŸêŚlb©žŰ–A…±YșUܱÙ[n‹±yύ?˜œ·ëò·ŻüÄTxbWțZ‹Ÿ‰ÓTá‰őÙ­Tę ‡ @Ț4àÎÍÙ܈9[ęVuE)ŒćüCVûŸœwu]ŻCoé=dŠsî=  €œßv»žGćâæŐi`uæ?’äŰúéSÿ"ÀńćÔA/4țsÎ>câî>zI|bßFk· cßĂ1XgJ\”yŸ.—Èu±Ôyśï+*–ÏÏúŽöMrhôû[$ü„ŽăìüæÍ<Ïìü§sđ«łKP  ź·È͋żç|aÁS6Ûÿ4Ź€Ź 4č`væ"> Nëć·UÏgHßă‹?Ú~À›ú™ „eX†Ö.ê<ŐÚ玀ćqç> Nûćw©žś6’Ÿ_4ƒŠö žŸ țòŰÏjZ+JWŹèÚe ”€eĂĘ> Nûćw©žś6’Ÿ_4ƒŠö žŸ ~xœ€[5 Ʒ˶às'- Ví=> Nûćw©žś&Ÿ'/Óąíü<p? „eXR7tÀa fk@Š6”> Nûćw„žśh€ïdjŽ_@ÜÏ€őęń ÊT0!UJsu‘vM€pșÍ > Nëćw©žÏ&Ÿ_$ÓąęžŸ >?MÀ\.€oû_n:”C Eum·> NëćwĄžÏ&Ÿ_$ÓąęžŸ ž]pż†VdĆ,9Č2*bûg> Nûćw„žśĐHß/ÈèŁę Ÿ ęú5đPÇ‚)„J&œ<ï&Ug“> Nûćw©žś¶‘Ÿ_$ƒ¶đú™uzÀ’nm'à”Ž àU€ő€Á öŽáEš{t>6„Ÿą4hŽăőíÛă8ŰăÏ_ź੔>0áLž¶6.趗ç/;—oŸ^œ‡Üżżżż|né}Â1S_òȘx wŠƒ·Íy2“QÚÿ?Æ'­¶őÿ·fJ”śśƒ·IvóÿôĂ‹f϶EUÁô Cș臚6ÉüđᒶÇgeËć2Ä_O4đlúŠE¶Xîâs™â ô©w–ł1L3—"„'ÓÆx±]ÙG5č«u68\ô‡E‡ĘÚLNùÏ\<ś;‹fźÁÍĆ#‡ăÙź:țÙVò.iŽêŻŚĄ…ÍtN2l~Íw#Žî„šÈűúŠà=ŒfT;)ÚöáMČŐèęzEűK[>îȚœ; =ü~!>Ź(ûe\~íŸü7]æțÍ4ÔTšÍfŚŚË…ÿțÂ,”$> Nëć·UÏgÓHß/’ĄŠö 0 ŸÀókęçî ÀömŻ-Æű-@˜5âA> Nëć·UÏgIß/’ĄŠö 0hYŸ:x ÿĘ Č"+łoäz&M‰ > Nëćw©žÏ`€ïdĐhż€ Ž,€őᓠżì˜*„‚čÚț Lę‹Ä> NëćwĄžÏˆ€ïÉËPŁíüĐ€JX†ehÍÜ€~»”`‚™È> Nûćw©žśÖHß/Zö 4 |ë±ęe˜ÈȘXŃț»œk`‚iŽ > Nûćw©žśĆHß/’ĄFû~xŹ@ٌo—mÿúz«u`V*> Nûćw©žśh€ïdhŃöȚZ”a–„­őEô—]ŸŃ€ ŃŒüń> Nûćw©žś&Ÿ_€*Ú/ -‹Àś—€țwWÀ„T)Ì·Ï|­zÛź5> Nëć·UÏgÓHß/’ĄŠö @?€ÏOè?ś€ăÛŸíۛTŚ”fç=Ÿű5ÜMą{æ‰[Ò[ö>ZżX‘loaæű§—6ÿł:ԞęĆCłw€cq{è9zŚ-?w_gZrˆ(Âș±ÖĂ țÖDĂț\1œóăâžĂϝ‚û°üW—`\Qk”Ź),GźgÛł‡ïż–míÂCęâÖz ©Äé$«ùùáő/>ăDQw1[>öÏ0A±dbKŠŐŁv4KȚî {ĂÊk?^îȞ‹|cŠi"żŠÿÖ&ëčÈÏÚÔÒ&òk˜ș”©ę9§ŽfMKi‘Ÿ‡©;ŸÚĘù9śżÜ“ęyđżò©]›’Ÿÿ‹-™VŸS‹ìÔ­Mdođ/LA^|D2ÁŻzŚÂ ›Ÿ=ž»{ù‡S‰uT•ŻÈ·WŸ"ß^™ő·©ÌÜŚžÎDvá߯ôßŰDlewK3çíęËhS$ÚŽÁVòFŁÏ_ÂOjhÌoȚŒ™ç9˜żÇ  éî„?˜ûДlțÿęćïögłÙl6«”ÊöÎG/Œžžžžž(> Nûćw©žś¶‘Ÿ_4ƒö ĐÏ?ß}ÀSM[0Á.g|ûÔ žZ ș±ś.> Nûćw©žśŠ‘Ÿ_Ą¶đ~ę €P†ehfŒžjŰ#”@ôźÓó> Nûćw©žśŠ‘Ÿ_Ąö h€~ôŻw@(O Á”R%Lֹь‰ŸOggSÀłÎFdDìŻc<<=:97M”1#K<8=>:==::<=:=F«1P==7779=9=L¶/L;=:==;:85> Nëć·UÏg3Ÿ_$Cí`@?€Ïžèï{3Û·}[Œ9S- <Œè+> Nëć·UÏgÓHß/’ĄŠö 0 ŸÀÛSęçî Č"}$f§{$„YùŽ> NûćwM=ïÁHßÏÈ ąę*ĐČÖÓû ÿĘ0!UJłï°G*zÚm> NëćwĄžÏÆHß/ÈĐąíüq@Ë"€èNh•­ô—] Lđçç‹> NûćwĄžś%Ÿ_$Cö Đ€Ÿuę@™ŹÈȘXŃț?ŸŃÀì‚> Nûćw©žśIß/Zö 4 üù €~X@0~Šl‡ë–Đ€ ©oȚȚűuÊ]Jśèűí…đ–[ ”_ŹŰ^ČP9ìę?ëôđÿò|êȚƒŽL7ۙ5sŽÎ‘%ąȘHbüöù[őȚÉR 3”âț:țæáŸ'5Ë#\lśĄ{`Œ”łžŐ_ę]€Ő„”ÖZ+ ˟ăwŻI€äàżŻü·ćŚùĂÿ_Ù$“éH›żùûŸUűŒtÎ}—/xFŠżŻšXšX2ę}¶Äà©őLÖ3ŠäEàÛ]Šű„ŸĘĂ?kƒürSûł6‘_Údê~ÖŠä—6ńßÏ4Ć/šòżžË?ӔüzŠű™ŠűÙ=LíÏ4ĆÏÚdjčáÇs[UüĆOăx/ŒÈXČŻY%žÔłÁS‰óÜ:MŐü$Oú$żÛïöÿ‡ ?țIȘ-țțÿg$já?ëđÜSŒÍČ@ź\><9$:ÿ—ż.B™šÊ¶_ÏÏ ÚöÊMŠŐèóś-áOUűĐĂpśîxČ g 8*Č€ôßÜæ…ìÔăÿKËlțüÜÌÌLC©T¶ś>­ȘȘő:ź"V> NûćwM=ïŃHß/ÈŒhż€€ŸęëĘ`«*SJ•Lx°Š6u„,:š> Nëćw©žÏIßÏÈČhû_ €ŸB–aIöf%d­ (4Re2> Nûćw„žś&Ÿ_4Óąę €Ÿ~»û€“Z"+JGÖé!ƒ4ČőB > Nûćw©žś6’Ÿ_4يö H?üóźnŚ1À»œńÍĂ.[Ž äÒĂæ> Nûćw©žś&’Ÿ'/ƒŠ¶0 ~@X†ešfCÀó:Và|-ĘŸ;> Nûćw©žś¶‘Ÿ_$CíĐę đïŁÀS [Š”Ș˜đșȚÓZ șáî,> Nëć·UÏg;Ÿ_€>Ú/À€~űűtęĄÜ°}Û·ęžEŐ úș“g> Nëć·UÏg3ŸŸQm?àÿè'€Š^CÉdfˆ@/Ï>‘&ü> Nûćw„žśIß/È ąę*@?Àúțęçî 0!UJső›.fiȚűuÜMryôęû–ô–ûu?è:€ă/@ Țóśÿ€”Ćúéo“°FÀÍüșHë,+ô-š–käąć{íc€T"|Ț%6òśleĘż^ûqÄ-Șp†huü6F”ÒșŽV€Ő?_żžyśő'ku鋌ì֏ÿ|=Ł«ùŻ \qw öž`Ÿ œ}œèKJđòù™†©) Nûćw©žś¶‘Ÿ_PíPú°Ÿß)űRn&€Jé`źȚő•&Ű_/> Nûćw©žś¶‘Ÿ_Ž>Ú/@ę đùi^kŰŸí;|{Sî8 öăNś> NëćwĄžÏ&’Ÿ_$ƒŠö  ę đìڀ·uŹ€Y1K6>B€<óćš> NûćwM=ï!Ÿ'/SEû4 ę èŚŻÀœuLŠ”*„Â̕·)rȚç­> Nëćw©žÏ&Ÿ_iŃöț` ę è%ʞ€­ȘÀ %B}¶> Nëćw©žÏ&’Ÿ_$Ëąę €Ÿ~»ë€Ź5€ÈŠŹÈ:œÆó*©â\> NóćżUÏg €ïé—iŃ~~űóźÀIUÀÛ·ƒù~ÂZ:Čő > Nûćw©žś&’ŸÇ/ي¶PH? ”aÊ4ž]Çœk@È„/> NË楞ÏI߯‘AEû4 ę đÏÇÏëX ˜ÈŠŹđ敜u@tăòȚvüMrÓÌ·€·ÜbÔšëęû~&|>›ô€Ÿž§k‹Ó/9ąïlĘ~]Î6ÖëuQ$o«”Ö@k­”ÖZcĆù_Ë.¶{“=Ă"Xę*űCZk­5á€Ő?żŠÍgÿ] wżțzÛÁăӇ§u”©A»đŐëăÐűŒtdUaú»Őû\|ö 4),.JcSĆë™LM1°žŸ”©)Żçg“©)ÏÏτ255ćÇęüüüŒajjj Żççg7&SSS~ NëćwĄžÏ&’Ÿ_€*Ú/PeűËc֟CÁD銟]–X+˜17ïU> Nûćw©žś6Ÿ_Ž*Ú/Peűáqëû@°ęLÙöÜéą@0™W?> Nûćw©žśÆHß/ÈPŁíü<š€B–aI=íÀúP?Ł€`PÔ± > Nûćw©žśIß/šFûT ą`}<Ź›LH•RÁ\]€© @0„Śn> NûćwĄžśÖHß/šáFûš(đùiëiàÛŸĂ—łźÌ)&Œ±j> Nëć·UÏgHß/’ĄFûš€ž]XêX‘łäÈš‚±ČkY> NûćwM=ïÁHß/È ąęšČúők°țŠ”*™pòŒkȘ“>oÎ1> NëćwĄžÏ&’Ÿ_Pm?àd ô€’޶“Xÿő*ÀŒŒàô)> NûćwĄžśe }żH†>Ú/À€~ż>°~ "+JGÖéoÖł?ßoŸű5üMRóèűęŠôEùh€/U`TŸۗ–Ïâ§C™a‹›_s‚>œ ôź€Yyœł­ÿ^QÓXô] ă+śŚŚ„ƒíÖńș8îŰÍU>ÀgæìËÌQQkčŽÖ€rà·©VûÙĘnÓl¶SÀ5yÌ„ŐL„Ô(ÿ;ó’î_ÙöŚŐç&đÏf%[2ę}ZÆ$ͧ˜&òË <ÓŁLá|»‹|Ӕüą©őLț抖6‘_îÉț(Á‹yŠĆÜô•ąąišZ ”L‹©Û=™ș”)ùY›ìÏùÔîŠnś ?»'òs>ĆÏĆ?ùЉ Œ€ ßAw-œȚOÀ ˆS…ĆéÆPöÓŃé N·ÔmÍjęç–oQig„ŹWRŠ|rÛw#ŒX›P™mΠ{8ymæ3ˆ"f ÌÏÚŽÊUJæŃùy?éÁ~zîȚœ; Cêü ę~§OÁxŰO=żü?ÏíVž›ëő}żžžŰëû~Ś›2wwwwwww> NëćwĄžÏŠ‘Ÿ_€>Ú/À€~&x{JÀk­ Y‘‰Ù[t֓‚û> Nûćw©žśŠ‘Ÿ'Ż>Ú/ ô3`}x\X_Șv`BȘ” fŸ3­j`œ_ç> Nûćw©žśŠ‘ŸŸQm?à§ú ”P†ehí­ëKuÓsꯏ> Nûćw©žś6’Ÿ_Ž*Ú/@ęŸő8€őïÄAŹ(]±ąęSĐłÂùË> Nûćw©žś6’Ÿ_Ž*Ú/Peűëc%֟Ă@0ćLÙڇWi3æĂKŁ > Nûćw©žśHßÏÈPEÛx;šČ„–aIűR,`ę9ä:À4ÿČĐ > Nûćw©žśÖHß/’ĄFûT ’űó.°>Ô&˜R:˜onÓ @0(ŠZ> Nëć·UÏgc€ïÉPŁęTűütëfđmßöíMóïVLéÛ:> NëćwĄžÏIß/šFÛű§ˆ^ ]éß5°n‚)œuÔOggST]ÎFoVnÒ1KŽ/&H:<<<<:==9Cź(,&P<:>;><<<=NgȚűuü]Š{ôùÛ á-·őđ}$û]gè§èŸżùFïÓx7ÙCzâdĆÙÿ.(ÓăXR‹Zk ŽSŚ`Zk|Țö€żö(k{œÆźÎ-|́aHè/î'v^ŽÖrÔZȘŻ—sútq6€[Wí§nÄĄ\…ȘF T6êĘÖZkàq:Ûß-ìŸQÊŽèŠűćÆ@ńăÙ=LÉŻgššZÏÚ0…—VüxŠa ÏîaJ~=ÓÔÔęŹMük•˜ŠČV ûw çuN-Ÿó)ù~Ɣüz.SțŚ3MM­gś0ĆÏî2…gm˜âgw™’_Ï4ćÇ3$Ź=_ÛYR~/ìï À%|ƞ6Ș­=ŸœN‹'ÍÄ?ęÿ9ă;τôęÏD/„bÍżőźŐïtŻ ƒ2eFł¶æÎ`áș·Żż[«.Šöü úŽÖMŠŃèÏç†đW”˘ç7ÓyÆüő•Ò°ŐńGòk^żűùÛ]$wƒęęYZç\mÊ8Œž»ûrș3> Nûćw„žśĐHß/ÈòŃöȚp?”° ËĐÌßN@Ô4€„” …j> Nëćw„žÏ`€ïd^Ž_@îg@ÿz0— SJ•LxßÖ@Šê]…> NëćwĄžÏ&’Ÿ_$يö 0p?||ö€óš`û¶o‹E瀘ÿo> NëćwĄžÏ&’Ÿ_$ƒŠö 0p?Œ=%à~M ­ÈŠDïțó$ˆłÿ/ > Nûćw©žśŠ‘Ÿ'WíPú™°žž€ŚZ0!UJ•Ìȕș °žL}> Nëć·UÏg3Ÿ_Pm?àèg€6Đ OŚ|©Ú`ęt{> NëćwĄžÏŠ‘Ÿ_€>Ú/@ęŸőX€ő„:°"+Čąę;UsęâW> NóćżUÏgŒ€ïéWíĐęț} XÿNÁ”m߄·WìÒ fEûç> NÇćw„žś&’ŸŸ‘ĄŠ¶PÈ"ʰ KXƒÀúwHM­`Vš]UȚvÊMrxvÿ¶!œćQ? źă?ż€àï }Xï“ûËÓ­-žțæ-Ÿ e2b&#Âzÿ9Ă& >Uąíïț„čŸ]šuüö•ăTâ;„9†^ȚÌÆ(ŽŽZ+@őőòÏŹ_Ì<ȘÀ‰ő›Dđ;S#iźN„êę­WÁ|:sŸțț‰æÏ^çxqqq‘->»©=»LŠp»nvűmÜÆ!š‹Ș ęëIŸ<űőĂÖÌłK›ö_ńÏ‹Š»ŐߟÍfłș„́sÎ9眫\ ö—àač\ê[»AúŐÀŸ€țÇZàű ¶ÛïÓ­¶ÓÇï+"[0 ö—àač]ő=Šb €_ ì èK——Àòyÿśęčűž ƒ: ö—àač\ő-Še~5°/€țšæé€ÒÏŸ>őšôY‡]YD?­“ öŻ—a9m‘úĆô û5l폆ižàæ ĐwU•R„Kf ö—àač\ő-Še~Հ}ÖßÔ °> Őûob­îÿvEr Pđg ö—àačlő-ŠÓ ęȘÀŸ€ő0——@.Ÿ?żŻçú¶țZ] €ó—; ö—`ač\ő->Ź ÒŻ ì XßVŽïV^Ț~]ziN~2!ù] öϗa9]ŒúĆHż öepƒăőYaTąŒƒŸżŠU@ČőH öϗa9]ő-Š! „e°/ ÿžf€‡€ŚòńJìőè°5Ę €Ù§Vțűò)i㖇]Qßb Ą_°=èŸÉ đCƒ—‡ïm„œÿûÿUß§&ű2êź±NXï!Éêß?ȚcAuUȘȘą(ȘD_ŚÄêżßMڶ_WÎÙFÏÜÁnaVk€ 4áš{ÿgNŠûŸÿ_ËÂÙ?©ońP_ÍϷ֚T[Íg?ț3[đò1”˜ hȏ#qŽM§*ț„a=Ï^îÉrOd SÀmediascanner2-0.115/test/media/testvideo_1080p.ogv000066400000000000000000000315361436755250000216630ustar00rootroot00000000000000OggSà)^KiXĐ*€theoraxD€8 (ÀOggSà)^xî :ÿÿÿÿÿÿÿÿÿÿ?theora+Xiph.Org libtheora 1.1 20090822 (Thusnelda)‚theoraŸÍ(śčÍk”©IJsœæ1ŒR”€!1Œb„!@ôád.UIŽvőpޘk•ą©@–HĄĐGәžÖd0K%R‘,”F"ƒ‘ÀĐd1‹b0”F!ƒĄÀÈ`, „‚ đh0û™֕UTÒёPO Í̌‹KKJ ÈÈLJ‡‡†FFFEĂĂĂĂĂĂ‚‚‚‚‚‚‚AAAAAA@!°Ą‚ƒ3ĐÀÀá1ŁŁĂpàŃ‚ƒ”S€áaÓ5uá!bS€FÖtт3t‡Ăćvw—†T…Ćö'Fv1!‚ö6661!Q€&6661Ł†66662ô&66666666666666666666666666666666666661AÂAÂƒAÂƒAÂƒÂƒÂƒƒƒâü·Đ 3ŸBàꉜčf"ĘGà?r‰"^àŰÉ}j©fWbìBä`öŽŠKç0ÚՆÆU)^}»‡äÄ4ÌźĆۂÈŁĂUš62^?9w1+ŁÚ<“„ûȘ€ ĆsTûȘ€WcAÊiAíuܐàek1·ÎĆx^ yÛțŽÓhó©"cuk3)š5ß –Łû@œ xŰæ]‘ w(=ŁÎčM¶»Ű+‚•F_ĆąÖ9Ü"ÍŻśŽ†bÊЧ%r({·IzȘJ„yœ'JÖlĆźÀÒ@äńCĘșïÿČcylƒÙàZuÇ+1‡ÀeG#FąidÚÿêRÜìŽ,>“‚idÚ<đőt +-1äÊÿêR܄֏^vj"ÿì|Ÿ„ÜÂv}-°VžK&șb€ó'wȘ€@kőî^§K”çY°‰j…•†Ÿâ#ż8UIJèÎČbQŻÂÊĆääÒí#ÎÜ9ˆCžś6ô¶ą”W8bőŹąÊǒ/í»›j>ÌFaПę2[ E(Â,ŹĘyö€ó?mB"\Űt-2{§Ì|HçșŠ©V/”>śÀmDÿëÓ<#H2áŠnméŒ0őŹÔ@u Ÿ]P§­Ï9;DzJąi#üÓ# ó çW`CùÓž¶Ćsè54Ž8rŒ&•íyźFæ \Ă{IÊìcpíûƒž3Ú%“ÎȘS™“è°Ü›{,‘5†öó•âł0rê=2\ÇĆT­a}{ òn5íNaVč€đ•±DȚÉ…ŹßđȘ‘Kâ7țNă4 înÀ5&6ŁÙd‰§EʜÂc}BȘF\g ț7Âb†Úû.ŰźÒ Ëç <‹„)#w…ĄûžV±Äă—Q!čžf (mŻłK•ÿ \N1fŃ2BivÖă›Îęđ}jÊÁD8žËš*RŽÒí&ŽspÎàZ~zŽ.Ź+<ÂvæXđ)(ÓKʚBżŁƒÜŒZaß#ë˜łòś7š,ź„;„ś‚fÚË€™BàżßBŒđíoÇ&1Ș“‘&L1òw±Z:ù čÄeÛYŽ—;Ś~ȱU$+^Ïč‡Ń~Čłąù;r0Ä6ÖlćQ”BȘ[€ÍŁmrÆ[Ÿ„ĂÂö &üEbŽ-{:>D)p=”dÓ:0ż» Đ«Zžâ4]%Mÿ܃‡šîžó’&ÔC:ƔȘÈčĄÓ°šY=óì-ˆ|áæ[ŸŁŚ™Ć•ź4tˆKŽžŃMFpZ-_óÛΊ [MŻŸap#‹ŸrŹČ„,Yș„>e…*ĘCč ë,ŽŒ S6ÖM#ϓÇhžŃ8}u*ŽY‰S6ÖOcx;DŁ?°ám…p71†qçü©šZ†qÙí1€Í”–L_Ybï pEărŚr‚ą2j™ŠrĐÇĘEYbžÉ{‹FèV‹Ô šfmź,&ŸŽ ì)šEńԜÉ}ș7œ ©_Âs yZŃßèhƒmR&—HòôČČï7îyÂ߀‹(n€ćxH&•¶șgʱ ČČwœT)|1$j•€ÙćD-Ü:ô?0ț+°OÊŐĘ šoR˜”Ńp[ÓïD%ä†fÚ„i6LpÿÔê#YXȚ„(xú/`ƐÛTÍ3—ÿ`§]AoŐŒX‹±ć*œ„`6ÔÊf™ăr{D(č€zùGë+#wÖăʔ‹~‘Ï€VVF ə4́¶žĘÿQÓ„ŁÄg!tĆæ ±4ͶŽ[슠)wbŽ€ÿüBâ&ț G°šQÔì8?ËáœÆ,ùʰ"$%m6±śćëjR\e߁&•¶șIš*/ű?u놠ܶĆd<Î*€#$%4™¶čŃ{–8ùđ;eözăq=Ö+€ßú~`SP”w€fi3meË‘śaZœ}žÜèbd˜Ó6ÚÊŁ—ŁćMDŸVXOÛۋx?ŠžžOœÄȘ’'żÚYXïšÜ¶Źšg‹ —ËwƒqúuÀÓ6ژPËæ’4'Čę U"'–Ö°ïź JBäëțś*€ČČ!9đÛU /\e‹Lę™ć±ƒŻx„SëA@j•€Òg<ßrîŰ2ˆO抶ԁÚG‰ùW`/Žï–š0$ޘÛ\òÆ&ï]ț)šwó«`‘ĆÊÊÏ7!~^‘ƒ¶Č“4Ï+‰èà,Ź‹šTÔŸ\^LÓ9FÚㄎVŃæïGóśÀFć#éęUHßÈJ;ˆâĆarUăLÛja3,· Ț/Akÿ»ăÂÊÁ*á–-36Ő3 磔#…U%(ÍĘő#‹ ܒ…Ó0żń¶ŠXŽÏć gđöŠ©@F,ŹqżdźDÊ'6ŐÿĂ,Zf]k úô‰äˆZŠ †ç?“˜ šfmŹčcʅ{…ążçœ_§$ڙLÓ9Xá}Ő)€xçáDVîXăŐgqÂ)š‡ÒȚ| #P”4ͶŠ2Æû·ûâĆdÿ:ˆ#Ë™›j™…śKúčYZš„>@n‰"‹^žuŰôäĄiüÁ–-36Ős çïtŒqa·I~BŠAŒdÄ@883ȶuś&|kŁęčQß„•Œ•U#xÉ!ˆ€p: pg‘lëîLűŚGûrŁżK+y*ȘFń’CàtàÏ"ÙŚÜ™ńźöćG~–VòUTŠ©8Oԉx&i™¶Čć#!ńebȚ_z.쎏ś±ĂR—ź_Đ,źÍÂä† FS4ÌÛYoŸƒż‘tóóć*–Vš‹Öđ 8$/J4Ͷ€Ì±ÂîŃ­è,Źćkéü#p“¶ț!:?JLË晶Öê"SQIä@úz;qÏĄ]‘À7ŸŒ±i™3 Í”ș^€†Ą '«+„RŒù2û§Z5ŃțÆ83Ê^]žQÔdQ’ąëX^LąsŸŸęb°m«ž ±ižțùȘJ©@F,ŹqżdźDÊ'6ŐÿĂ,Zf]k úô‰ä€OggSCà)^Êk’Ž,ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ% ƒLœśŸśȚûß{ï}ïœśŸśȚûß{ï}öŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}vŸśȚûßxœśŸśȚûß{ï}oŒ}5Cï5PûÇĂWïIżxù*‡Ț>jĄśŒ}ăàȘxùȘ‡Ț>żxúMûÇÉT>ńóUŒ}ăïPûÇÍT>ńđŐûÇÒoȚ>JĄśšš}ăïxű*‡Ț>jĄś†ŻȚ>“~ńòUŒ|ŐCïxûÇÁT>ńóUŒ|5~ńô›ś’š}ăæȘxûÇȚ> Ąśšš}ă᫜€ßŒ|•Cï5PûÇȚ>ńđUŒ|ŐCï _Œ}&ęăäȘxùȘ‡Ț>ńś‚š}ăæȘxűjęăé7ï%PûÇÍT>ńśŒ|Cï5PûÇĂWïIżxù*‡Ț>jĄśŒ}ăàȘxùȘ‡Ț>żxúMûÇÉT>ńóUŒ}ăïPûÇÍT>ńđŐûÇÒoȚ>JĄśšš}ăïxű*‡Ț>jĄś†ŻȚ>“~ńòUŒ|ŐCïxûÇÁT>ńóUŒ|5~ńô›ś’š}ăæȘxûÇȚ> Ąśšš}ă᫜€ßŒ|•Cï5PûÇȚ>ńđUŒ|ŐCï _Œ}&ęăäȘxùȘ‡Ț>ńś‚š}ăæȘxűjęăé7ï%PûÇÍT>ńśŒ|Cï5PûÇĂWïIżxù*‡Ț>jĄśĄśŒȚ>JĄśŒ}ăÜߌ}ăïPûÇȚ>ńśŒ{ŐŒ}ăïæęăïxűê‡Ț>ńśŒ}ăȚš}ăïxś7ïxûÇÇT>ńśŒ}ăïőCïxûÇčżxûÇȚ>:ĄśŒ}ăïxśȘxûÇȚ=ÍûÇȚ>ńńŐŒ}ăïxûÇœPûÇȚ>ńîoȚ>ńśŽš}ăïxûÇȚ=ê‡Ț>ńśs~ńśŒ|uCïxûÇȚ>ńïT>ńśŒ{›śŒ}ăăȘxûÇȚ>ńśzĄśŒ}ăÜߌ}ăïPûÇȚ>ńśŒ{ŐŒ}ăïæęăïxűê‡Ț>ńśŒ}ăȚš}ăïxś7ïxûÇÇT>ńśŒ}ăïőCïxûÇčżxûÇȚ>:ĄśŒ}ăïxśȘxûÇȚ=ÍûÇȚ>ńńŐŒ}ăïxûÇœPûÇȚ>ńîoȚ>ńśŽš}ăïxûÇȚ=ê‡Ț>ńśs~ńśŒ|uCïxûÇȚ>ńïT>ńśŒ{›śŒ}ăăȘxûÇȚ>ńśzĄśŒ}ăÜߌ}ăïPûÇÎC@Ą PĐ(h4  †€C@Ą PĐ(h4  †€C@Ą PĐ(h4  †€C@Ą PĐ(h4  †€C@Ą QTPU@UEEQTQTPU@UEEQTQTPU@UEEQTQTPU@UEEQTQTPU@UEEQTQTPU@UEEQTQTPU@UEEQTQTPU@UEEQTQTPUHÒš€Ș*€(  š€Ș*€(  š€Ș*€(  š€Ș*€(  š€Ș*€(  š€Ș*€(  š€Ș*€(  š€Ș*€(  š€Ș•Á+˜áÚce^ŻĆAPW„1Ăű^Ż0ŚsAzŒÀ{eà+ÁíŁ –•âLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtâLŠ:☯Aê‚ĄLW„2˜ëŠbŒ ł—stàöÁà+ÁìtᄦV’ŚČZôÈ€LÈ4ói:57Ó"!2MŠȘfT€ÈŸÉl"”dÈÚ‚”«@ÂhIZÒZöKdÈ€LÈŠ@èDX0ȈLÔ©S2„&@çd¶HE26… „*ĐŽ™­%Żd¶LÈ̀Jd„Eƒ ˆ„ÈJ•3*RdvKa„S shZ R­ AIZÒZöKdÈ€LÈŠ@èDX0ȈLÔ©S2„&@çd¶HE26… „*ĐŽ™­%Żd¶LÈ̀Jd„Eƒ ˆ„ÈJ•3*RdvKa„S shZ R­ AIZÒZöKdÈ€LÈŠ@èDX0ȈLÔ©S2„&@çd¶HE26… „*ĐŽ™­%Żd¶LÈ̀Jd„Eƒ ˆ„ÈJ•3*RdvKa„S shZ R­ AIZÒZöKdÈ€LÈŠ@èDX0ȈLÔ©S2„&@çd¶HE26… „*ĐŽ™­%Żd¶LÈ̀Jd„Eƒ ˆ„ÈJ•3*RdvKa„S shZ R­ AIZÒZöKdÈ€LÈŠ@èDX0ȈLÔ©S2„&@çd¶HE26… „*ĐŽ™­%Żd¶LÈ̀Jd„Eƒ ˆ„ÈJ•3*RdvKa„S shZ R­ AIZÒZöKdÈ€LÈŠ@èDX0ȈLÔ©S2„&@çd¶HE26… „*ĐŽ™­%Żd¶LÈ̀Jd„Eƒ ˆ„ÈJ•3*RdvKa„S shZ R­ AIZÒZöKdÈ€LÈŠ@èDX0ȈLÔ©S2„&@çd¶HE26… „*ĐŽ™­%Żd¶LÈ̀Jd„Eƒ ˆ„ÈJ•3*RdvKa„S shZ R­ AIZÒZöKdÈ€LÈŠ@èDX0ȈLÔ©S2„&@çd¶HE26… „*ĐŽ™­%Żd¶LÈ̀Jd„Eƒ ˆ„ÈJ•3*RdvKa„S shZ R­ AI—é@ˆkG“ uf͖LŠ@ę„&†©™R“ sú_ț—”ÈÚ€ˆÚ”ÔÈ@ț—é@ˆLŐ›6Ye2„Lʔ™ŸÒÿôżLÍĄhĄh d KôÈ Ä@Œ&@ê͛,ș RŠeJLÏéú_Š@æĐŽFĐŽ2?„údb F uf͖YL‚©S2„&@çôżę/Ó shZ#hZ™Òę21# :łfË,ŠAT©™R“ sú_ț—é9Ž-Ž-L„é~™ˆ„ÈYłe–S *TÌ©I9ę/ÿKôÈÚ€ˆÚ€ŠBôżL‚ŒDÂdŹÙČË)@•*fT€Èț—ÿ„údm @Dm @S!ú_ŠAF"a2VlÙe”È J•3*RdKÿÒę26… "6… )ę/Ó €#0™«6lČÊd%J™•)2?„ÿé~™›BĐBĐÈ@ț—é@ˆLŐ›6Ye2„Lʔ™ŸÒÿôżLÍĄhĄh d KôÈ Ä@Œ&@ê͛,ș RŠeJLÏéú_Š@æĐŽFĐŽ2?„údb F uf͖YL‚©S2„&@çôżę/Ó shZ#hZ™Òę21# :łfË,ŠAT©™R“ sú_ț—é9Ž-Ž-L„é~™ˆ„ÈYłe–S *TÌ©I9ę/ÿKôÈÚ€ˆÚ€ŠBôżL‚ŒDÂdŹÙČË)@•*fT€Èț—ÿ„údm @Dm @S!ú_ŠAF"a2VlÙe”È J•3*RdKÿÒę26… "6… )ę/Ó €#0™«6lČÊd%J™•)2?„ÿé~™›BĐBĐÈ ’&Ü„-„o0Fą%@! ! 0@ ……ƒ˜ ‰PB€BÁƒÌDš„!@!`Áæ"TB °`ó*!PX0y‚ˆ•„(„,<ÁDJ€BB `€"%@! ! 0@ ……ƒ˜ ‰PB€BÁƒÌDš„!@!`Áæ"TB °`ó*!PX0y‚ˆ•„(„,<ÁDJ€BB `€"%@! ! 0@ ……ƒ˜ ‰PB€BÁƒÌDš„!@!`Áæ"TB °`ó*!PX0y‚ˆ•„(„,<ÁDJ€BB `€"%@! ! 0@ ……ƒ˜ ‰PB€BÁƒÌDš„!@!`Áæ"TB °`ó*!PX0y‚ˆ•„(„,<ÁDJ€BB `€"%@! ! 0@ … ‚șj”N”ZŁŃèôz;„ô~úß[êț‡CĄĐĘŻtœyA]5Z§Z­Qèôz=Òúż}o­őCĄĐèn‹Śș^ŒÈ ‚.š­S­Všôz=Žé}_Ÿ·ÖúżĄĐèt7EëĘ/^dAMV©Ö«Tz=GtŸŻß[ë}_Đèt:ąő2 ‹Š«TëUȘ=GŁș_Wï­őŸŻèt: ŃzśKŚ™EÓUȘuȘՏGŁŃĘ/«śÖúßWô:†èœ{„ë̂"éȘŐ:ŐjGŁŃèî—Őûë}o«ú‡Ct^œÒőæAtŐjj”GŁŃèôwKêęőŸ·Őę‡CĄș/^ézó ‚șj”N”ZŁŃèôz;„ő~úß[êț‡CĄĐĘŻtœyA]5Z§Z­Qèôz=Òúż}o­őCĄĐèn‹Śș^ŒÈ ‚.š­S­Všôz=Žé}_Ÿ·ÖúżĄĐèt7EëĘ/^dAMV©Ö«Tz=GtŸŻß[ë}_Đèt:ąő2 ‹Š«TëUȘ=GŁș_Wï­őŸŻèt: ŃzśKŚ™EÓUȘuȘՏGŁŃĘ/«śÖúßWô:†èœ{„ë̂"éȘŐ:ŐjGŁŃèî—Őûë}o«ú‡Ct^œÒőæAtŐjj”GŁŃèôwKêęőŸ·Őę‡CĄș/^ézó ‚șj”N”ZŁŃèôz;„ő~úß[êț‡CĄĐĘŻtœ}ŚCĄuĄĐù~ę_«ôÿwuŃJ—JTĘt:Z ß§ïŐúżOśw]©t„MŚCĄuĄĐú~ę_«ôÿwuŃJ—JTĘt:Z ß§ïŐúżOśw]©t„MŚCĄuĄĐú~ę_«ôÿwuŃJ—JTĘt:Z ß§ïŐúżOśw]©t„MŚCĄuĄĐú~ę_«ôÿwuŃJ—JTĘt:Z ß§ïŐúżOśw]©t„MŚCĄuĄĐú~ę_«ôÿwuŃJ—JTĘt:Z ß§ïŐúżOśw]©t„MŚCĄuĄĐú~ę_«ôÿwuŃJ—JTĘt:Z ß§ïŐúżOśw]©t„MŚCĄuĄĐú~ę_«ôÿwuŃJ—JTĘt:Z ß§ïŐúżOśw]©t„MŚCĄuĄĐú~ę_«ôÿwuŃJ—JTĘt:Z ß§ïŐúżOśw]©t„MŚCĄuĄĐú~ę_«ôÿwuŃJ—JT˜@#•F€…  @#ńűüŃŁF B…  @‘űü~hŃŁPĄB… Hü~?4hŃšPĄB„$~?€š4hÔ(PĄB?ÀM4j(PĄ Çà&5 (PÇăđF€ (@Găńű ŁF@…  @#ńűüŃŁF B…  @‘űü~hŃŁPĄB… Hü~?4hŃšPĄB„$~?€š4hÔ(PĄB?ÀM4j(PĄ Çà&5 (PÇăđF€ (@Găńű ŁF@…  @#ńűüŃŁF B…  @‘űü~hŃŁPĄB… Hü~?4hŃšPĄB„$~?€š4hÔ(PĄB?ÀM4j(PĄ Çà&5 (PÇăđF€ (@Găńű ŁF@…  @#ńűüŃŁF B…  @‘űü~hŃŁPĄB… Hü~?4hŃšPĄB„$~?€š4hÔ(PĄB?ÀM4j(PĄ Çà&5 (PÇăđF€ (@Găńű ŁF@…  @#ńűüŃŁF B… ‰$H‘"܉-û‘"Eœ$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$WMȑ"ș"D‰$H‘]"DŠè‰$H‘"EtD‰+ą$H‘"D‰Ń$Hźˆ‘"D‰$WDH‘"ș"D‰$H‘]"DŠè‰$H‘"EtD‰+ą$H‘"D‰Ń$Hźˆ‘"D‰$WDH‘"ș"D‰$H‘]"DŠè‰$H‘"EtD‰+ą$H‘"D‰Ń$Hźˆ‘"D‰$WDH‘"ș"D‰$H‘]"DŠè‰$H‘"EtD‰+ą$H‘"D‰Ń$Hźˆ‘"D‰$WDH‘"ș"D‰Ÿ0ù‡0áĂæ8|ǘpáó>aÇÌ8pù‡0áĂæ8|ǘpáó>aÇÌ8pù‡0áĂæ8|ǘpáó>aÇÌ8pù‡0áĂæ8|ǘpáó>aÇÌ8pù‡0ĐBe–P„*b” í¶Ò"ÁB,ČË„*R”¶Ûm ‚„"Ë,±B„)Km¶Ò B,ČË„*R”¶Ûm ‚„"Ë,±B„)Km¶Ò B,ČË„*R”¶Ûm ‚„"Ë,±B„)Km¶Ò B,ČË„*R”¶Ûm ‚„"Ë,±B„)Km¶Ò B,ČË„*R”¶Ûm ‚„"Ë,±B„)Km¶Ò B,ČË„*R”¶Ûm ‚„"Ë,±B„)Km¶Ò B,ČË„*R”¶Ûm ‚„"Ë,±B„)Km¶Ò B,ČË„*R”¶Ûm ‚„"Ë,±B„)Km¶Ò ‚"ÊR”¶ÛFAJR–Ûm€A„)m¶ÚAJR–Ûm€A„)m¶ÚAJR–Ûm€A„)m¶ÚAJR–Ûm€A„)m¶ÚAJR–Ûm€A„)m¶ÚAJR–Ûm€A„)m¶ÚAJR–Ûm€A„)m¶ÚAJR–Ûm€A„)m¶ÛCdPd                                                                 €ÌÌ»Ž’Y™œ…ww™™wv’Y™Ë»ŒÌË»Ž’ÌÌn]Ęæf]Ę€–fcrîï32îí$ł3—wy™—wi%™˜Ü»»ÌÌ»»I,ÌÆćĘȚfeĘÚIff7.îó3.îÒK31čww™™wv’Y™Ë»ŒÌË»Ž’ÌÌn]Ęæf]Ę€–fcrîï32îí$ł3—wy™—wi%™˜Ü»»ÌÌ»»I,ÌÆćĘȚfeĘÚIff7.îó3.îÒK31čww™‡)JffffR”Šfffe)JffffR”Šfffe)JffffR”Šfffe)JffffR”Šfffe)JffffR”Šfffe)JffffR”Šfffe)JffffR”Šfffe)JffffR”Šfffe)JffçÏš_>|ú…óçÏš_>|ú…óçÏš_>|ú…óçÏš_>|ú…óçÏš_>|ú…óçÏš_>|ú…óçÏš_>|ú…óçÏš_>|ú…óçÏš_>|ú…óçÏš_>|ú…óçÏš_>|ú…óçÏš_>|ú…óçÏš_>|ú…óçÏš_>|ú…óçÏš_>|ú…óçÏš_>|ú…óçÏš_>|ú&&&&**(ÆÆÆ„ąbbbbąąąŁcccccccbbbbbbbbbąąąŁcccccccbbbbbbbbbąąąŁcccccccbbbbbbbbbąąąŁcccccccbbbbbbbbbąąąŁcccccccbbbbbbbbbąąąŁcccccccbbbbbbbbbąąąŁcccccccbbbbbbbbbąąąŁcccccccbbbbbbbbbąąąŁcccccccbbbbbbbbbąąąŁcccccccbbbbbbbbbąąąŁcccccccbbbbbbbbbąąąŁcccccccbbbbbbbbbąąąŁcccccccbbbbbbbbbąąąŁcccccccbbbbbbbbbąąąŁcccccccbbbbbbbbbąąąŁcccccccbbbbcŁŁ”dllkr::::6665č܎ŽŽŽnGGGGFÆÆÆ·#ŁŁŁŁccc[‘ŃŃŃѱ±±­ÈèèèèŰŰŰÖättttlllkr::::6665č܎ŽŽŽnGGGGFÆÆÆ·#ŁŁŁŁccc[‘ŃŃŃѱ±±­ÈèèèèŰŰŰÖättttlllke€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€†’I$’I$’I$’I$’I$’I$’I$’I$’I$’I$’I$’I$’I$’I$’I$’I$’I&$¶=c4’[DZìe‚Il{DZ– %±ì{ÆX$–DZì{`’[DZìe‚Il{DZ– %±ì{ÆX$–DZì{`’[DZìe‚Il{DZ– %±ì{ÆX$–DZì{`’[DZìe‚Il{DZ– %±ì{ÆX$–DZì{Jșèú ~ h hèú ~ h hèú QTv‡a1°ì;ÂcaŰv„ÆĂ°ì; ‡aŰvðì&6‡aŰLl;ð˜Űv‡a1°ì;ÂcaŰv„ÆĂ°ì; ‡aŰvðì&6‡aŰLl;ð˜Űv‡aÚv§iÚv§iÚv§iÚv§iÚv§iÚv§iÚv§iÚv§iÚv§iÚv§iÚv§iÚv§iÚv§iÚv§iÚv§iÚv§iÚvŠčòI$’I$’I$’I$’I$’I$’I$’I$’BŽ2@OggSJà)^wy*tOggSKà)^§v?Rmediascanner2-0.115/test/media/testvideo_480p.ogv000066400000000000000000000152451436755250000216050ustar00rootroot00000000000000OggS EćSV*€theora6Và (ÀOggS EČAà :ÿÿÿÿÿÿÿÿÿÿ?theora+Xiph.Org libtheora 1.1 20090822 (Thusnelda)‚theoraŸÍ(śčÍk”©IJsœæ1ŒR”€!1Œb„!@ôád.UIŽvőpޘk•ą©@–HĄĐGәžÖd0K%R‘,”F"ƒ‘ÀĐd1‹b0”F!ƒĄÀÈ`, „‚ đh0û™֕UTÒёPO Í̌‹KKJ ÈÈLJ‡‡†FFFEĂĂĂĂĂĂ‚‚‚‚‚‚‚AAAAAA@!°Ą‚ƒ3ĐÀÀá1ŁŁĂpàŃ‚ƒ”S€áaÓ5uá!bS€FÖtт3t‡Ăćvw—†T…Ćö'Fv1!‚ö6661!Q€&6661Ł†66662ô&66666666666666666666666666666666666661AÂAÂƒAÂƒAÂƒÂƒÂƒƒƒâü·Đ 3ŸBàꉜčf"ĘGà?r‰"^àŰÉ}j©fWbìBä`öŽŠKç0ÚՆÆU)^}»‡äÄ4ÌźĆۂÈŁĂUš62^?9w1+ŁÚ<“„ûȘ€ ĆsTûȘ€WcAÊiAíuܐàek1·ÎĆx^ yÛțŽÓhó©"cuk3)š5ß –Łû@œ xŰæ]‘ w(=ŁÎčM¶»Ű+‚•F_ĆąÖ9Ü"ÍŻśŽ†bÊЧ%r({·IzȘJ„yœ'JÖlĆźÀÒ@äńCĘșïÿČcylƒÙàZuÇ+1‡ÀeG#FąidÚÿêRÜìŽ,>“‚idÚ<đőt +-1äÊÿêR܄֏^vj"ÿì|Ÿ„ÜÂv}-°VžK&șb€ó'wȘ€@kőî^§K”çY°‰j…•†Ÿâ#ż8UIJèÎČbQŻÂÊĆääÒí#ÎÜ9ˆCžś6ô¶ą”W8bőŹąÊǒ/í»›j>ÌFaПę2[ E(Â,ŹĘyö€ó?mB"\Űt-2{§Ì|HçșŠ©V/”>śÀmDÿëÓ<#H2áŠnméŒ0őŹÔ@u Ÿ]P§­Ï9;DzJąi#üÓ# ó çW`CùÓž¶Ćsè54Ž8rŒ&•íyźFæ \Ă{IÊìcpíûƒž3Ú%“ÎȘS™“è°Ü›{,‘5†öó•âł0rê=2\ÇĆT­a}{ òn5íNaVč€đ•±DȚÉ…ŹßđȘ‘Kâ7țNă4 înÀ5&6ŁÙd‰§EʜÂc}BȘF\g ț7Âb†Úû.ŰźÒ Ëç <‹„)#w…ĄûžV±Äă—Q!čžf (mŻłK•ÿ \N1fŃ2BivÖă›Îęđ}jÊÁD8žËš*RŽÒí&ŽspÎàZ~zŽ.Ź+<ÂvæXđ)(ÓKʚBżŁƒÜŒZaß#ë˜łòś7š,ź„;„ś‚fÚË€™BàżßBŒđíoÇ&1Ș“‘&L1òw±Z:ù čÄeÛYŽ—;Ś~ȱU$+^Ïč‡Ń~Čłąù;r0Ä6ÖlćQ”BȘ[€ÍŁmrÆ[Ÿ„ĂÂö &üEbŽ-{:>D)p=”dÓ:0ż» Đ«Zžâ4]%Mÿ܃‡šîžó’&ÔC:ƔȘÈčĄÓ°šY=óì-ˆ|áæ[ŸŁŚ™Ć•ź4tˆKŽžŃMFpZ-_óÛΊ [MŻŸap#‹ŸrŹČ„,Yș„>e…*ĘCč ë,ŽŒ S6ÖM#ϓÇhžŃ8}u*ŽY‰S6ÖOcx;DŁ?°ám…p71†qçü©šZ†qÙí1€Í”–L_Ybï pEărŚr‚ą2j™ŠrĐÇĘEYbžÉ{‹FèV‹Ô šfmź,&ŸŽ ì)šEńԜÉ}ș7œ ©_Âs yZŃßèhƒmR&—HòôČČï7îyÂ߀‹(n€ćxH&•¶șgʱ ČČwœT)|1$j•€ÙćD-Ü:ô?0ț+°OÊŐĘ šoR˜”Ńp[ÓïD%ä†fÚ„i6LpÿÔê#YXȚ„(xú/`ƐÛTÍ3—ÿ`§]AoŐŒX‹±ć*œ„`6ÔÊf™ăr{D(č€zùGë+#wÖăʔ‹~‘Ï€VVF ə4́¶žĘÿQÓ„ŁÄg!tĆæ ±4ͶŽ[슠)wbŽ€ÿüBâ&ț G°šQÔì8?ËáœÆ,ùʰ"$%m6±śćëjR\e߁&•¶șIš*/ű?u놠ܶĆd<Î*€#$%4™¶čŃ{–8ùđ;eözăq=Ö+€ßú~`SP”w€fi3meË‘śaZœ}žÜèbd˜Ó6ÚÊŁ—ŁćMDŸVXOÛۋx?ŠžžOœÄȘ’'żÚYXïšÜ¶Źšg‹ —ËwƒqúuÀÓ6ژPËæ’4'Čę U"'–Ö°ïź JBäëțś*€ČČ!9đÛU /\e‹Lę™ć±ƒŻx„SëA@j•€Òg<ßrîŰ2ˆO抶ԁÚG‰ùW`/Žï–š0$ޘÛ\òÆ&ï]ț)šwó«`‘ĆÊÊÏ7!~^‘ƒ¶Č“4Ï+‰èà,Ź‹šTÔŸ\^LÓ9FÚㄎVŃæïGóśÀFć#éęUHßÈJ;ˆâĆarUăLÛja3,· Ț/Akÿ»ăÂÊÁ*á–-36Ő3 磔#…U%(ÍĘő#‹ ܒ…Ó0żń¶ŠXŽÏć gđöŠ©@F,ŹqżdźDÊ'6ŐÿĂ,Zf]k úô‰äˆZŠ †ç?“˜ šfmŹčcʅ{…ążçœ_§$ڙLÓ9Xá}Ő)€xçáDVîXăŐgqÂ)š‡ÒȚ| #P”4ͶŠ2Æû·ûâĆdÿ:ˆ#Ë™›j™…śKúčYZš„>@n‰"‹^žuŰôäĄiüÁ–-36Ős çïtŒqa·I~BŠAŒdÄ@883ȶuś&|kŁęčQß„•Œ•U#xÉ!ˆ€p: pg‘lëîLűŚGûrŁżK+y*ȘFń’CàtàÏ"ÙŚÜ™ńźöćG~–VòUTŠ©8Oԉx&i™¶Čć#!ńebȚ_z.쎏ś±ĂR—ź_Đ,źÍÂä† FS4ÌÛYoŸƒż‘tóóć*–Vš‹Öđ 8$/J4Ͷ€Ì±ÂîŃ­è,Źćkéü#p“¶ț!:?JLË晶Öê"SQIä@úz;qÏĄ]‘À7ŸŒ±i™3 Í”ș^€†Ą '«+„RŒù2û§Z5ŃțÆ83Ê^]žQÔdQ’ąëX^LąsŸŸęb°m«ž ±ižțùȘJ©@F,ŹqżdźDÊ'6ŐÿĂ,Zf]k úô‰ä€OggSE EÎÚK’ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿH? ƒLœśŸśȚûß{ï}ïŒȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûÀ}ïœśŸśȚûßxœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸđ{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœś€ûß{ï}ïœśŸđ{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}à>śȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûÚùȘüő~ZĄńˆ>jĄéT>ńđUùêü”Că|ŐCÒš}ăà«óŐùj‡Æ ùȘ‡„PûÇÁWç«òŐŒAóUJĄś‚ŻÏWćȘƒæȘ•Cï_žŻËT>1ÍT=*‡Ț> ż=_–š|bššzUŒa|KćOçNiêûÇĆWï PûDžPûÇȚ=*ęăáȘxđȘxûÇ„_Œ|5CïCïxô«ś†š}ăš}ăï•~ńđŐŒxUŒ}ăÒŻȚ>Ąś ĄśŒzUûÇĂT>ńáT>ńńê§ËO@€4h4 A  (Ș ȘąŠ*‚ȘšąŠ ȘȘ(ąš*ȘŠ(Ș ȘąŠ*‚ȘšąŠ ȘȘ(« ąš(*Š  Ș*‚‚šȘ ąš(*Š  Ș*€Ș–ô`AÈfÜ@B„48À, AC d„Ža€!šÛmÊÛä4?–Ń€à@B€€À@ƒF9f€Ì0ƶÛhÀVß-òÚ0  ƒà#@È`À,І8¶ÛmÊÛćŸ[Ft|` Aćš0ÀÛmŁ[|·ËhÀ€‚€Œp €  Œr͘`‹m¶Ń€ ­Ÿ[ćŽ`@AÎ p !@@` A€#ł@fâÛmŽ`+o–ùmĐAđ€  d0`–h Ă[m¶Œemòß-Ł:>0À€„€  Œr͘`‹m¶Ń€ ­Ÿ[ćŽ`@AÀF8€ 0 À€Y 3 qm¶Ú0•·Ë|¶Œè űÀÀÈ`À,І8¶ÛmÊÛćŸ[Ft|àÀ2‚0Ë4a€-¶ÛF2¶ùo–Ń€à@B€€À@ƒF9f€Ì0ƶÛhÀVß-òÚ0  ƒà#@È`À,І8¶ÛmÊÛćŸ[Ft|` Aćš0ÀÛmŁ[|·ËhÀ€‚€Œp €  Œr͘`‹m¶Ń€ ­Ÿ[ćŽ`Hˆcé©°Dmˆ M àə¶Ąh-kŽ/k¶È™ h­Ù ^Ùmvû`Œ:_©I)4$ÈÄC X"6ÄČfm…hZ Zí Avș ¶BڶZő°Kő)%)2qȍ±,™›aZ‚Ö»BĐ]Č&@d-”í–œlÒęJIJLœD2‚#l@`K&fŰV… ”źĐŽl‰™ d-{eŻ[ôżR’R“ g `ˆÛə¶Ąh-kŽ-Û"d&BÙ ^ÙkÖÁę/Ô€”€ÈÄC X"6ÄČfm…hZ Zí Avș ¶BڶZő°Kő)%)2qȍ±,™›aZ‚Ö»BĐ]Č&@d-”í–œlÒęJIJL,2ƒn,›p»BĐ]Œ ·™kÖçö)I'ÿČTÏę±”±źždD[#Ęl€)@ÓeČ?ÿûœ°Od¶ZÒ[]LŸęŸłÿlE­l\2" d„(VÈÿÿÿ¶Él”€”Š@ÏțÏÙÿ¶"Ö¶. 2R”«dÿÿÛd¶ZÒZÓ gÿgìÿÛk[ ˆ„È)JUČ?ÿÿíƒČ[-i-i3ÿłö툔­‹†DBd €„*ÙÿÿöÁÙ-–Ž–ŽÈÿÙû?öÄZÖĆĂ"!2@ R€lÿÿû`ì–ËZKZd ÿìęŸûb-kbá‘™ )@ ¶Gÿÿę°vKe­%­2œŸłÛ…±pȶB€o?țÜìĆźß*aŹČË8Ü`oe–Xae–XańŒoe–Xae–XańŒoe–`ÂË,°ĂăȚË,°ÂË,0ÆűÆ7ČË,0ČË,0Æă{,ČĂ ,ČĂ oŒc{,ČĂ ,ČĂ oŒc{,łYe†ßÆöYe†Ya†7Æ1œ–Ya…–Ya†7Ùe–Ye–c|cÙe–Ye–c|cÙe˜0ČË,0ÆűÆ7ČË,0ČË 1Ÿ1ìČË 4z=ŽéŹÖk5—BtA|”.Ż–€È!ŃłYŹÖ]4z=ŽéŹÖk5—BtA|”.Ż–€È!ŃłYŹÖ]4z=ŽéŹÖk5—BtA|”.Ż–€È!ŃłYŹÖ]4z=ŽéŹÖk5—BtA|”.Ż–€È!ŃłYŹÖ]4z=ŽéŹÖk5—BtA|”.Ż–€È!ŃłYŹÖ]4z=ŽéŹÖk5—BtA|”.Ż–€È!ŃłYŹÖ]4z=ŽéŹÖk5—BtA|”.Ż–€È!ŃłYŹÖ]4z7őšÇDęòÔŸAŹÖ?tŃhŽZ+”ùż.ŻÍù»ńèŽZ-Ò""éąŃhŽWKó~]_›ówăŃhŽZ+€DEÓEąŃhź—æüșż7æïÇąŃhŽWHˆ‹Š‹EąŃ]/Íùu~oÍߏEąŃhź‘M‹Eąș_›òêüߛż‹EąŃ]"".š-‹Etż7ćŐùż7~=‹EąșDD]4Z-Šé~oË«ó~nüz-‹EtˆEąu~oËź=‰ôH‘"żżżżD‰$[lżżżżD‰+ûûûôH‘"D‰+ûûûôH‘"żżżżD‰$H‘"żżżżD‰+ûûûôH‘"D‰+ûûûôH‘"żżżżD‰$H‘"żżżżD‰ËûûûôH‘"E±ËûûûôH‘"żżżżD‰$H‘"żżżżD‰+ûûûôH‘"D‰+ûûûôH‘"żżżżD‰$H‘"żżżżD‰+ûûûôH‘"D‰+ûûûôH‘lżżżżD‰$[lżżżżD‰+ûûûôH‘"D‰+ûûûôH‘"żżżżD‰$H‘"żżżżD‰+ûûûôH‘"D‰+ûûûôH‘"żżżżD‰$H‘"żżżżD‰$H‘"D‰$hŃŁD‰$H‘"D‰$H‘"D‰$hŃŁD‰$H‘"D‰$H‘"D‰$hŃŁD‰$H‘"D‰$H‘"D‰$hŃŁD‰$H‘"D‰$H‘"D‰$hŃŁD‰$H‘"D‰$H‘"D‰$hŃŁD‰$H‘"D‰$H‘"D‰$hŃŁD‰$H‘"D‰$H‘ŁD‰+ą$H‘#FąD‰Ń$H‘ŁFŒŃ"DŠè‰$HŃŁFh‘"EtD‰$hŃŁ4H‘"ș"D‰4hњ$H‘]"D‰4hÍ$Hźˆ‘"D4f‰$OąDąEîïîïîïîïîïîńîńîńîńîńîïîïîïîïîï1Œk,X1Œb„ĄB…ƱbƃÆ!J(P1Œk,X1Œb„ĄB…ƱbƃÆ!J(P1Œk,X1Œb„ĄB…ƱbƃÆ!J(P1Œk,X1Œb„ĄB…ƃ„ĄA$’Ie–Y$’Ie–Y$’Ie–Y$’Ie–Y$’Ie–Y$’Ie–Y$’Ie–Y$–ZI$’ŠHU€•ĘȚfe2™™™wv’Wwy™”ÊffeĘÚI]ĘæfS)™™—wi%ww™™LŠff]Ę€•ĘȚfe2™™™wv’Wwy™”ÊffeĘÚI]ĘæfS)™™—wi]æS3.ó3! 3330Bł333! 3330Bł333! 3330Bł333! 330BÌŠH@ÄÄÄÄĆEEEFÆÆÆÇGGGDÄÄÄĆEEEDÄÄÄĆEEEFÆÆÆÇGGGDÄÄÄĆEEEDÄÄÄĆEEEFÆÆÆÇGGGDÄÄÄĆEEEDÄÄÄĆEEEFÆÆÆÇGGGDÄÄÄĆEEEDÄÄÄĆEEEFÆÆÆÇGGGDÄÄÄĆEEEDÄÄÄĆEEEFÆÆÆÇGGGDÄÄÄĆEEEDÄÄÄĆEEEFÆÆÆÇGGGDÄÄÄĆEEEDÄĆEFÆÇGDÄĆEGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGl{DZ€’Il{DZ€’Il{DZ€’Il{DZ€’Il{DZ€’Il{DZ€’Il{DZ€’Il{Il;ð˜Ű¶-‹b1Ží;NÓ°ì;Âcb۶-ˆÆÓŽí;Nðì; ‹b۶#NÓŽí;ðì&6-‹bیm;NÓŽì;ð˜Ű¶-‹b1Ží;NÓ°ì;Âcb۶-ˆÆÓŽí;Nðì; ‹b۶#NÓŽí;Ăę‹bțÓŽí;NÓŽì[Ʊl;ðí;NÓŽì[Ʊl;ðí;NÓŽì[Ʊl;ðí;NÓŽì[Ʊl;ðí;NÓŽì[Ʊl;ðí;NÓŽì[Ʊl;ðí;NÓŽì[Ʊl;ðí;NƱl; ’I$’I$’I$’I$’I$’I$’I$’6Ûm¶Ûm¶Ûm¶Ûm¶Ûm¶Ûm¶Ûm¶ÙZ@J?§›ÍæóyŒȚoÁQù%ș)ÙNÊvSȝ”ì§ÁP( @ P( C 4hŃŁF4hŃŁYđOggSK EŰü<Œmediascanner2-0.115/test/media/testvideo_720p.ogv000066400000000000000000000222771436755250000216050ustar00rootroot00000000000000OggS 1DHĆ«*€theoraP-Đ (ÀOggS 1áÛâ :ÿÿÿÿÿÿÿÿÿÿ?theora+Xiph.Org libtheora 1.1 20090822 (Thusnelda)‚theoraŸÍ(śčÍk”©IJsœæ1ŒR”€!1Œb„!@ôád.UIŽvőpޘk•ą©@–HĄĐGәžÖd0K%R‘,”F"ƒ‘ÀĐd1‹b0”F!ƒĄÀÈ`, „‚ đh0û™֕UTÒёPO Í̌‹KKJ ÈÈLJ‡‡†FFFEĂĂĂĂĂĂ‚‚‚‚‚‚‚AAAAAA@!°Ą‚ƒ3ĐÀÀá1ŁŁĂpàŃ‚ƒ”S€áaÓ5uá!bS€FÖtт3t‡Ăćvw—†T…Ćö'Fv1!‚ö6661!Q€&6661Ł†66662ô&66666666666666666666666666666666666661AÂAÂƒAÂƒAÂƒÂƒÂƒƒƒâü·Đ 3ŸBàꉜčf"ĘGà?r‰"^àŰÉ}j©fWbìBä`öŽŠKç0ÚՆÆU)^}»‡äÄ4ÌźĆۂÈŁĂUš62^?9w1+ŁÚ<“„ûȘ€ ĆsTûȘ€WcAÊiAíuܐàek1·ÎĆx^ yÛțŽÓhó©"cuk3)š5ß –Łû@œ xŰæ]‘ w(=ŁÎčM¶»Ű+‚•F_ĆąÖ9Ü"ÍŻśŽ†bÊЧ%r({·IzȘJ„yœ'JÖlĆźÀÒ@äńCĘșïÿČcylƒÙàZuÇ+1‡ÀeG#FąidÚÿêRÜìŽ,>“‚idÚ<đőt +-1äÊÿêR܄֏^vj"ÿì|Ÿ„ÜÂv}-°VžK&șb€ó'wȘ€@kőî^§K”çY°‰j…•†Ÿâ#ż8UIJèÎČbQŻÂÊĆääÒí#ÎÜ9ˆCžś6ô¶ą”W8bőŹąÊǒ/í»›j>ÌFaПę2[ E(Â,ŹĘyö€ó?mB"\Űt-2{§Ì|HçșŠ©V/”>śÀmDÿëÓ<#H2áŠnméŒ0őŹÔ@u Ÿ]P§­Ï9;DzJąi#üÓ# ó çW`CùÓž¶Ćsè54Ž8rŒ&•íyźFæ \Ă{IÊìcpíûƒž3Ú%“ÎȘS™“è°Ü›{,‘5†öó•âł0rê=2\ÇĆT­a}{ òn5íNaVč€đ•±DȚÉ…ŹßđȘ‘Kâ7țNă4 înÀ5&6ŁÙd‰§EʜÂc}BȘF\g ț7Âb†Úû.ŰźÒ Ëç <‹„)#w…ĄûžV±Äă—Q!čžf (mŻłK•ÿ \N1fŃ2BivÖă›Îęđ}jÊÁD8žËš*RŽÒí&ŽspÎàZ~zŽ.Ź+<ÂvæXđ)(ÓKʚBżŁƒÜŒZaß#ë˜łòś7š,ź„;„ś‚fÚË€™BàżßBŒđíoÇ&1Ș“‘&L1òw±Z:ù čÄeÛYŽ—;Ś~ȱU$+^Ïč‡Ń~Čłąù;r0Ä6ÖlćQ”BȘ[€ÍŁmrÆ[Ÿ„ĂÂö &üEbŽ-{:>D)p=”dÓ:0ż» Đ«Zžâ4]%Mÿ܃‡šîžó’&ÔC:ƔȘÈčĄÓ°šY=óì-ˆ|áæ[ŸŁŚ™Ć•ź4tˆKŽžŃMFpZ-_óÛΊ [MŻŸap#‹ŸrŹČ„,Yș„>e…*ĘCč ë,ŽŒ S6ÖM#ϓÇhžŃ8}u*ŽY‰S6ÖOcx;DŁ?°ám…p71†qçü©šZ†qÙí1€Í”–L_Ybï pEărŚr‚ą2j™ŠrĐÇĘEYbžÉ{‹FèV‹Ô šfmź,&ŸŽ ì)šEńԜÉ}ș7œ ©_Âs yZŃßèhƒmR&—HòôČČï7îyÂ߀‹(n€ćxH&•¶șgʱ ČČwœT)|1$j•€ÙćD-Ü:ô?0ț+°OÊŐĘ šoR˜”Ńp[ÓïD%ä†fÚ„i6LpÿÔê#YXȚ„(xú/`ƐÛTÍ3—ÿ`§]AoŐŒX‹±ć*œ„`6ÔÊf™ăr{D(č€zùGë+#wÖăʔ‹~‘Ï€VVF ə4́¶žĘÿQÓ„ŁÄg!tĆæ ±4ͶŽ[슠)wbŽ€ÿüBâ&ț G°šQÔì8?ËáœÆ,ùʰ"$%m6±śćëjR\e߁&•¶șIš*/ű?u놠ܶĆd<Î*€#$%4™¶čŃ{–8ùđ;eözăq=Ö+€ßú~`SP”w€fi3meË‘śaZœ}žÜèbd˜Ó6ÚÊŁ—ŁćMDŸVXOÛۋx?ŠžžOœÄȘ’'żÚYXïšÜ¶Źšg‹ —ËwƒqúuÀÓ6ژPËæ’4'Čę U"'–Ö°ïź JBäëțś*€ČČ!9đÛU /\e‹Lę™ć±ƒŻx„SëA@j•€Òg<ßrîŰ2ˆO抶ԁÚG‰ùW`/Žï–š0$ޘÛ\òÆ&ï]ț)šwó«`‘ĆÊÊÏ7!~^‘ƒ¶Č“4Ï+‰èà,Ź‹šTÔŸ\^LÓ9FÚㄎVŃæïGóśÀFć#éęUHßÈJ;ˆâĆarUăLÛja3,· Ț/Akÿ»ăÂÊÁ*á–-36Ő3 磔#…U%(ÍĘő#‹ ܒ…Ó0żń¶ŠXŽÏć gđöŠ©@F,ŹqżdźDÊ'6ŐÿĂ,Zf]k úô‰äˆZŠ †ç?“˜ šfmŹčcʅ{…ążçœ_§$ڙLÓ9Xá}Ő)€xçáDVîXăŐgqÂ)š‡ÒȚ| #P”4ͶŠ2Æû·ûâĆdÿ:ˆ#Ë™›j™…śKúčYZš„>@n‰"‹^žuŰôäĄiüÁ–-36Ős çïtŒqa·I~BŠAŒdÄ@883ȶuś&|kŁęčQß„•Œ•U#xÉ!ˆ€p: pg‘lëîLűŚGûrŁżK+y*ȘFń’CàtàÏ"ÙŚÜ™ńźöćG~–VòUTŠ©8Oԉx&i™¶Čć#!ńebȚ_z.쎏ś±ĂR—ź_Đ,źÍÂä† FS4ÌÛYoŸƒż‘tóóć*–Vš‹Öđ 8$/J4Ͷ€Ì±ÂîŃ­è,Źćkéü#p“¶ț!:?JLË晶Öê"SQIä@úz;qÏĄ]‘À7ŸŒ±i™3 Í”ș^€†Ą '«+„RŒù2û§Z5ŃțÆ83Ê^]žQÔdQ’ąëX^LąsŸŸęb°m«ž ±ižțùȘJ©@F,ŹqżdźDÊ'6ŐÿĂ,Zf]k úô‰ä€OggSC 1«EûÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿV0 ƒLœśŸśȚûß{ï}ïœśŸ,>śȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚù;ûÇßśŸȚ}ÿyśęçßśŸȚ}ÿyśęçßśęïœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœç‡Țûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß'xûțóïûÏżï>ÿŒûțóïûÏżï>ÿŒûțÿœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŒđûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{äïïȚ}ÿyśęçßśŸȚ}ÿyśęçßśŸßśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśž{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï|ęăïûÏżï>ÿŒûțóïûÏżï>ÿŒûțóïûțśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚóĂï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}ï“żŒ}ÿyśęçßśŸȚ}ÿyśęçßśŸȚ}ÿȚûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûȚx}ïœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœòwśżï>ÿŒûțóïûÏżï>ÿŒûțóïûÏżïûß{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ÏœśŸśȚûß{ï}ïœśŸśȚûß{ï}ïœśŸNțńśęçßśŸȚ}ÿyśęçßśŸȚ}ÿyśęÿ{ï}ïœśŸśȚûß{ï}ïœśŸśȚûß{ïyáśŸśȚûß{ï}ïœśŸśȚûß{Àż{ï}ïÀśŒ«ś‹T>đȘePûš}ćSïx𫜋T>đȘePûš}ćSïx𫜋T>đȘePûš}ćSïx𫜋T>đȘePûš}ćSïx𫜋T>đȘePûš}ćSïx𫜋T>đȘePûš}ćSïx𫜋T>đȘePûš}ćSïx𫜋T>đȘePûš}ćSïx𫜋T>đȘePûš}ćSïx𫜋T>đȘePûš}ćSïx𫜋T>đȘePûš}ćSïŁĄŰèv:Œ}ăÒš}ăïUŒ}ăÈߌ}ăïPûÇȚ8ȘxûǑżxûÇȚ>:ĄśŒqT>ńś#~ńśŒ|uCïxâš}ăïFęăïxűê‡Ț>ńĆPûÇȚ<ûÇȚ>ńńŐŒ}㊡śŒyśŒ}ăăȘxûÇCïxò7ïxûÇÇT>ńśŽ*‡Ț>ńäoȚ>ńśŽš}ăïUŒ}ăÈߌ}ăïPûÇȚ8ȘxûǑżxûÇȚ>:ĄśŒqT>ńś#~ńôˆ|C° OœśŸśȚûß{ï}ï—À 4ûß{ï}ïœśŸśȚùpì OœśŸśȚûß{ï}ï—À 4ûß{ï}ïœśŸśȚùpì OœśŸśȚûß{ï}ï—À 4ûß{ï}ïœśŸśȚùpì OœśŸśȚûß{ï}ï—À  Ș*ȘŠ   Ș*ȘŠ   Ș*ȘŠ   Ș*ȘŠ   Ș*ȘŠ   Ș*ȘŠ   Ș*ȘŠ   Ș*ȘŠ   Ș*ȘŠ   Ș*ȘŠ   Ș*ȘŠ  @Ș  (  Ș€Ș  (  Ș€Ș  (  Ș€Ș  (  Ș€Ș  (  Ș€Ș  ([Đ!€À ˜À )ˆ@C ąRš0Ł3yGŒÌ0•¶Ú› Ł$°-¶a€*@ € !@@`€<TfoŒfa€­¶ÛFQ¶¶ÀŽ` žr80€„€đp`Ffń›ÆfJÛmŽ``[l F©0€„€đQ™Œfń™†R¶ÛmFŰÛр*@ € !@@`€<TfoŒfa€­¶ÛFQ¶¶ÀŽ` žr80€„€đp`Ffń›ÆfJÛmŽ``[l F©0€„€đQ™Œfń™†R¶ÛmFŰÛр*@ € !@@`€<TfoŒfa€­¶ÛFQ¶¶ÀŽ` žr80€„€đp`Ffń›ÆfJÛmŽ``[l F©0€„€đQ™Œfń™†R¶ÛmFŰÛр*@ € !@@`€<TfoŒfa€­¶ÛFQ¶¶ÀŽ` žr80€„€đp`Ffń›ÆfJÛmŽ``[l F©0€„€đQ™Œfń™†R¶ÛmFŰÛр*@ € !@@`€<TfoŒfa€­¶ÛFQ¶¶ÀŽ` žr80€„€đp`Ffń›ÆfJÛmŽ``[l F©0€„€đQ™Œfń™†R¶ÛmFŰÛр*@ € !@@`€<TfoŒfa€­¶ÛFQ¶¶ÀŽ` žr80€„€đp`Ffń›ÆfJÛmŽ``[l F©0€„€đQ™Œfń™†R¶ÛmFŰÛр*@ € !@@`€<TfoŒfa€­¶ÛFQ¶¶ÀŽ` žr80€„€đp`Ffń›ÆfJÛmŽ``[l F©0€„€đQ™Œfń™†R¶ÛmFŰÛр€`Ą€ @đ€ Æo`‹mŠß-LDXÔßLˆ„ÈZ‚”«@ÂhI(Û X:™·Kő)%&ˆ™&șŽ–œ’ŚŠ@… 4`™È)‹ "Ž-)V… €Èí„rdKő)%)2KZK^Él™™È)‹ "Ž-)V… €Èí„rdKő)%)2KZK^Él™™È)‹ "Ž-)V… €Èí„rdKő)%)2KZK^Él™™È)‹ "Ž-)V… €Èí„rdKő)%)2KZK^Él™™È)‹ "Ž-)V… €Èí„rdKő)%)2KZK^Él™™È)‹ "Ž-)V… €Èí„rdKő)%)2KZK^Él™™È)‹ "Ž-)V… €Èí„rdKő)%)2KZK^Él™™È)‹ "Ž-)V… €Èí„rdKő)%)2KZK^Él™™È)‹ "Ž-)V… €Èí„rdKő)%)2KZK^Él™™È)‹ "Ž-)V… €Èí„rdKő)%)2KZK^Él™™ÈŒ[›AVèučê·KÖæA2ŸöOgę2[-kĆ5äÈYȕé!h2”ÍŚ%ri‰"—é(5!ÛŠAKțÏÙÿLËZöZd ,ÙJ͔ÈL„3uÉ]rdżKôÈa°ŠdżìęŸôÈl”ŻeŠ@ąÍ”ŹÙLÊS7\•Ś&@‹ôżLFÛŠAKțÏÙÿLËZöZd ,ÙJ͔ÈL„3uÉ]rdżKôÈa°ŠdżìęŸôÈl”ŻeŠ@ąÍ”ŹÙLÊS7\•Ś&@‹ôżLFÛŠAKțÏÙÿLËZöZd ,ÙJ͔ÈL„3uÉ]rdżKôÈa°ŠdżìęŸôÈl”ŻeŠ@ąÍ”ŹÙLÊS7\•Ś&@‹ôżLFÛŠAKțÏÙÿLËZöZd ,ÙJ͔ÈL„3uÉ]rdżKôÈa°ŠdżìęŸôÈl”ŻeŠ@ąÍ”ŹÙLÊS7\•Ś&@‹ôżLFÛŠAKțÏÙÿLËZöZd ,ÙJ͔ÈL„3uÉ]rdżKôÈa°ŠdżìęŸôÈl”ŻeŠ@ąÍ”ŹÙLÊS7\•Ś&@‹ôżLFÛŠ@Ïûs—nŹÛ©șĘ{u†Ę’&Ä! iČ„B€"Fąm!D"…ȘȘÚ"$BˆD" ‰‰Žâ$LD"…DDÚ"$BˆD" UUŽDH„!ˆD"5iÄH˜ˆD" ‰‰ŽDH„!ˆD"Ș6«hˆB!ˆD($j&ӈ0!ˆD"5hˆB!ˆD(TmVŃ „B!ˆPHÔM§ `B!ˆD($j&Ń „B!ˆP šÚ­ą"@!„B! ‘š›N"@À„B!ˆPHÔMą"@!„B! Q”[DD€B„B!@#Q6œD€ „B! ‘š›DD€B„B!@*Łj¶ˆ‰„"„B€"Fąm8‰„B!@#Q6ˆ‰ˆD(ÔMąŁŃèôwEëĘ/^jŐșVŹïńŸ7ÆèAtŸß á|Ńèôz;ąő5jĘ+VwÆűßăt ‚ș_ï…đŸèôz=ŃzśKښ”n•«;ă|ońșA]/ƒśÂű_ôz=Žèœ{„ëÍZ·J՝ńŸ7ÆűĘ ‚.—Áûá|/ƒú=Gt^œÒőæ­[„jÎűßă|n„AKàęđŸÁęGŁș/^ézóV­Ò”g|ońŸ7B ‹„đ~ű_ àțGŁŃĘŻtœy«VéZłŸ7ÆűߥEÒű?|/…đGŁŃèș^ŒŐ«t­Yßă|oĐ‚"é|ŸÂű?ŁŃèôwEëĘ/^jŐșVŹïńŸ7ÆèAtŸß á|Ńèôz;ąő5jĘ+VwÆűßăt ‚ș_ï…đŸèôz=ŃzśKښ”n•«;ă|ońșA]/ƒśÂű_ôfŒŐń2űwuŃJ—JTŸőŸ·ÖúÜÆ™ę6›LÿwuŃJ—JTőŸ·ÖúÜÆ™ę6›LÿwuŃJ—JTőŸ·ÖúÜÆ™ę6›LÿwuŃJ—JTőŸ·ÖúÜÆ™ę6›LÿwuŃJ—JTőŸ·ÖúÜÆ™ę6›LÿwuŃJ—JTőŸ·ÖúÜÆ™ę6›LÿwuŃJ—JTőŸ·ÖúÜÆ™ę6›LÿwuŃJ—JTőŸ·ÖúÜÆ™ę6›LÿwuŃJ—JTőŸ·ÖúÜÆ™ę6›LÿwuŃJ—JTőŸ·ÖúÜÆ™ę6›LÿwuŃJ—JTőŸ·ÖúÜÆ™ę6›Lÿ“ŸŻéŒ(PĄB… hŃŁP (PĄB…€4hÔ @ B… 4hŃšCA (SABÀ 4j @B… (P°F€$(PĄ`5h @ĄB… h(XŁF@(PĄB… hŃŁP€ ,ŃŁF  (PĄM 4hŃš@ (PĄBÀ 4j @ĄB…€4hÔ! … ) Ą`5 @ĄB… (XŁF@(P°F€4 PĄB…4,ŃŁF (PĄB… 4hŃš@@… hŃŁP†‚ (PŠ‚…€4hÔ @… (PĄ`5 HPĄBÀ 4jĐ@B… ĐP°F€ PĄB†hĐa"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$H‘"D‰$WDH‘"ș"D‰$H‘]"DŠè‰$H‘"EtD‰+ą$H‘"D‰Ń$Hźˆ‘"D‰$WDH‘"ș"D‰$H‘]"DŠè‰$H‘"EtD‰+ą$H‘"D‰Ń$Hźˆ‘"D‰$WDH‘"ș"D‰$H‘]"DŠè‰$H‘"EtD‰+ą$H‘"D‰ą4H°áÇ8páǝÇ8páÇw8páÇ8yÜ8páÇ8páçpáÇ8páǝÇ8páÇw8páÇ8yÜ8páÄBˆ ‚ ‚(P B±Œ`Bˆ ‚ ‚(P B±Œ`Bˆ ‚ ‚(P B±Œ`Bˆ ‚ ‚(P B±Œ`Bˆ ‚ ‚(P B±Œ`Bˆ ‚ ‚(P B±Œ`Bˆ ‚ ‚(P B±Œ`Bˆ ‚ ‚(P B±Œ`Bˆ ‚ ‚(P B±Œ`Bˆ ‚ ‚(P B±Œ`Bˆ ‚ ‚(P B±Œ`D X¶ÛmR„Jœç9¶ÛmR„Jœç9¶ÛmR„Jœç9¶ÛmR„Jœç9¶ÛmR„Jœç9¶ÛmR„Jœç9¶ÛmR„Jœç9¶ÛmR„Jœç9¶ÛmR„Jœç9¶ÛmR„Jœç9¶ÛmR„Jœç9”N`Á™™ĘĘ ÌÎîê33Żș ™ĘĐ`ÌÌîî 33:û Á™™ĘĘ ÌÎîê33Żș ™ĘĐ`ÌÌîî 33:û Á™™ĘĘ ÌÎîê33Żș ™ĘĐ`ÌÌîî 33:û Á™™ĘĘ ÌÎîê33Żș ™ĘÔgrI]Ęæf$–fffbI]Ęæf$–fffbI]Ęæf$–fffbI]Ęæf$–fffbI]Ęæf$–fffbI]Ęæf$–fffbI]Ęæf$–fffbI]Ęæf$–fffbI]Ęæf$–fffbI]Ęæf$–fffbI]Ęæf$–fffbŒY™™‘ŃŃŃŚwy™‘ŃŃŃŚwy™‘ŃŃŃŚwy™‘ŃŃŃŚwy™‘ŃŃŃŚwy™‘ŃŃŃŚwy™‘ŃŃŃŚwy™‘ŃŃŃŚwy™‘ŃŃŃŚwy™‘ŃŃŃŚwy™‘ŃŃŃŚwyoŸ>}@>|ùőùóçÔçϟPŸ>}@>|ùőùóçÔçϟPŸ>}@>|ùőùóçÔçϟPŸ>}@>|ùőùóçÔçϟPŸ>}@>|ùőùóçÔçϟPŸ>}@>|ùőùśűŰŰŰۘ˜˜˜èèèèŰŰŰۘ˜˜˜ŰŰŰŰŰŰŰۘ˜˜˜dᓆN8ŰŰŰۘ˜˜˜ŰŰŰŰŰŰŰۘ˜˜˜dᓆN8ŰŰŰۘ˜˜˜ŰŰŰŰŰŰŰۘ˜˜˜dᓆN8ŰŰŰۘ˜˜˜ŰŰŰŰŰŰŰۘ˜˜˜dᓆN8ŰŰŰۘ˜˜˜ŰŰŰŰŰŰŰۘ˜˜˜dᓆN8ŰŰŰۘ˜˜˜ŰŰŰŰŰŰŰۘ˜˜˜dᓆN8ŰŰŰۘ˜˜˜ŰŰŰŰŰŰŰۘ˜˜˜dᓆN8ŰŰŰۘ˜˜˜ŰŰŰŰŰŰŰۘ˜˜˜dᓆN8ŰŰŰۘ˜˜˜ŰŰŰŰŰŰŰۘ˜˜˜dᓆN8ŰŰŰۘ˜˜˜ŰŰŰŰŰŰŰۘ˜˜˜dᓆN8ŰŰŰۘ˜˜˜ŰŰŰŰۘdăbc[„DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD@Űö=c0’I$ccŰö=ŒÂI$‘cŰö3 $’F6=cŰÌ$’IŰö=c0’I$ccŰö=ŒÂI$‘cŰö3 $’F6=cŰÌ$’IŰö=c0’I$ccŰö=ŒÂI$‘cŰö3 $’F68”I$’I$’I$’I$’I$’I%@Đ4 @Đ4 @Đ4 @Đ4 @Đ4 @Đ4 @Đ4 @Đ4 @Đ4 @Đ4 @Đ4 @Đ4 @Đ4 @Đ4 @Đ4 @Đ4 @Đ4 @Đ4 @Đ4 @Đ4 @Đ4 @Đ4 @Đ;NÓŽí%(!Ûm¶Ûm¶Ûm¶Ûm¶Ûm¶ŰhZJ?7‡Âű_ á|/…đŸÂű_ đß~©NâwžÄî'q;‰ÜNâwžàüÀTOggSJ 1‘b€“OggSK 1&˜Dmediascanner2-0.115/test/noscan/000077500000000000000000000000001436755250000165025ustar00rootroot00000000000000mediascanner2-0.115/test/noscan/.nomedia000066400000000000000000000000001436755250000201050ustar00rootroot00000000000000mediascanner2-0.115/test/qml/000077500000000000000000000000001436755250000160125ustar00rootroot00000000000000mediascanner2-0.115/test/qml/tst_albumsmodel.qml000066400000000000000000000101471436755250000217260ustar00rootroot00000000000000import QtQuick 2.0 import QtTest 1.0 import MediaScanner 0.1 Item { id: root MediaStore { id: store } AlbumsModel { id: model store: store } SignalSpy { id: modelStatus target: model signalName: "statusChanged" } TestCase { name: "AlbumsModelTests" function waitForReady() { while (model.status == AlbumsModel.Loading) { modelStatus.wait(); } compare(model.status, AlbumsModel.Ready); } function cleanup() { model.artist = undefined; model.albumArtist = undefined; model.genre = undefined; waitForReady(); } function test_initial_state() { compare(model.artist, undefined); compare(model.albumArtist, undefined); compare(model.genre, undefined); compare(model.count, 4); compare(model.get(0, AlbumsModel.RoleTitle), "April Uprising"); compare(model.get(0, AlbumsModel.RoleArtist), "The John Butler Trio"); compare(model.get(0, AlbumsModel.RoleDate), "2010-01-01"); compare(model.get(0, AlbumsModel.RoleGenre), "roots"); compare(model.get(0, AlbumsModel.RoleArt), "image://albumart/artist=The%20John%20Butler%20Trio&album=April%20Uprising"); compare(model.get(1, AlbumsModel.RoleTitle), "Ivy and the Big Apples"); compare(model.get(1, AlbumsModel.RoleArtist), "Spiderbait"); compare(model.get(1, AlbumsModel.RoleDate), "1996-10-04"); compare(model.get(1, AlbumsModel.RoleGenre), "rock"); compare(model.get(1, AlbumsModel.RoleArt), "image://albumart/artist=Spiderbait&album=Ivy%20and%20the%20Big%20Apples"); compare(model.get(2, AlbumsModel.RoleTitle), "Spiderbait"); compare(model.get(2, AlbumsModel.RoleArtist), "Spiderbait"); compare(model.get(2, AlbumsModel.RoleDate), "2013-11-15"); compare(model.get(2, AlbumsModel.RoleGenre), "rock"); compare(model.get(2, AlbumsModel.RoleArt), "image://albumart/artist=Spiderbait&album=Spiderbait"); compare(model.get(3, AlbumsModel.RoleTitle), "Sunrise Over Sea"); compare(model.get(3, AlbumsModel.RoleArtist), "The John Butler Trio"); compare(model.get(3, AlbumsModel.RoleDate), "2004-03-08"); compare(model.get(3, AlbumsModel.RoleGenre), "roots"); compare(model.get(3, AlbumsModel.RoleArt), "image://albumart/artist=The%20John%20Butler%20Trio&album=Sunrise%20Over%20Sea"); } function test_limit() { // The limit property is deprecated now, but we need to // keep it until music-app stops using it. compare(model.limit, -1); model.limit = 1; compare(model.limit, -1); } function test_artist() { model.artist = "The John Butler Trio"; waitForReady(); compare(model.count, 2); compare(model.get(0, AlbumsModel.RoleTitle), "April Uprising"); compare(model.get(0, AlbumsModel.RoleArtist), "The John Butler Trio"); model.artist = "unknown"; waitForReady(); compare(model.count, 0); } function test_album_artist() { model.albumArtist = "The John Butler Trio"; waitForReady(); compare(model.count, 2); compare(model.get(0, AlbumsModel.RoleTitle), "April Uprising"); compare(model.get(0, AlbumsModel.RoleArtist), "The John Butler Trio"); model.albumArtist = "unknown"; waitForReady(); compare(model.count, 0); } function test_genre() { model.genre = "rock"; waitForReady(); compare(model.count, 2); compare(model.get(0, AlbumsModel.RoleTitle), "Ivy and the Big Apples"); compare(model.get(1, AlbumsModel.RoleTitle), "Spiderbait"); model.genre = "unknown"; waitForReady(); compare(model.count, 0); } } } mediascanner2-0.115/test/qml/tst_artistsmodel.qml000066400000000000000000000041641436755250000221360ustar00rootroot00000000000000import QtQuick 2.0 import QtTest 1.0 import MediaScanner 0.1 Item { id: root MediaStore { id: store } ArtistsModel { id: model store: store } SignalSpy { id: modelStatus target: model signalName: "statusChanged" } TestCase { name: "ArtistsModelTests" function waitForReady() { while (model.status == ArtistsModel.Loading) { modelStatus.wait(); } compare(model.status, ArtistsModel.Ready); } function cleanup() { model.albumArtists = false; model.genre = undefined; waitForReady(); } function test_initial_state() { compare(model.albumArtists, false); compare(model.genre, undefined); waitForReady(); compare(model.count, 2); compare(model.get(0, ArtistsModel.RoleArtist), "Spiderbait"); compare(model.get(1, ArtistsModel.RoleArtist), "The John Butler Trio"); } function test_limit() { // The limit property is deprecated now, but we need to // keep it until music-app stops using it. compare(model.limit, -1); model.limit = 1; compare(model.limit, -1); } function test_album_artists() { model.albumArtists = true; waitForReady(); compare(model.count, 2); compare(model.get(0, ArtistsModel.RoleArtist), "Spiderbait"); compare(model.get(1, ArtistsModel.RoleArtist), "The John Butler Trio"); } function test_genre() { model.genre = "rock"; waitForReady(); compare(model.count, 1); compare(model.get(0, ArtistsModel.RoleArtist), "Spiderbait"); model.genre = "roots"; waitForReady(); compare(model.count, 1); compare(model.get(0, ArtistsModel.RoleArtist), "The John Butler Trio"); model.genre = "unknown"; waitForReady(); compare(model.count, 0); } } } mediascanner2-0.115/test/qml/tst_genresmodel.qml000066400000000000000000000021401436755250000217200ustar00rootroot00000000000000import QtQuick 2.0 import QtTest 1.0 import MediaScanner 0.1 Item { id: root MediaStore { id: store } GenresModel { id: model store: store } SignalSpy { id: modelStatus target: model signalName: "statusChanged" } TestCase { name: "GenresModelTests" function waitForReady() { while (model.status == GenresModel.Loading) { modelStatus.wait(); } compare(model.status, GenresModel.Ready); } function cleanup() { } function test_initial_state() { waitForReady(); compare(model.count, 2); compare(model.get(0, ArtistsModel.RoleGenre), "rock"); compare(model.get(1, ArtistsModel.RoleGenre), "roots"); } function test_limit() { // The limit property is deprecated now, but we need to // keep it until music-app stops using it. compare(model.limit, -1); model.limit = 1; compare(model.limit, -1); } } } mediascanner2-0.115/test/qml/tst_mediastore.qml000066400000000000000000000033711436755250000215570ustar00rootroot00000000000000import QtQuick 2.0 import QtTest 1.0 import MediaScanner 0.1 Item { id: root MediaStore { id: store } TestCase { name: "MediaStoreTests" function test_lookup() { var song = store.lookup("/unknown.ogg"); compare(song, null, "song == null"); song = store.lookup("/path/foo1.ogg"); verify(song !== null, "song != null"); var checkAttr = function (attr, value) { compare(song[attr], value, "song." + attr + " == \"" + value + "\""); }; checkAttr("filename", "/path/foo1.ogg"); checkAttr("uri", "file:///path/foo1.ogg"); checkAttr("contentType", "audio/ogg"); checkAttr("eTag", "etag"); checkAttr("title", "Straight Through The Sun"); checkAttr("author", "Spiderbait"); checkAttr("album", "Spiderbait"); checkAttr("albumArtist", "Spiderbait"); checkAttr("date", "2013-11-15"); checkAttr("genre", "rock"); checkAttr("discNumber", 1); checkAttr("trackNumber", 1); checkAttr("duration", 235); checkAttr("width", 0); checkAttr("height", 0); checkAttr("latitude", 0.0); checkAttr("longitude", 0.0); checkAttr("art", "image://albumart/artist=Spiderbait&album=Spiderbait"); } function test_query() { var songs = store.query("unknown", MediaStore.AudioMedia); compare(songs.length, 0, "songs.length == 0"); var songs = store.query("Pony", MediaStore.AudioMedia); compare(songs.length, 1, "songs.length == 1"); compare(songs[0].title, "Buy Me a Pony"); } } } mediascanner2-0.115/test/qml/tst_songsearchmodel.qml000066400000000000000000000017171436755250000226020ustar00rootroot00000000000000import QtQuick 2.0 import QtTest 1.0 import MediaScanner 0.1 Item { id: root MediaStore { id: store } SongsSearchModel { id: model store: store } SignalSpy { id: modelStatus target: model signalName: "statusChanged" } TestCase { name: "SongsSearchModelTests" function waitForReady() { while (model.status == SongsSearchModel.Loading) { modelStatus.wait(); } compare(model.status, SongsSearchModel.Ready); } function test_search() { // By default, the model lists all songs. waitForReady(); compare(model.count, 7, "songs_model.count == 7"); model.query = "revolution"; waitForReady(); compare(model.count, 1, "songs_model.count == 1"); compare(model.get(0, SongsSearchModel.RoleTitle), "Revolution"); } } } mediascanner2-0.115/test/qml/tst_songsmodel.qml000066400000000000000000000072021436755250000215720ustar00rootroot00000000000000import QtQuick 2.0 import QtTest 1.0 import MediaScanner 0.1 Item { id: root MediaStore { id: store } SongsModel { id: model store: store } SignalSpy { id: modelStatus target: model signalName: "statusChanged" } TestCase { name: "SongsModelTests" function waitForReady() { while (model.status == SongsModel.Loading) { modelStatus.wait(); } compare(model.status, SongsModel.Ready); } function cleanup() { model.artist = undefined; model.albumArtist = undefined; model.album = undefined; model.genre = undefined; waitForReady(); } function test_initial_state() { compare(model.artist, undefined); compare(model.albumArtist, undefined); compare(model.album, undefined); compare(model.count, 7); compare(model.get(0, SongsModel.RoleTitle), "Buy Me a Pony") compare(model.get(0, SongsModel.RoleAlbum), "Ivy and the Big Apples"); compare(model.get(0, SongsModel.RoleAuthor), "Spiderbait"); compare(model.get(1, SongsModel.RoleTitle), "Straight Through The Sun"); compare(model.get(2, SongsModel.RoleTitle), "It's Beautiful"); compare(model.get(3, SongsModel.RoleTitle), "Revolution"); compare(model.get(4, SongsModel.RoleTitle), "One Way Road"); compare(model.get(5, SongsModel.RoleTitle), "Peaches & Cream"); compare(model.get(6, SongsModel.RoleTitle), "Zebra"); } function test_limit() { // The limit property is deprecated now, but we need to // keep it until music-app stops using it. compare(model.limit, -1); model.limit = 1; compare(model.limit, -1); } function test_artist() { model.artist = "The John Butler Trio"; waitForReady(); compare(model.count, 4); compare(model.get(0, SongsModel.RoleTitle), "Revolution"); compare(model.get(0, SongsModel.RoleAuthor), "The John Butler Trio"); model.artist = "unknown"; waitForReady(); compare(model.count, 0); } function test_album_artist() { model.albumArtist = "The John Butler Trio"; waitForReady(); compare(model.count, 4); compare(model.get(0, SongsModel.RoleTitle), "Revolution"); compare(model.get(0, SongsModel.RoleAuthor), "The John Butler Trio"); model.albumArtist = "unknown"; waitForReady(); compare(model.count, 0); } function test_album() { model.album = "Sunrise Over Sea"; waitForReady(); compare(model.count, 2); compare(model.get(0, SongsModel.RoleTitle), "Peaches & Cream"); compare(model.get(0, SongsModel.RoleAuthor), "The John Butler Trio"); model.albumArtist = "unknown"; waitForReady(); compare(model.count, 0); } function test_genre() { model.genre = "rock"; waitForReady(); compare(model.count, 3); compare(model.get(0, SongsModel.RoleTitle), "Buy Me a Pony"); compare(model.get(1, SongsModel.RoleTitle), "Straight Through The Sun"); compare(model.get(2, SongsModel.RoleTitle), "It's Beautiful"); model.albumArtist = "unknown"; waitForReady(); compare(model.count, 0); } } } mediascanner2-0.115/test/services/000077500000000000000000000000001436755250000170445ustar00rootroot00000000000000mediascanner2-0.115/test/services/com.canonical.MediaScanner2.Extractor.service.in000066400000000000000000000001701436755250000301210ustar00rootroot00000000000000[D-BUS Service] Name=com.canonical.MediaScanner2.Extractor Exec=@CMAKE_BINARY_DIR@/src/extractor/mediascanner-extractor mediascanner2-0.115/test/services/com.canonical.MediaScanner2.service.in000066400000000000000000000001531436755250000261500ustar00rootroot00000000000000[D-BUS Service] Name=com.canonical.MediaScanner2 Exec=@CMAKE_BINARY_DIR@/src/ms-dbus/mediascanner-dbus-2.0 mediascanner2-0.115/test/test_config.h.in000066400000000000000000000001401436755250000202760ustar00rootroot00000000000000#define TEST_DIR "${CMAKE_CURRENT_BINARY_DIR}" #define SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}" mediascanner2-0.115/test/test_dbus.cc000066400000000000000000000061701436755250000175300ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include class MediaStoreDBusTests : public ::testing::Test { protected: virtual void SetUp() override { ::testing::Test::SetUp(); message = core::dbus::Message::make_method_call( "org.example.Name", core::dbus::types::ObjectPath("/org/example/Path"), "org.example.Interface", "Method"); } core::dbus::Message::Ptr message; }; TEST_F(MediaStoreDBusTests, mediafile_codec) { mediascanner::MediaFile media = mediascanner::MediaFileBuilder("a") .setContentType("type") .setETag("etag") .setDate("1900") .setTitle("b") .setAuthor("c") .setAlbum("d") .setAlbumArtist("e") .setGenre("f") .setDiscNumber(0) .setTrackNumber(1) .setDuration(5) .setWidth(640) .setHeight(480) .setLatitude(20.42) .setLongitude(-30.67) .setModificationTime(4200) .setType(mediascanner::AudioMedia); message->writer() << media; EXPECT_EQ("(sssssssssiiiiiddbti)", message->signature()); EXPECT_EQ(core::dbus::helper::TypeMapper::signature(), message->signature()); mediascanner::MediaFile media2; message->reader() >> media2; EXPECT_EQ(media, media2); } TEST_F(MediaStoreDBusTests, album_codec) { mediascanner::Album album("title", "artist", "date", "genre", "art_file", true, 1); message->writer() << album; EXPECT_EQ("(sssssbi)", message->signature()); EXPECT_EQ(core::dbus::helper::TypeMapper::signature(), message->signature()); mediascanner::Album album2; message->reader() >> album2; EXPECT_EQ("title", album2.getTitle()); EXPECT_EQ("artist", album2.getArtist()); EXPECT_EQ("date", album2.getDate()); EXPECT_EQ("genre", album2.getGenre()); EXPECT_EQ("art_file", album2.getArtFile()); EXPECT_EQ(true, album2.getHasThumbnail()); EXPECT_EQ(album, album2); } TEST_F(MediaStoreDBusTests, filter_codec) { mediascanner::Filter filter; filter.setArtist("Artist1"); filter.setAlbum("Album1"); filter.setAlbumArtist("AlbumArtist1"); filter.setGenre("Genre"); filter.setOffset(42); filter.setLimit(100); message->writer() << filter; EXPECT_EQ("a{sv}", message->signature()); EXPECT_EQ(core::dbus::helper::TypeMapper::signature(), message->signature()); mediascanner::Filter other; message->reader() >> other; EXPECT_EQ(filter, other); } TEST_F(MediaStoreDBusTests, filter_codec_empty) { mediascanner::Filter empty; message->writer() << empty; EXPECT_EQ("a{sv}", message->signature()); mediascanner::Filter other; message->reader() >> other; EXPECT_EQ(empty, other); } int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } mediascanner2-0.115/test/test_extractorbackend.cc000066400000000000000000000220221436755250000221100ustar00rootroot00000000000000/* * Copyright (C) 2014 Canonical, Ltd. * * Authors: * Jussi Pakkanen * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include "test_config.h" #include #include #include #include #include using namespace std; using namespace mediascanner; namespace { bool supports_decoder(const std::string& format) { typedef std::unique_ptr CapsPtr; static std::vector formats; if (formats.empty()) { std::unique_ptr decoders( gst_element_factory_list_get_elements(GST_ELEMENT_FACTORY_TYPE_DECODER, GST_RANK_NONE), gst_plugin_feature_list_free); for (const GList* l = decoders.get(); l != nullptr; l = l->next) { const auto factory = static_cast(l->data); const GList* templates = gst_element_factory_get_static_pad_templates(factory); for (const GList* l = templates; l != nullptr; l = l->next) { const auto t = static_cast(l->data); if (t->direction != GST_PAD_SINK) { continue; } CapsPtr caps(gst_static_caps_get(&t->static_caps), gst_caps_unref); if (gst_caps_is_any(caps.get())) { continue; } formats.emplace_back(std::move(caps)); } } } char *end = nullptr; GstStructure *structure = gst_structure_from_string(format.c_str(), &end); assert(structure != nullptr); assert(end == format.c_str() + format.size()); // GstCaps adopts the GstStructure CapsPtr caps(gst_caps_new_full(structure, nullptr), gst_caps_unref); for (const auto &other : formats) { if (gst_caps_is_always_compatible(caps.get(), other.get())) { return true; } } return false; } } class ExtractorBackendTest : public ::testing::Test { protected: ExtractorBackendTest() { } virtual ~ExtractorBackendTest() { } virtual void SetUp() override { } virtual void TearDown() override { } }; TEST_F(ExtractorBackendTest, init) { ExtractorBackend extractor; } TEST_F(ExtractorBackendTest, extract_vorbis) { ExtractorBackend e; string testfile = SOURCE_DIR "/media/testfile.ogg"; DetectedFile df(testfile, "etag", "audio/ogg", 42, AudioMedia); MediaFile file = e.extract(df); EXPECT_EQ(file.getType(), AudioMedia); EXPECT_EQ(file.getTitle(), "track1"); EXPECT_EQ(file.getAuthor(), "artist1"); EXPECT_EQ(file.getAlbum(), "album1"); EXPECT_EQ(file.getDate(), "2013"); EXPECT_EQ(file.getTrackNumber(), 1); EXPECT_EQ(file.getDuration(), 5); } TEST_F(ExtractorBackendTest, extract_mp3) { ExtractorBackend e; string testfile = SOURCE_DIR "/media/testfile.mp3"; DetectedFile df(testfile, "etag", "audio/mpeg", 42, AudioMedia); MediaFile file = e.extract(df); EXPECT_EQ(file.getType(), AudioMedia); EXPECT_EQ(file.getTitle(), "track1"); EXPECT_EQ(file.getAuthor(), "artist1"); EXPECT_EQ(file.getAlbum(), "album1"); EXPECT_EQ(file.getGenre(), "Hip-Hop"); EXPECT_EQ(file.getDate(), "2013-06-03"); EXPECT_EQ(file.getTrackNumber(), 1); EXPECT_EQ(file.getDuration(), 1); } TEST_F(ExtractorBackendTest, extract_m4a) { ExtractorBackend e; string testfile = SOURCE_DIR "/media/testfile.m4a"; DetectedFile df(testfile, "etag", "audio/mpeg4", 42, AudioMedia); MediaFile file = e.extract(df); EXPECT_EQ(file.getType(), AudioMedia); EXPECT_EQ(file.getTitle(), "Title"); EXPECT_EQ(file.getAuthor(), "Artist"); EXPECT_EQ(file.getAlbum(), "Album"); EXPECT_EQ(file.getGenre(), "Rock"); EXPECT_EQ(file.getDate(), "2015-10-07"); EXPECT_EQ(file.getTrackNumber(), 4); EXPECT_EQ(file.getDuration(), 1); } TEST_F(ExtractorBackendTest, extract_video) { ExtractorBackend e; MediaFile file = e.extract(DetectedFile( SOURCE_DIR "/media/testvideo_480p.ogv", "etag", "video/ogg", 42, VideoMedia)); EXPECT_EQ(file.getType(), VideoMedia); EXPECT_EQ(file.getDuration(), 1); EXPECT_EQ(file.getWidth(), 854); EXPECT_EQ(file.getHeight(), 480); file = e.extract(DetectedFile( SOURCE_DIR "/media/testvideo_720p.ogv", "etag", "video/ogg", 42, VideoMedia)); EXPECT_EQ(file.getType(), VideoMedia); EXPECT_EQ(file.getDuration(), 1); EXPECT_EQ(file.getWidth(), 1280); EXPECT_EQ(file.getHeight(), 720); file = e.extract(DetectedFile( SOURCE_DIR "/media/testvideo_1080p.ogv", "etag", "video/ogg", 42, VideoMedia)); EXPECT_EQ(file.getType(), VideoMedia); EXPECT_EQ(file.getDuration(), 1); EXPECT_EQ(file.getWidth(), 1920); EXPECT_EQ(file.getHeight(), 1080); } TEST_F(ExtractorBackendTest, extract_photo) { ExtractorBackend e; // An landscape image that should be rotated to portrait MediaFile file = e.extract(DetectedFile( SOURCE_DIR "/media/image1.jpg", "etag", "image/jpeg", 42, ImageMedia)); EXPECT_EQ(ImageMedia, file.getType()); EXPECT_EQ(2848, file.getWidth()); EXPECT_EQ(4272, file.getHeight()); EXPECT_EQ("2013-01-04T08:25:46", file.getDate()); EXPECT_DOUBLE_EQ(-28.249409333333336, file.getLatitude()); EXPECT_DOUBLE_EQ(153.150774, file.getLongitude()); // A landscape image without rotation. file = e.extract(DetectedFile( SOURCE_DIR "/media/image2.jpg", "etag", "image/jpeg", 42, ImageMedia)); EXPECT_EQ(ImageMedia, file.getType()); EXPECT_EQ(4272, file.getWidth()); EXPECT_EQ(2848, file.getHeight()); EXPECT_EQ("2013-01-04T09:52:27", file.getDate()); EXPECT_DOUBLE_EQ(-28.259611, file.getLatitude()); EXPECT_DOUBLE_EQ(153.1727346, file.getLongitude()); } TEST_F(ExtractorBackendTest, extract_photo_date_original) { ExtractorBackend e; MediaFile file = e.extract(DetectedFile( SOURCE_DIR "/media/krillin.jpg", "etag", "image/jpeg", 42, ImageMedia)); EXPECT_EQ(ImageMedia, file.getType()); EXPECT_EQ("2016-01-22T18:28:42", file.getDate()); } void compare_taglib_gst(const DetectedFile d) { GStreamerExtractor gst(5); MediaFileBuilder builder_gst(d.filename); gst.extract(d, builder_gst); TaglibExtractor taglib; MediaFileBuilder builder_taglib(d.filename); ASSERT_TRUE(taglib.extract(d, builder_taglib)); MediaFile media_gst(builder_gst); MediaFile media_taglib(builder_taglib); EXPECT_EQ(media_gst, media_taglib); // And check individual keys to improve error handling: EXPECT_EQ(media_gst.getTitle(), media_taglib.getTitle()); EXPECT_EQ(media_gst.getAuthor(), media_taglib.getAuthor()); EXPECT_EQ(media_gst.getAlbum(), media_taglib.getAlbum()); EXPECT_EQ(media_gst.getAlbumArtist(), media_taglib.getAlbumArtist()); EXPECT_EQ(media_gst.getDate(), media_taglib.getDate()); EXPECT_EQ(media_gst.getGenre(), media_taglib.getGenre()); EXPECT_EQ(media_gst.getDiscNumber(), media_taglib.getDiscNumber()); EXPECT_EQ(media_gst.getTrackNumber(), media_taglib.getTrackNumber()); EXPECT_EQ(media_gst.getDuration(), media_taglib.getDuration()); EXPECT_EQ(media_gst.getHasThumbnail(), media_taglib.getHasThumbnail()); } TEST_F(ExtractorBackendTest, check_taglib_gst_vorbis) { DetectedFile d(SOURCE_DIR "/media/testfile.ogg", "etag", "audio/ogg", 42, AudioMedia); compare_taglib_gst(d); } TEST_F(ExtractorBackendTest, check_taglib_gst_mp3) { if (!supports_decoder("audio/mpeg, mpegversion=(int)1, layer=(int)3")) { printf("MP3 codec not supported\n"); return; } DetectedFile d(SOURCE_DIR "/media/testfile.mp3", "etag", "audio/mpeg", 42, AudioMedia); compare_taglib_gst(d); } TEST_F(ExtractorBackendTest, check_taglib_gst_m4a) { if (!supports_decoder("audio/mpeg, mpegversion=(int)4, stream-format=(string)raw")) { printf("M4A codec not supported\n"); return; } DetectedFile d(SOURCE_DIR "/media/testfile.m4a", "etag", "audio/mp4", 42, AudioMedia); compare_taglib_gst(d); } int main(int argc, char **argv) { gst_init(&argc, &argv); ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } mediascanner2-0.115/test/test_mediastore.cc000066400000000000000000001263131436755250000207310ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace mediascanner; class MediaStoreTest : public ::testing::Test { protected: MediaStoreTest() { } virtual ~MediaStoreTest() { } virtual void SetUp() { } virtual void TearDown() { } }; TEST_F(MediaStoreTest, init) { MediaStore store(":memory:", MS_READ_WRITE); } TEST_F(MediaStoreTest, mediafile_uri) { MediaFile media = MediaFileBuilder("/path/to/file.ogg"); EXPECT_EQ(media.getUri(), "file:///path/to/file.ogg"); } TEST_F(MediaStoreTest, equality) { MediaFile audio1 = MediaFileBuilder("a") .setContentType("type") .setETag("etag") .setDate("1900") .setTitle("b") .setAuthor("c") .setAlbum("d") .setAlbumArtist("e") .setGenre("f") .setDiscNumber(0) .setTrackNumber(1) .setDuration(5) .setType(AudioMedia); MediaFile audio2 = MediaFileBuilder("aa") .setContentType("type") .setETag("etag") .setDate("1900") .setTitle("b") .setAuthor("c") .setAlbum("d") .setAlbumArtist("e") .setGenre("f") .setDiscNumber(0) .setTrackNumber(1) .setDuration(5) .setType(AudioMedia); MediaFile video1 = MediaFileBuilder("a") .setContentType("type") .setETag("etag") .setDate("1900") .setTitle("b") .setDuration(5) .setWidth(41) .setHeight(42) .setType(VideoMedia); MediaFile video2 = MediaFileBuilder("aa") .setContentType("type") .setETag("etag") .setDate("1900") .setTitle("b") .setDuration(5) .setWidth(43) .setHeight(44) .setType(VideoMedia); MediaFile image1 = MediaFileBuilder("a") .setContentType("type") .setETag("etag") .setDate("1900") .setTitle("b") .setWidth(640) .setHeight(480) .setLatitude(20.0) .setLongitude(30.0) .setType(ImageMedia); MediaFile image2 = MediaFileBuilder("aa") .setContentType("type") .setETag("etag") .setDate("1900") .setTitle("b") .setWidth(480) .setHeight(640) .setLatitude(-20.0) .setLongitude(-30.0) .setType(ImageMedia); EXPECT_EQ(audio1, audio1); EXPECT_EQ(video1, video1); EXPECT_EQ(image1, image1); EXPECT_NE(audio1, audio2); EXPECT_NE(video1, video2); EXPECT_NE(image1, image2); EXPECT_NE(audio1, video1); EXPECT_NE(audio1, image1); EXPECT_NE(video1, image1); } TEST_F(MediaStoreTest, lookup) { MediaFile audio = MediaFileBuilder("/aaa") .setContentType("type") .setETag("etag") .setDate("1900-01-01") .setTitle("bbb bbb") .setAuthor("ccc") .setAlbum("ddd") .setAlbumArtist("eee") .setGenre("fff") .setDiscNumber(0) .setTrackNumber(3) .setDuration(5) .setType(AudioMedia); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio); EXPECT_EQ(store.lookup("/aaa"), audio); EXPECT_THROW(store.lookup("not found"), std::runtime_error); } TEST_F(MediaStoreTest, roundtrip) { MediaFile audio = MediaFileBuilder("/aaa") .setContentType("type") .setETag("etag") .setDate("1900-01-01") .setTitle("bbb bbb") .setAuthor("ccc") .setAlbum("ddd") .setAlbumArtist("eee") .setGenre("fff") .setDiscNumber(0) .setTrackNumber(3) .setDuration(5) .setHasThumbnail(true) .setModificationTime(4200) .setType(AudioMedia); MediaFile video = MediaFileBuilder("/aaa2") .setContentType("type") .setETag("etag") .setDate("2012-01-01") .setTitle("bbb bbb") .setDuration(5) .setWidth(1280) .setHeight(720) .setType(VideoMedia); MediaFile image = MediaFileBuilder("/aaa3") .setContentType("type") .setETag("etag") .setDate("2012-01-01") .setTitle("bbb bbb") .setWidth(480) .setHeight(640) .setLatitude(20.0) .setLongitude(-30.0) .setType(ImageMedia); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio); store.insert(video); store.insert(image); Filter filter; vector result = store.query("bbb", AudioMedia, filter); ASSERT_EQ(result.size(), 1); EXPECT_EQ(result[0], audio); result = store.query("bbb", VideoMedia, filter); ASSERT_EQ(result.size(), 1); EXPECT_EQ(result[0], video); result = store.query("bbb", ImageMedia, filter); ASSERT_EQ(1, result.size()); EXPECT_EQ(image, result[0]); } TEST_F(MediaStoreTest, query_by_album) { MediaFile audio = MediaFileBuilder("/path/foo.ogg") .setType(AudioMedia) .setTitle("title") .setAuthor("artist") .setAlbum("album") .setAlbumArtist("albumartist"); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio); Filter filter; vector result = store.query("album", AudioMedia, filter); ASSERT_EQ(result.size(), 1); EXPECT_EQ(result[0], audio); } TEST_F(MediaStoreTest, query_by_artist) { MediaFile audio = MediaFileBuilder("/path/foo.ogg") .setType(AudioMedia) .setTitle("title") .setAuthor("artist") .setAlbum("album") .setAlbumArtist("albumartist"); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio); Filter filter; vector result = store.query("artist", AudioMedia, filter); ASSERT_EQ(result.size(), 1); EXPECT_EQ(result[0], audio); } TEST_F(MediaStoreTest, query_ranking) { MediaFile audio1 = MediaFileBuilder("/path/foo1.ogg") .setType(AudioMedia) .setTitle("title") .setAuthor("artist") .setAlbum("album") .setAlbumArtist("albumartist"); MediaFile audio2 = MediaFileBuilder("/path/foo2.ogg") .setType(AudioMedia) .setTitle("title aaa") .setAuthor("artist") .setAlbum("album") .setAlbumArtist("albumartist"); MediaFile audio3 = MediaFileBuilder("/path/foo3.ogg") .setType(AudioMedia) .setTitle("title") .setAuthor("artist aaa") .setAlbum("album") .setAlbumArtist("albumartist"); MediaFile audio4 = MediaFileBuilder("/path/foo4.ogg") .setType(AudioMedia) .setTitle("title") .setAuthor("artist") .setAlbum("album aaa") .setAlbumArtist("albumartist"); MediaFile audio5 = MediaFileBuilder("/path/foo5.ogg") .setType(AudioMedia) .setTitle("title aaa") .setAuthor("artist aaa") .setAlbum("album aaa") .setAlbumArtist("albumartist"); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio1); store.insert(audio2); store.insert(audio3); store.insert(audio4); store.insert(audio5); Filter filter; vector result = store.query("aaa", AudioMedia, filter); ASSERT_EQ(result.size(), 4); EXPECT_EQ(result[0], audio5); // Term appears in title, artist and album EXPECT_EQ(result[1], audio2); // title has highest weighting EXPECT_EQ(result[2], audio4); // then album EXPECT_EQ(result[3], audio3); // then artist } TEST_F(MediaStoreTest, query_limit) { MediaFile audio1 = MediaFileBuilder("/path/foo5.ogg") .setType(AudioMedia) .setTitle("title aaa") .setAuthor("artist aaa") .setAlbum("album aaa") .setAlbumArtist("albumartist"); MediaFile audio2 = MediaFileBuilder("/path/foo2.ogg") .setType(AudioMedia) .setTitle("title aaa") .setAuthor("artist") .setAlbum("album") .setAlbumArtist("albumartist"); MediaFile audio3 = MediaFileBuilder("/path/foo4.ogg") .setType(AudioMedia) .setTitle("title") .setAuthor("artist") .setAlbum("album aaa") .setAlbumArtist("albumartist"); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio1); store.insert(audio2); store.insert(audio3); Filter filter; filter.setLimit(2); vector result = store.query("aaa", AudioMedia, filter); ASSERT_EQ(result.size(), 2); EXPECT_EQ(result[0], audio1); // Term appears in title, artist and album EXPECT_EQ(result[1], audio2); // title has highest weighting } TEST_F(MediaStoreTest, query_short) { MediaFile audio1 = MediaFileBuilder("/path/foo5.ogg") .setType(AudioMedia) .setTitle("title xyz") .setAuthor("artist") .setAlbum("album") .setAlbumArtist("albumartist"); MediaFile audio2 = MediaFileBuilder("/path/foo2.ogg") .setType(AudioMedia) .setTitle("title xzy") .setAuthor("artist") .setAlbum("album") .setAlbumArtist("albumartist"); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio1); store.insert(audio2); Filter filter; vector result = store.query("x", AudioMedia, filter); EXPECT_EQ(result.size(), 2); result = store.query("xy", AudioMedia, filter); EXPECT_EQ(result.size(), 1); } TEST_F(MediaStoreTest, query_empty) { MediaFile audio1 = MediaFileBuilder("/path/foo5.ogg") .setType(AudioMedia) .setTitle("title aaa") .setAuthor("artist aaa") .setAlbum("album aaa") .setAlbumArtist("albumartist"); MediaFile audio2 = MediaFileBuilder("/path/foo2.ogg") .setType(AudioMedia) .setTitle("title aaa") .setAuthor("artist") .setAlbum("album") .setAlbumArtist("albumartist"); MediaFile audio3 = MediaFileBuilder("/path/foo4.ogg") .setType(AudioMedia) .setTitle("title") .setAuthor("artist") .setAlbum("album aaa") .setAlbumArtist("albumartist"); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio1); store.insert(audio2); store.insert(audio3); // An empty query should return some results Filter filter; filter.setLimit(2); vector result = store.query("", AudioMedia, filter); ASSERT_EQ(result.size(), 2); } TEST_F(MediaStoreTest, query_order) { MediaFile audio1 = MediaFileBuilder("/path/foo1.ogg") .setType(AudioMedia) .setTitle("foo") .setDate("2010-01-01") .setAuthor("artist") .setAlbum("album") .setModificationTime(2); MediaFile audio2 = MediaFileBuilder("/path/foo2.ogg") .setType(AudioMedia) .setTitle("foo foo") .setDate("2010-01-03") .setAuthor("artist") .setAlbum("album") .setModificationTime(1); MediaFile audio3 = MediaFileBuilder("/path/foo3.ogg") .setType(AudioMedia) .setTitle("foo foo foo") .setDate("2010-01-02") .setAuthor("artist") .setAlbum("album") .setModificationTime(3); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio1); store.insert(audio2); store.insert(audio3); Filter filter; // Default sort order is by rank vector result = store.query("foo", AudioMedia, filter); ASSERT_EQ(3, result.size()); EXPECT_EQ("/path/foo3.ogg", result[0].getFileName()); EXPECT_EQ("/path/foo2.ogg", result[1].getFileName()); EXPECT_EQ("/path/foo1.ogg", result[2].getFileName()); // Sorting by rank (same as default) filter.setOrder(MediaOrder::Rank); result = store.query("foo", AudioMedia, filter); ASSERT_EQ(3, result.size()); EXPECT_EQ("/path/foo3.ogg", result[0].getFileName()); EXPECT_EQ("/path/foo2.ogg", result[1].getFileName()); EXPECT_EQ("/path/foo1.ogg", result[2].getFileName()); // Sorting by rank, reversed filter.setReverse(true); result = store.query("foo", AudioMedia, filter); ASSERT_EQ(3, result.size()); EXPECT_EQ("/path/foo1.ogg", result[0].getFileName()); EXPECT_EQ("/path/foo2.ogg", result[1].getFileName()); EXPECT_EQ("/path/foo3.ogg", result[2].getFileName()); // Sorting by title filter.setReverse(false); filter.setOrder(MediaOrder::Title); result = store.query("foo", AudioMedia, filter); ASSERT_EQ(3, result.size()); EXPECT_EQ("/path/foo1.ogg", result[0].getFileName()); EXPECT_EQ("/path/foo2.ogg", result[1].getFileName()); EXPECT_EQ("/path/foo3.ogg", result[2].getFileName()); // Sorting by title, reversed filter.setReverse(true); result = store.query("foo", AudioMedia, filter); ASSERT_EQ(3, result.size()); EXPECT_EQ("/path/foo3.ogg", result[0].getFileName()); EXPECT_EQ("/path/foo2.ogg", result[1].getFileName()); EXPECT_EQ("/path/foo1.ogg", result[2].getFileName()); // Sorting by date filter.setReverse(false); filter.setOrder(MediaOrder::Date); result = store.query("foo", AudioMedia, filter); ASSERT_EQ(3, result.size()); EXPECT_EQ("/path/foo1.ogg", result[0].getFileName()); EXPECT_EQ("/path/foo3.ogg", result[1].getFileName()); EXPECT_EQ("/path/foo2.ogg", result[2].getFileName()); // Sorting by date, reversed filter.setReverse(true); result = store.query("foo", AudioMedia, filter); ASSERT_EQ(3, result.size()); EXPECT_EQ("/path/foo2.ogg", result[0].getFileName()); EXPECT_EQ("/path/foo3.ogg", result[1].getFileName()); EXPECT_EQ("/path/foo1.ogg", result[2].getFileName()); // Sorting by modification date filter.setReverse(false); filter.setOrder(MediaOrder::Modified); result = store.query("foo", AudioMedia, filter); ASSERT_EQ(3, result.size()); EXPECT_EQ("/path/foo2.ogg", result[0].getFileName()); EXPECT_EQ("/path/foo1.ogg", result[1].getFileName()); EXPECT_EQ("/path/foo3.ogg", result[2].getFileName()); // Sorting by modification date, reversed filter.setReverse(true); result = store.query("foo", AudioMedia, filter); ASSERT_EQ(3, result.size()); EXPECT_EQ("/path/foo3.ogg", result[0].getFileName()); EXPECT_EQ("/path/foo1.ogg", result[1].getFileName()); EXPECT_EQ("/path/foo2.ogg", result[2].getFileName()); } TEST_F(MediaStoreTest, unmount) { MediaFile audio1 = MediaFileBuilder("/media/username/dir/fname.ogg") .setType(AudioMedia) .setETag("etag") .setContentType("audio/ogg") .setTitle("bbb bbb") .setDate("2015-01-01") .setAuthor("artist") .setAlbum("album") .setAlbumArtist("album_artist") .setGenre("genre") .setDiscNumber(5) .setTrackNumber(10) .setDuration(42) .setWidth(640) .setHeight(480) .setLatitude(8.0) .setLongitude(4.0) .setHasThumbnail(true) .setModificationTime(10000); MediaFile audio2 = MediaFileBuilder("/home/username/Music/fname.ogg") .setType(AudioMedia) .setTitle("bbb bbb"); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio1); store.insert(audio2); Filter filter; vector result = store.query("bbb", AudioMedia, filter); ASSERT_EQ(result.size(), 2); store.archiveItems("/media/username"); result = store.query("bbb", AudioMedia, filter); ASSERT_EQ(result.size(), 1); EXPECT_EQ(result[0], audio2); store.restoreItems("/media/username"); result = store.query("bbb", AudioMedia, filter); ASSERT_EQ(result.size(), 2); std::sort(result.begin(), result.end(), [](const MediaFile &m1, const MediaFile &m2) -> bool { return m1.getFileName() < m2.getFileName(); }); EXPECT_EQ(result[0], audio2); EXPECT_EQ(result[1], audio1); } TEST_F(MediaStoreTest, utils) { string source("_a.b(c)[d]{e}f.mp3"); string correct = {" a b c d e f"}; string result = filenameToTitle(source); EXPECT_EQ(correct, result); string unquoted(R"(It's a living.)"); string quoted(R"('It''s a living.')"); EXPECT_EQ(sqlQuote(unquoted), quoted); } TEST_F(MediaStoreTest, queryAlbums) { MediaFile audio1 = MediaFileBuilder("/home/username/Music/track1.ogg") .setType(AudioMedia) .setTitle("TitleOne") .setAuthor("ArtistOne") .setAlbum("AlbumOne") .setAlbumArtist("Various Artists") .setDate("2000-01-01") .setDiscNumber(1) .setTrackNumber(1) .setGenre("GenreOne") .setModificationTime(1); MediaFile audio2 = MediaFileBuilder("/home/username/Music/track2.ogg") .setType(AudioMedia) .setTitle("TitleTwo") .setAuthor("ArtistTwo") .setAlbum("AlbumOne") .setAlbumArtist("Various Artists") .setDate("2000-01-01") .setDiscNumber(1) .setTrackNumber(2) .setGenre("GenreOne") .setModificationTime(2); MediaFile audio3 = MediaFileBuilder("/home/username/Music/track3.ogg") .setType(AudioMedia) .setTitle("TitleThree") .setAuthor("ArtistThree") .setAlbum("AlbumOne") .setAlbumArtist("Various Artists") .setDate("2000-01-01") .setDiscNumber(2) .setTrackNumber(1) .setGenre("GenreOne") .setModificationTime(3); MediaFile audio4 = MediaFileBuilder("/home/username/Music/fname.ogg") .setType(AudioMedia) .setTitle("TitleFour") .setAuthor("ArtistFour") .setAlbum("AlbumTwo") .setAlbumArtist("ArtistFour") .setDate("2014-06-01") .setTrackNumber(1) .setGenre("GenreTwo") .setHasThumbnail(true) .setModificationTime(4); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio1); store.insert(audio2); store.insert(audio3); store.insert(audio4); // Query a track title Filter filter; vector albums = store.queryAlbums("TitleOne", filter); ASSERT_EQ(albums.size(), 1); EXPECT_EQ(albums[0].getTitle(), "AlbumOne"); EXPECT_EQ(albums[0].getArtist(), "Various Artists"); EXPECT_EQ(albums[0].getDate(), "2000-01-01"); EXPECT_EQ(albums[0].getGenre(), "GenreOne"); EXPECT_EQ(albums[0].getArtUri(), "image://albumart/artist=Various%20Artists&album=AlbumOne"); // Query an album name albums = store.queryAlbums("AlbumTwo", filter); ASSERT_EQ(albums.size(), 1); EXPECT_EQ(albums[0].getTitle(), "AlbumTwo"); EXPECT_EQ(albums[0].getArtist(), "ArtistFour"); EXPECT_EQ(albums[0].getDate(), "2014-06-01"); EXPECT_EQ(albums[0].getGenre(), "GenreTwo"); EXPECT_EQ(albums[0].getArtUri(), "image://thumbnailer/file:///home/username/Music/fname.ogg"); // Query an artist name albums = store.queryAlbums("ArtistTwo", filter); ASSERT_EQ(albums.size(), 1); EXPECT_EQ(albums[0].getTitle(), "AlbumOne"); EXPECT_EQ(albums[0].getArtist(), "Various Artists"); // Sort results by modification time filter.setOrder(MediaOrder::Modified); filter.setReverse(true); albums = store.queryAlbums("", filter); ASSERT_EQ(2, albums.size()); EXPECT_EQ("AlbumTwo", albums[0].getTitle()); EXPECT_EQ("AlbumOne", albums[1].getTitle()); } TEST_F(MediaStoreTest, queryAlbums_limit) { MediaFile audio1 = MediaFileBuilder("/home/username/Music/track1.ogg") .setType(AudioMedia) .setTitle("TitleOne") .setAuthor("ArtistOne") .setAlbum("AlbumOne") .setAlbumArtist("Various Artists") .setDiscNumber(1) .setTrackNumber(1); MediaFile audio2 = MediaFileBuilder("/home/username/Music/track2.ogg") .setType(AudioMedia) .setTitle("TitleTwo") .setAuthor("ArtistTwo") .setAlbum("AlbumOne") .setAlbumArtist("Various Artists") .setDiscNumber(1) .setTrackNumber(2); MediaFile audio3 = MediaFileBuilder("/home/username/Music/track3.ogg") .setType(AudioMedia) .setTitle("TitleThree") .setAuthor("ArtistThree") .setAlbum("AlbumOne") .setAlbumArtist("Various Artists") .setDiscNumber(2) .setTrackNumber(1); MediaFile audio4 = MediaFileBuilder("/home/username/Music/fname.ogg") .setType(AudioMedia) .setTitle("TitleFour") .setAuthor("ArtistFour") .setAlbum("AlbumTwo") .setAlbumArtist("ArtistFour") .setTrackNumber(1); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio1); store.insert(audio2); store.insert(audio3); store.insert(audio4); Filter filter; vector albums = store.queryAlbums("Artist", filter); EXPECT_EQ(2, albums.size()); filter.setLimit(1); albums = store.queryAlbums("Artist", filter); EXPECT_EQ(1, albums.size()); } TEST_F(MediaStoreTest, queryAlbums_empty) { MediaFile audio1 = MediaFileBuilder("/home/username/Music/track1.ogg") .setType(AudioMedia) .setTitle("TitleOne") .setAuthor("ArtistOne") .setAlbum("AlbumOne") .setAlbumArtist("Various Artists") .setDiscNumber(1) .setTrackNumber(1); MediaFile audio2 = MediaFileBuilder("/home/username/Music/track2.ogg") .setType(AudioMedia) .setTitle("TitleTwo") .setAuthor("ArtistTwo") .setAlbum("AlbumOne") .setAlbumArtist("Various Artists") .setDiscNumber(1) .setTrackNumber(2); MediaFile audio3 = MediaFileBuilder("/home/username/Music/track3.ogg") .setType(AudioMedia) .setTitle("TitleThree") .setAuthor("ArtistThree") .setAlbum("AlbumOne") .setAlbumArtist("Various Artists") .setDiscNumber(2) .setTrackNumber(1); MediaFile audio4 = MediaFileBuilder("/home/username/Music/fname.ogg") .setType(AudioMedia) .setTitle("TitleFour") .setAuthor("ArtistFour") .setAlbum("AlbumTwo") .setAlbumArtist("ArtistFour") .setTrackNumber(1); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio1); store.insert(audio2); store.insert(audio3); store.insert(audio4); Filter filter; vector albums = store.queryAlbums("", filter); EXPECT_EQ(2, albums.size()); filter.setLimit(1); albums = store.queryAlbums("", filter); EXPECT_EQ(1, albums.size()); } TEST_F(MediaStoreTest, queryAlbums_order) { MediaFile audio1 = MediaFileBuilder("/path/foo1.ogg") .setType(AudioMedia) .setTitle("title") .setDate("2010-01-01") .setAuthor("artist") .setAlbum("foo"); MediaFile audio2 = MediaFileBuilder("/path/foo2.ogg") .setType(AudioMedia) .setTitle("title") .setDate("2010-01-03") .setAuthor("artist") .setAlbum("foo foo"); MediaFile audio3 = MediaFileBuilder("/path/foo3.ogg") .setType(AudioMedia) .setTitle("title") .setDate("2010-01-02") .setAuthor("artist") .setAlbum("foo foo foo"); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio1); store.insert(audio2); store.insert(audio3); // Default sort Filter filter; vector albums = store.queryAlbums("foo", filter); ASSERT_EQ(3, albums.size()); EXPECT_EQ("foo", albums[0].getTitle()); EXPECT_EQ("foo foo", albums[1].getTitle()); EXPECT_EQ("foo foo foo", albums[2].getTitle()); // Sort by title (same as default) filter.setOrder(MediaOrder::Title); albums = store.queryAlbums("foo", filter); ASSERT_EQ(3, albums.size()); EXPECT_EQ("foo", albums[0].getTitle()); EXPECT_EQ("foo foo", albums[1].getTitle()); EXPECT_EQ("foo foo foo", albums[2].getTitle()); // Sort by title, reversed filter.setReverse(true); albums = store.queryAlbums("foo", filter); ASSERT_EQ(3, albums.size()); EXPECT_EQ("foo foo foo", albums[0].getTitle()); EXPECT_EQ("foo foo", albums[1].getTitle()); EXPECT_EQ("foo", albums[2].getTitle()); // Other orders are not supported filter.setOrder(MediaOrder::Rank); EXPECT_THROW(store.queryAlbums("foo", filter), std::runtime_error); filter.setOrder(MediaOrder::Date); EXPECT_THROW(store.queryAlbums("foo", filter), std::runtime_error); } TEST_F(MediaStoreTest, queryArtists) { MediaFile audio1 = MediaFileBuilder("/home/username/Music/track1.ogg") .setType(AudioMedia) .setTitle("TitleOne") .setAuthor("ArtistOne") .setAlbum("AlbumOne") .setAlbumArtist("Various Artists") .setDiscNumber(1) .setTrackNumber(1); MediaFile audio2 = MediaFileBuilder("/home/username/Music/track2.ogg") .setType(AudioMedia) .setTitle("TitleTwo") .setAuthor("ArtistTwo") .setAlbum("AlbumOne") .setAlbumArtist("Various Artists") .setDiscNumber(1) .setTrackNumber(2); MediaFile audio3 = MediaFileBuilder("/home/username/Music/track3.ogg") .setType(AudioMedia) .setTitle("TitleThree") .setAuthor("ArtistThree") .setAlbum("AlbumOne") .setAlbumArtist("Various Artists") .setDiscNumber(2) .setTrackNumber(1); MediaFile audio4 = MediaFileBuilder("/home/username/Music/fname.ogg") .setType(AudioMedia) .setTitle("TitleFour") .setAuthor("ArtistFour") .setAlbum("AlbumTwo") .setAlbumArtist("ArtistFour") .setTrackNumber(1); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio1); store.insert(audio2); store.insert(audio3); store.insert(audio4); // Query a track title Filter filter; vector artists = store.queryArtists("TitleOne", filter); ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists[0], "ArtistOne"); // Query an album name artists = store.queryArtists("AlbumTwo", filter); ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists[0], "ArtistFour"); // Query an artist name artists = store.queryArtists("ArtistTwo", filter); ASSERT_EQ(artists.size(), 1); EXPECT_EQ(artists[0], "ArtistTwo"); } TEST_F(MediaStoreTest, queryArtists_limit) { MediaFile audio1 = MediaFileBuilder("/home/username/Music/track1.ogg") .setType(AudioMedia) .setTitle("TitleOne") .setAuthor("ArtistOne") .setAlbum("AlbumOne") .setAlbumArtist("Various Artists") .setDiscNumber(1) .setTrackNumber(1); MediaFile audio2 = MediaFileBuilder("/home/username/Music/track2.ogg") .setType(AudioMedia) .setTitle("TitleTwo") .setAuthor("ArtistTwo") .setAlbum("AlbumOne") .setAlbumArtist("Various Artists") .setDiscNumber(1) .setTrackNumber(2); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio1); store.insert(audio2); Filter filter; vector artists = store.queryArtists("Artist", filter); EXPECT_EQ(2, artists.size()); filter.setLimit(1); artists = store.queryArtists("Artist", filter); EXPECT_EQ(1, artists.size()); } TEST_F(MediaStoreTest, queryArtists_empty) { MediaFile audio1 = MediaFileBuilder("/home/username/Music/track1.ogg") .setType(AudioMedia) .setTitle("TitleOne") .setAuthor("ArtistOne") .setAlbum("AlbumOne") .setAlbumArtist("Various Artists") .setDiscNumber(1) .setTrackNumber(1); MediaFile audio2 = MediaFileBuilder("/home/username/Music/track2.ogg") .setType(AudioMedia) .setTitle("TitleTwo") .setAuthor("ArtistTwo") .setAlbum("AlbumOne") .setAlbumArtist("Various Artists") .setDiscNumber(1) .setTrackNumber(2); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio1); store.insert(audio2); Filter filter; vector artists = store.queryArtists("", filter); EXPECT_EQ(2, artists.size()); filter.setLimit(1); artists = store.queryArtists("", filter); EXPECT_EQ(1, artists.size()); } TEST_F(MediaStoreTest, queryArtists_order) { MediaFile audio1 = MediaFileBuilder("/path/foo1.ogg") .setType(AudioMedia) .setTitle("title") .setDate("2010-01-01") .setAuthor("foo") .setAlbum("album"); MediaFile audio2 = MediaFileBuilder("/path/foo2.ogg") .setType(AudioMedia) .setTitle("title") .setDate("2010-01-03") .setAuthor("foo foo") .setAlbum("album"); MediaFile audio3 = MediaFileBuilder("/path/foo3.ogg") .setType(AudioMedia) .setTitle("title") .setDate("2010-01-02") .setAuthor("foo foo foo") .setAlbum("album"); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio1); store.insert(audio2); store.insert(audio3); // Default sort Filter filter; vector artists = store.queryArtists("foo", filter); ASSERT_EQ(3, artists.size()); EXPECT_EQ("foo", artists[0]); EXPECT_EQ("foo foo", artists[1]); EXPECT_EQ("foo foo foo", artists[2]); // Sort by title (same as default) filter.setOrder(MediaOrder::Title); artists = store.queryArtists("foo", filter); ASSERT_EQ(3, artists.size()); EXPECT_EQ("foo", artists[0]); EXPECT_EQ("foo foo", artists[1]); EXPECT_EQ("foo foo foo", artists[2]); // Sort by title, reversed filter.setReverse(true); artists = store.queryArtists("foo", filter); ASSERT_EQ(3, artists.size()); EXPECT_EQ("foo foo foo", artists[0]); EXPECT_EQ("foo foo", artists[1]); EXPECT_EQ("foo", artists[2]); // Other orders are not supported filter.setOrder(MediaOrder::Rank); EXPECT_THROW(store.queryArtists("foo", filter), std::runtime_error); filter.setOrder(MediaOrder::Date); EXPECT_THROW(store.queryArtists("foo", filter), std::runtime_error); } TEST_F(MediaStoreTest, getAlbumSongs) { MediaFile audio1 = MediaFileBuilder("/home/username/Music/track1.ogg") .setType(AudioMedia) .setTitle("TitleOne") .setAuthor("ArtistOne") .setAlbum("AlbumOne") .setAlbumArtist("Various Artists") .setDiscNumber(1) .setTrackNumber(1); MediaFile audio2 = MediaFileBuilder("/home/username/Music/track2.ogg") .setType(AudioMedia) .setTitle("TitleTwo") .setAuthor("ArtistTwo") .setAlbum("AlbumOne") .setAlbumArtist("Various Artists") .setDiscNumber(1) .setTrackNumber(2); MediaFile audio3 = MediaFileBuilder("/home/username/Music/track3.ogg") .setType(AudioMedia) .setTitle("TitleThree") .setAuthor("ArtistThree") .setAlbum("AlbumOne") .setAlbumArtist("Various Artists") .setDiscNumber(2) .setTrackNumber(1); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio1); store.insert(audio2); store.insert(audio3); vector tracks = store.getAlbumSongs( Album("AlbumOne", "Various Artists")); ASSERT_EQ(tracks.size(), 3); EXPECT_EQ(tracks[0].getTitle(), "TitleOne"); EXPECT_EQ(tracks[1].getTitle(), "TitleTwo"); EXPECT_EQ(tracks[2].getTitle(), "TitleThree"); } TEST_F(MediaStoreTest, getETag) { MediaFile file = MediaFileBuilder("/path/file.ogg") .setETag("etag") .setType(AudioMedia); MediaStore store(":memory:", MS_READ_WRITE); store.insert(file); EXPECT_EQ(store.getETag("/path/file.ogg"), "etag"); EXPECT_EQ(store.getETag("/something-else.mp3"), ""); } TEST_F(MediaStoreTest, constraints) { MediaFile file = MediaFileBuilder("no_slash_at_beginning.ogg") .setETag("etag") .setType(AudioMedia); MediaFile file2 = MediaFileBuilder("/invalid_type.ogg") .setETag("etag"); MediaStore store(":memory:", MS_READ_WRITE); ASSERT_THROW(store.insert(file), std::runtime_error); ASSERT_THROW(store.insert(file2), std::runtime_error); } TEST_F(MediaStoreTest, listSongs) { MediaFile audio1 = MediaFileBuilder("/home/username/Music/track1.ogg") .setType(AudioMedia) .setTitle("TitleOne") .setAuthor("ArtistOne") .setAlbum("AlbumOne") .setTrackNumber(1); MediaFile audio2 = MediaFileBuilder("/home/username/Music/track2.ogg") .setType(AudioMedia) .setTitle("TitleTwo") .setAuthor("ArtistOne") .setAlbum("AlbumOne") .setTrackNumber(2); MediaFile audio3 = MediaFileBuilder("/home/username/Music/track3.ogg") .setType(AudioMedia) .setTitle("TitleThree") .setAuthor("ArtistOne") .setAlbum("AlbumTwo"); MediaFile audio4 = MediaFileBuilder("/home/username/Music/track4.ogg") .setType(AudioMedia) .setTitle("TitleFour") .setAuthor("ArtistTwo") .setAlbum("AlbumThree"); MediaFile audio5 = MediaFileBuilder("/home/username/Music/track5.ogg") .setType(AudioMedia) .setTitle("TitleFive") .setAuthor("ArtistOne") .setAlbum("AlbumFour") .setAlbumArtist("Various Artists") .setTrackNumber(1); MediaFile audio6 = MediaFileBuilder("/home/username/Music/track6.ogg") .setType(AudioMedia) .setTitle("TitleSix") .setAuthor("ArtistTwo") .setAlbum("AlbumFour") .setAlbumArtist("Various Artists") .setTrackNumber(2); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio1); store.insert(audio2); store.insert(audio3); store.insert(audio4); store.insert(audio5); store.insert(audio6); Filter filter; vector tracks = store.listSongs(filter); ASSERT_EQ(6, tracks.size()); EXPECT_EQ("TitleOne", tracks[0].getTitle()); // Apply a limit filter.setLimit(4); tracks = store.listSongs(filter); EXPECT_EQ(4, tracks.size()); filter.setLimit(-1); // List songs by artist filter.setArtist("ArtistOne"); tracks = store.listSongs(filter); EXPECT_EQ(4, tracks.size()); // List songs by album filter.clear(); filter.setAlbum("AlbumOne"); tracks = store.listSongs(filter); EXPECT_EQ(2, tracks.size()); // List songs by album artist filter.clear(); filter.setAlbumArtist("Various Artists"); tracks = store.listSongs(filter); EXPECT_EQ(2, tracks.size()); // Combinations filter.clear(); filter.setArtist("ArtistOne"); filter.setAlbum("AlbumOne"); tracks = store.listSongs(filter); EXPECT_EQ(2, tracks.size()); filter.clear(); filter.setAlbum("AlbumOne"); filter.setAlbumArtist("ArtistOne"); tracks = store.listSongs(filter); EXPECT_EQ(2, tracks.size()); filter.clear(); filter.setArtist("ArtistOne"); filter.setAlbum("AlbumOne"); filter.setAlbumArtist("ArtistOne"); tracks = store.listSongs(filter); EXPECT_EQ(2, tracks.size()); filter.clear(); filter.setArtist("ArtistOne"); filter.setAlbumArtist("ArtistOne"); tracks = store.listSongs(filter); EXPECT_EQ(3, tracks.size()); } TEST_F(MediaStoreTest, listAlbums) { MediaFile audio1 = MediaFileBuilder("/home/username/Music/track1.ogg") .setType(AudioMedia) .setTitle("TitleOne") .setAuthor("ArtistOne") .setAlbum("AlbumOne") .setTrackNumber(1); MediaFile audio2 = MediaFileBuilder("/home/username/Music/track3.ogg") .setType(AudioMedia) .setTitle("TitleThree") .setAuthor("ArtistOne") .setAlbum("AlbumTwo"); MediaFile audio3 = MediaFileBuilder("/home/username/Music/track4.ogg") .setType(AudioMedia) .setTitle("TitleFour") .setAuthor("ArtistTwo") .setAlbum("AlbumThree"); MediaFile audio4 = MediaFileBuilder("/home/username/Music/track5.ogg") .setType(AudioMedia) .setTitle("TitleFive") .setAuthor("ArtistOne") .setAlbum("AlbumFour") .setAlbumArtist("Various Artists") .setTrackNumber(1); MediaFile audio5 = MediaFileBuilder("/home/username/Music/track6.ogg") .setType(AudioMedia) .setTitle("TitleSix") .setAuthor("ArtistTwo") .setAlbum("AlbumFour") .setAlbumArtist("Various Artists") .setTrackNumber(2); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio1); store.insert(audio2); store.insert(audio3); store.insert(audio4); store.insert(audio5); Filter filter; vector albums = store.listAlbums(filter); ASSERT_EQ(4, albums.size()); EXPECT_EQ("AlbumFour", albums[0].getTitle()); // test limit filter.setLimit(2); albums = store.listAlbums(filter); EXPECT_EQ(2, albums.size()); filter.setLimit(-1); // Songs by artist filter.setArtist("ArtistOne"); albums = store.listAlbums(filter); EXPECT_EQ(3, albums.size()); // Songs by album artist filter.clear(); filter.setAlbumArtist("ArtistOne"); albums = store.listAlbums(filter); EXPECT_EQ(2, albums.size()); // Combination filter.clear(); filter.setArtist("ArtistOne"); filter.setAlbumArtist("Various Artists"); albums = store.listAlbums(filter); EXPECT_EQ(1, albums.size()); } TEST_F(MediaStoreTest, listArtists) { MediaFile audio1 = MediaFileBuilder("/home/username/Music/track1.ogg") .setType(AudioMedia) .setTitle("TitleOne") .setAuthor("ArtistOne") .setAlbum("AlbumOne") .setTrackNumber(1); MediaFile audio2 = MediaFileBuilder("/home/username/Music/track3.ogg") .setType(AudioMedia) .setTitle("TitleThree") .setAuthor("ArtistOne") .setAlbum("AlbumTwo"); MediaFile audio3 = MediaFileBuilder("/home/username/Music/track4.ogg") .setType(AudioMedia) .setTitle("TitleFour") .setAuthor("ArtistTwo") .setAlbum("AlbumThree"); MediaFile audio4 = MediaFileBuilder("/home/username/Music/track5.ogg") .setType(AudioMedia) .setTitle("TitleFive") .setAuthor("ArtistOne") .setAlbum("AlbumFour") .setAlbumArtist("Various Artists") .setTrackNumber(1); MediaFile audio5 = MediaFileBuilder("/home/username/Music/track6.ogg") .setType(AudioMedia) .setTitle("TitleSix") .setAuthor("ArtistTwo") .setAlbum("AlbumFour") .setAlbumArtist("Various Artists") .setTrackNumber(2); MediaStore store(":memory:", MS_READ_WRITE); store.insert(audio1); store.insert(audio2); store.insert(audio3); store.insert(audio4); store.insert(audio5); Filter filter; vector artists = store.listArtists(filter); ASSERT_EQ(2, artists.size()); EXPECT_EQ("ArtistOne", artists[0]); EXPECT_EQ("ArtistTwo", artists[1]); // Test limit clause filter.setLimit(1); artists = store.listArtists(filter); EXPECT_EQ(1, artists.size()); filter.setLimit(-1); // List "album artists" artists = store.listAlbumArtists(filter); ASSERT_EQ(3, artists.size()); EXPECT_EQ("ArtistOne", artists[0]); EXPECT_EQ("ArtistTwo", artists[1]); EXPECT_EQ("Various Artists", artists[2]); } TEST_F(MediaStoreTest, hasMedia) { MediaStore store(":memory:", MS_READ_WRITE); EXPECT_FALSE(store.hasMedia(AudioMedia)); EXPECT_FALSE(store.hasMedia(VideoMedia)); EXPECT_FALSE(store.hasMedia(ImageMedia)); EXPECT_FALSE(store.hasMedia(AllMedia)); MediaFile audio = MediaFileBuilder("/home/username/Music/track1.ogg") .setType(AudioMedia) .setTitle("Title") .setAuthor("Artist") .setAlbum("Album"); store.insert(audio); EXPECT_TRUE(store.hasMedia(AudioMedia)); EXPECT_FALSE(store.hasMedia(VideoMedia)); EXPECT_FALSE(store.hasMedia(ImageMedia)); EXPECT_TRUE(store.hasMedia(AllMedia)); } TEST_F(MediaStoreTest, brokenFiles) { MediaStore store(":memory:", MS_READ_WRITE); std::string file = "/foo/bar/baz.mp3"; std::string other_file = "/foo/bar/abc.mp3"; std::string broken_etag = "123"; std::string ok_etag = "124"; ASSERT_FALSE(store.is_broken_file(file, broken_etag)); ASSERT_FALSE(store.is_broken_file(file, ok_etag)); ASSERT_FALSE(store.is_broken_file(other_file, ok_etag)); ASSERT_FALSE(store.is_broken_file(other_file, broken_etag)); store.insert_broken_file(file, broken_etag); ASSERT_TRUE(store.is_broken_file(file, broken_etag)); ASSERT_FALSE(store.is_broken_file(file, ok_etag)); ASSERT_FALSE(store.is_broken_file(other_file, ok_etag)); ASSERT_FALSE(store.is_broken_file(other_file, broken_etag)); store.remove_broken_file(file); ASSERT_FALSE(store.is_broken_file(file, broken_etag)); ASSERT_FALSE(store.is_broken_file(file, ok_etag)); ASSERT_FALSE(store.is_broken_file(other_file, ok_etag)); ASSERT_FALSE(store.is_broken_file(other_file, broken_etag)); } TEST_F(MediaStoreTest, removeSubtree) { MediaStore store(":memory:", MS_READ_WRITE); // Include some SQL like expression meta characters in the path store.insert(MediaFileBuilder("/hello_%/world.mp3").setType(AudioMedia)); store.insert(MediaFileBuilder("/hello_%/a/b/c/world.mp3").setType(AudioMedia)); store.insert(MediaFileBuilder("/hello_%sibling.mp3").setType(AudioMedia)); store.insert(MediaFileBuilder("/helloxyz.mp3").setType(AudioMedia)); EXPECT_EQ(4, store.size()); store.removeSubtree("/hello_%"); EXPECT_EQ(2, store.size()); store.lookup("/hello_%sibling.mp3"); store.lookup("/helloxyz.mp3"); try { store.lookup("/hello_%/world.mp3"); FAIL(); } catch (const std::runtime_error &e) { string msg = e.what(); EXPECT_NE(string::npos, msg.find("Could not find media")); } try { store.lookup("/hello_%/a/b/c/world.mp3"); FAIL(); } catch (const std::runtime_error &e) { string msg = e.what(); EXPECT_NE(string::npos, msg.find("Could not find media")); } } TEST_F(MediaStoreTest, transaction) { MediaStore store(":memory:", MS_READ_WRITE); // Run a transaction without committing: file not added. { MediaStoreTransaction txn = store.beginTransaction(); store.insert(MediaFileBuilder("/one.mp3").setType(AudioMedia)); } EXPECT_EQ(0, store.size()); EXPECT_THROW(store.lookup("/one.mp3"), std::runtime_error); // Commit two files in a transaction, then one file in a second // transaction, and leave the last uncommitted. { MediaStoreTransaction txn = store.beginTransaction(); store.insert(MediaFileBuilder("/one.mp3").setType(AudioMedia)); store.insert(MediaFileBuilder("/two.mp3").setType(AudioMedia)); txn.commit(); store.insert(MediaFileBuilder("/three.mp3").setType(AudioMedia)); txn.commit(); store.insert(MediaFileBuilder("/four.mp3").setType(AudioMedia)); } EXPECT_EQ(3, store.size()); store.lookup("/one.mp3"); store.lookup("/two.mp3"); store.lookup("/three.mp3"); EXPECT_THROW(store.lookup("/four.mp3"), std::runtime_error); } int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } mediascanner2-0.115/test/test_metadataextractor.cc000066400000000000000000000256631436755250000223170ustar00rootroot00000000000000/* * Copyright (C) 2013-2014 Canonical, Ltd. * * Authors: * Jussi Pakkanen * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include "test_config.h" #include #include #include #include #include #include #include #include #include using namespace std; using namespace mediascanner; class MetadataExtractorTest : public ::testing::Test { protected: virtual void SetUp() override { test_dbus_.reset(g_test_dbus_new(G_TEST_DBUS_NONE)); g_test_dbus_add_service_dir(test_dbus_.get(), TEST_DIR "/services"); } virtual void TearDown() override { session_bus_.reset(); test_dbus_.reset(); unsetenv("MEDIASCANNER_EXTRACTOR_CRASH_AFTER"); } GDBusConnection *session_bus() { if (!bus_started_) { g_test_dbus_up(test_dbus_.get()); GError *error = nullptr; char *address = g_dbus_address_get_for_bus_sync(G_BUS_TYPE_SESSION, nullptr, &error); if (!address) { std::string errortxt(error->message); g_error_free(error); throw std::runtime_error( std::string("Failed to determine session bus address: ") + errortxt); } session_bus_.reset(g_dbus_connection_new_for_address_sync( address, static_cast(G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION), nullptr, nullptr, &error)); g_free(address); if (!session_bus_) { std::string errortxt(error->message); g_error_free(error); throw std::runtime_error( std::string("Failed to connect to session bus: ") + errortxt); } bus_started_ = true; } return session_bus_.get(); } private: unique_ptr test_dbus_ {nullptr, g_object_unref}; unique_ptr session_bus_ {nullptr, g_object_unref}; bool bus_started_ = false; }; TEST_F(MetadataExtractorTest, init) { MetadataExtractor extractor(session_bus()); } TEST_F(MetadataExtractorTest, detect_audio) { MetadataExtractor e(session_bus()); string testfile = SOURCE_DIR "/media/testfile.ogg"; DetectedFile d = e.detect(testfile); EXPECT_NE(d.etag, ""); EXPECT_EQ(d.content_type, "audio/ogg"); EXPECT_EQ(d.type, AudioMedia); struct stat st; ASSERT_EQ(0, stat(testfile.c_str(), &st)); EXPECT_EQ(st.st_mtime, d.mtime); } TEST_F(MetadataExtractorTest, detect_video) { MetadataExtractor e(session_bus()); string testfile = SOURCE_DIR "/media/testvideo_480p.ogv"; DetectedFile d = e.detect(testfile); EXPECT_NE(d.etag, ""); EXPECT_EQ(d.content_type, "video/ogg"); EXPECT_EQ(d.type, VideoMedia); struct stat st; ASSERT_EQ(0, stat(testfile.c_str(), &st)); EXPECT_EQ(st.st_mtime, d.mtime); } TEST_F(MetadataExtractorTest, detect_notmedia) { MetadataExtractor e(session_bus()); string testfile = SOURCE_DIR "/CMakeLists.txt"; EXPECT_THROW(e.detect(testfile), runtime_error); } TEST_F(MetadataExtractorTest, extract) { MetadataExtractor e(session_bus()); string testfile = SOURCE_DIR "/media/testfile.ogg"; MediaFile file = e.extract(e.detect(testfile)); EXPECT_EQ(file.getType(), AudioMedia); EXPECT_EQ(file.getTitle(), "track1"); EXPECT_EQ(file.getAuthor(), "artist1"); EXPECT_EQ(file.getAlbum(), "album1"); EXPECT_EQ(file.getDate(), "2013"); EXPECT_EQ(file.getTrackNumber(), 1); EXPECT_EQ(file.getDuration(), 5); EXPECT_EQ(file.getHasThumbnail(), false); } TEST_F(MetadataExtractorTest, extract_vorbis_art) { MetadataExtractor e(session_bus()); string testfile = SOURCE_DIR "/media/embedded-art.ogg"; MediaFile file = e.extract(e.detect(testfile)); EXPECT_EQ(file.getHasThumbnail(), true); } TEST_F(MetadataExtractorTest, extract_mp3) { MetadataExtractor e(session_bus()); string testfile = SOURCE_DIR "/media/testfile.mp3"; MediaFile file = e.extract(e.detect(testfile)); EXPECT_EQ(file.getType(), AudioMedia); EXPECT_EQ(file.getTitle(), "track1"); EXPECT_EQ(file.getAuthor(), "artist1"); EXPECT_EQ(file.getAlbum(), "album1"); EXPECT_EQ(file.getDate(), "2013-06-03"); EXPECT_EQ(file.getTrackNumber(), 1); EXPECT_EQ(file.getDuration(), 1); EXPECT_EQ(file.getGenre(), "Hip-Hop"); struct stat st; ASSERT_EQ(0, stat(testfile.c_str(), &st)); EXPECT_EQ(st.st_mtime, file.getModificationTime()); } TEST_F(MetadataExtractorTest, extract_m4a) { MetadataExtractor e(session_bus()); string testfile = SOURCE_DIR "/media/testfile.m4a"; MediaFile file = e.extract(e.detect(testfile)); EXPECT_EQ(AudioMedia, file.getType()); EXPECT_EQ("Title", file.getTitle()); EXPECT_EQ("Artist", file.getAuthor()); EXPECT_EQ("Album", file.getAlbum()); EXPECT_EQ("Album Artist", file.getAlbumArtist()); EXPECT_EQ("2015-10-07", file.getDate()); EXPECT_EQ(4, file.getTrackNumber()); EXPECT_EQ(1, file.getDiscNumber()); EXPECT_EQ(1, file.getDuration()); EXPECT_EQ("Rock", file.getGenre()); EXPECT_EQ(true, file.getHasThumbnail()); struct stat st; ASSERT_EQ(0, stat(testfile.c_str(), &st)); EXPECT_EQ(st.st_mtime, file.getModificationTime()); } TEST_F(MetadataExtractorTest, extract_video) { MetadataExtractor e(session_bus()); MediaFile file = e.extract(e.detect(SOURCE_DIR "/media/testvideo_480p.ogv")); EXPECT_EQ(file.getType(), VideoMedia); EXPECT_EQ(file.getDuration(), 1); EXPECT_EQ(file.getWidth(), 854); EXPECT_EQ(file.getHeight(), 480); file = e.extract(e.detect(SOURCE_DIR "/media/testvideo_720p.ogv")); EXPECT_EQ(file.getType(), VideoMedia); EXPECT_EQ(file.getDuration(), 1); EXPECT_EQ(file.getWidth(), 1280); EXPECT_EQ(file.getHeight(), 720); file = e.extract(e.detect(SOURCE_DIR "/media/testvideo_1080p.ogv")); EXPECT_EQ(file.getType(), VideoMedia); EXPECT_EQ(file.getDuration(), 1); EXPECT_EQ(file.getWidth(), 1920); EXPECT_EQ(file.getHeight(), 1080); } TEST_F(MetadataExtractorTest, extract_photo) { MetadataExtractor e(session_bus()); // An landscape image that should be rotated to portrait MediaFile file = e.extract(e.detect(SOURCE_DIR "/media/image1.jpg")); EXPECT_EQ(ImageMedia, file.getType()); EXPECT_EQ(2848, file.getWidth()); EXPECT_EQ(4272, file.getHeight()); EXPECT_EQ("2013-01-04T08:25:46", file.getDate()); EXPECT_DOUBLE_EQ(-28.249409333333336, file.getLatitude()); EXPECT_DOUBLE_EQ(153.150774, file.getLongitude()); // A landscape image without rotation. file = e.extract(e.detect(SOURCE_DIR "/media/image2.jpg")); EXPECT_EQ(ImageMedia, file.getType()); EXPECT_EQ(4272, file.getWidth()); EXPECT_EQ(2848, file.getHeight()); EXPECT_EQ("2013-01-04T09:52:27", file.getDate()); EXPECT_DOUBLE_EQ(-28.259611, file.getLatitude()); EXPECT_DOUBLE_EQ(153.1727346, file.getLongitude()); } TEST_F(MetadataExtractorTest, extract_bad_date) { MetadataExtractor e(session_bus()); string testfile = SOURCE_DIR "/media/baddate.ogg"; MediaFile file = e.extract(e.detect(testfile)); EXPECT_EQ(file.getType(), AudioMedia); EXPECT_EQ(file.getTitle(), "Track"); EXPECT_EQ(file.getAuthor(), "Artist"); EXPECT_EQ(file.getAlbum(), "Album"); EXPECT_EQ(file.getDate(), ""); } TEST_F(MetadataExtractorTest, extract_mp3_bad_date) { MetadataExtractor e(session_bus()); string testfile = SOURCE_DIR "/media/baddate.mp3"; MediaFile file = e.extract(e.detect(testfile)); EXPECT_EQ(file.getType(), AudioMedia); EXPECT_EQ(file.getTitle(), "Track"); EXPECT_EQ(file.getAuthor(), "Artist"); EXPECT_EQ(file.getAlbum(), "Album"); EXPECT_EQ(file.getDate(), ""); } TEST_F(MetadataExtractorTest, blacklist) { MetadataExtractor e(session_bus()); string testfile = SOURCE_DIR "/media/playlist.m3u"; try { e.detect(testfile); FAIL(); } catch(const std::runtime_error &e) { std::string error_message(e.what()); ASSERT_NE(error_message.find("blacklist"), std::string::npos); } } TEST_F(MetadataExtractorTest, png_file) { // PNG files don't have exif entries, so test we work with those, too. MetadataExtractor e(session_bus()); MediaFile file = e.extract(e.detect(SOURCE_DIR "/media/image3.png")); EXPECT_EQ(ImageMedia, file.getType()); EXPECT_EQ(640, file.getWidth()); EXPECT_EQ(400, file.getHeight()); // The time stamp on the test file can be anything. We can't guarantee what it is, // so just inspect the format. auto timestr = file.getDate(); EXPECT_EQ(timestr.size(), 19); EXPECT_EQ(timestr.find('T'), 10); // These can't go inside EXPECT_EQ because it is a macro and mixing templates // with macros makes things explode. auto dashes = std::count_if(timestr.begin(), timestr.end(), [](char c) { return c == '-';}); auto colons = std::count_if(timestr.begin(), timestr.end(), [](char c) { return c == ':';}); EXPECT_EQ(dashes, 2); EXPECT_EQ(colons, 2); EXPECT_DOUBLE_EQ(0, file.getLatitude()); EXPECT_DOUBLE_EQ(0, file.getLongitude()); } TEST_F(MetadataExtractorTest, extractor_crash) { setenv("MEDIASCANNER_EXTRACTOR_CRASH_AFTER", "0", true); MetadataExtractor e(session_bus()); string testfile = SOURCE_DIR "/media/testfile.ogg"; try { MediaFile file = e.extract(e.detect(testfile)); FAIL(); } catch (const std::runtime_error &e) { EXPECT_NE(std::string::npos, std::string(e.what()).find("ExtractMetadata D-Bus call failed")) << e.what(); } } TEST_F(MetadataExtractorTest, crash_recovery) { setenv("MEDIASCANNER_EXTRACTOR_CRASH_AFTER", "1", true); MetadataExtractor e(session_bus()); string testfile = SOURCE_DIR "/media/testfile.ogg"; // First extraction succeeds MediaFile file = e.extract(e.detect(testfile)); // Second try succeeds, with the extraction daemon being // restarted. file = e.extract(e.detect(testfile)); EXPECT_EQ("track1", file.getTitle()); } int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } mediascanner2-0.115/test/test_mfbuilder.cc000066400000000000000000000272411436755250000205460ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "test_config.h" #include "mediascanner/Album.hh" #include "mediascanner/MediaFile.hh" #include "mediascanner/MediaFileBuilder.hh" #include #include #include #include #include #include #include #include #include using namespace mediascanner; class MFBTest : public ::testing::Test { protected: MFBTest() = default; virtual ~MFBTest() = default; virtual void SetUp() override { tmpdir = TEST_DIR "/mfbuilder-test.XXXXXX"; ASSERT_NE(nullptr, mkdtemp(&tmpdir[0])); } virtual void TearDown() override { if (!tmpdir.empty()) { std::string cmd = "rm -rf " + tmpdir; ASSERT_EQ(0, system(cmd.c_str())); } } void touch(const std::string &fname, bool sleep=false) { if (sleep) { // Ensure time stamps change std::this_thread::sleep_for(std::chrono::milliseconds(50)); } int fd = open(fname.c_str(), O_CREAT, 0600); ASSERT_GT(fd, 0); ASSERT_EQ(0, close(fd)); } void remove(const std::string &fname, bool sleep=false) { if (sleep) { // Ensure time stamps change std::this_thread::sleep_for(std::chrono::milliseconds(50)); } ASSERT_EQ(0, unlink(fname.c_str())); } std::string tmpdir; }; TEST_F(MFBTest, basic) { MediaType type(AudioMedia); std::string fname("abc"); std::string title("def"); std::string date("ghi"); std::string author("jkl"); std::string album("mno"); std::string album_artist("pqr"); std::string etag("stu"); std::string content_type("vwx"); std::string genre("yz"); int disc_number = 2; int track_number = 13; int duration = 99; int width = 640; int height = 480; double latitude = 67.2; double longitude = -7.5; uint64_t mtime = 42; MediaFileBuilder b(fname); b.setType(type); b.setTitle(title); b.setDate(date); b.setAuthor(author); b.setAlbum(album); b.setAlbumArtist(album_artist); b.setGenre(genre); b.setDiscNumber(disc_number); b.setTrackNumber(track_number); b.setDuration(duration); b.setETag(etag); b.setContentType(content_type); b.setWidth(width); b.setHeight(height); b.setLatitude(latitude); b.setLongitude(longitude); b.setModificationTime(mtime); // Now see if data survives a round trip. MediaFile mf = b.build(); EXPECT_EQ(mf.getType(), type); EXPECT_EQ(mf.getFileName(), fname); EXPECT_EQ(mf.getTitle(), title); EXPECT_EQ(mf.getDate(), date); EXPECT_EQ(mf.getAuthor(), author); EXPECT_EQ(mf.getAlbum(), album); EXPECT_EQ(mf.getAlbumArtist(), album_artist); EXPECT_EQ(mf.getGenre(), genre); EXPECT_EQ(mf.getDiscNumber(), disc_number); EXPECT_EQ(mf.getTrackNumber(), track_number); EXPECT_EQ(mf.getDuration(), duration); EXPECT_EQ(mf.getETag(), etag); EXPECT_EQ(mf.getContentType(), content_type); EXPECT_EQ(mf.getWidth(), width); EXPECT_EQ(mf.getHeight(), height); EXPECT_DOUBLE_EQ(mf.getLatitude(), latitude); EXPECT_DOUBLE_EQ(mf.getLongitude(), longitude); EXPECT_EQ(mf.getModificationTime(), mtime); MediaFileBuilder mfb2(mf); MediaFile mf2 = mfb2.build(); EXPECT_EQ(mf, mf2); } TEST_F(MFBTest, chaining) { MediaType type(AudioMedia); std::string fname("abc"); std::string title("def"); std::string date("ghi"); std::string author("jkl"); std::string album("mno"); std::string album_artist("pqr"); std::string etag("stu"); std::string content_type("vwx"); std::string genre("yz"); int disc_number = 2; int track_number = 13; int duration = 99; int width = 640; int height = 480; double latitude = 67.2; double longitude = -7.5; uint64_t mtime = 42; MediaFile mf = MediaFileBuilder(fname) .setType(type) .setTitle(title) .setDate(date) .setAuthor(author) .setAlbum(album) .setAlbumArtist(album_artist) .setGenre(genre) .setDiscNumber(disc_number) .setTrackNumber(track_number) .setDuration(duration) .setWidth(width) .setHeight(height) .setLatitude(latitude) .setLongitude(longitude) .setETag(etag) .setContentType(content_type) .setModificationTime(42); // Now see if data survives a round trip. EXPECT_EQ(mf.getType(), type); EXPECT_EQ(mf.getFileName(), fname); EXPECT_EQ(mf.getTitle(), title); EXPECT_EQ(mf.getDate(), date); EXPECT_EQ(mf.getAuthor(), author); EXPECT_EQ(mf.getAlbum(), album); EXPECT_EQ(mf.getAlbumArtist(), album_artist); EXPECT_EQ(mf.getGenre(), genre); EXPECT_EQ(mf.getDiscNumber(), disc_number); EXPECT_EQ(mf.getTrackNumber(), track_number); EXPECT_EQ(mf.getDuration(), duration); EXPECT_EQ(mf.getETag(), etag); EXPECT_EQ(mf.getContentType(), content_type); EXPECT_EQ(mf.getWidth(), width); EXPECT_EQ(mf.getHeight(), height); EXPECT_DOUBLE_EQ(mf.getLatitude(), latitude); EXPECT_DOUBLE_EQ(mf.getLongitude(), longitude); EXPECT_EQ(mf.getModificationTime(), mtime); } TEST_F(MFBTest, fallback_title) { // Fallback title is derived from file name. MediaFile mf = MediaFileBuilder("/path/to/abc.ogg"); EXPECT_EQ(mf.getTitle(), "abc"); } TEST_F(MFBTest, fallback_album_artist) { // Fallback album_artist is the author. MediaFile mf = MediaFileBuilder("abc") .setAuthor("author"); EXPECT_EQ(mf.getAlbumArtist(), "author"); } TEST_F(MFBTest, faulty_usage) { MediaFileBuilder mfb("/foo/bar/baz.mp3"); MediaFile m1(std::move(mfb)); ASSERT_THROW(MediaFile m2(std::move(mfb)), std::logic_error); ASSERT_THROW(MediaFile m3(mfb), std::logic_error); } TEST_F(MFBTest, album_art_uri) { // No embedded art: use external art fetcher MediaFile mf = MediaFileBuilder("/foo/bar/baz.mp3") .setType(AudioMedia) .setAuthor("The Artist") .setAlbum("The Album"); EXPECT_EQ("image://albumart/artist=The%20Artist&album=The%20Album", mf.getArtUri()); // Embedded art: use thumbnailer mf = MediaFileBuilder("/foo/bar/baz.mp3") .setType(AudioMedia) .setAuthor("The Artist") .setAlbum("The Album") .setHasThumbnail(true); EXPECT_EQ("image://thumbnailer/file:///foo/bar/baz.mp3", mf.getArtUri()); // Videos use thumbnailer mf = MediaFileBuilder("/foo/bar/baz.mp4") .setType(VideoMedia) .setAuthor("The Artist") .setAlbum("The Album"); EXPECT_EQ("image://thumbnailer/file:///foo/bar/baz.mp4", mf.getArtUri()); } TEST_F(MFBTest, folder_art) { std::string fname = tmpdir + "/dummy.mp3"; MediaFile media = MediaFileBuilder(fname) .setType(AudioMedia) .setTitle("Title") .setAuthor("Artist") .setAlbum("Album"); EXPECT_EQ("image://albumart/artist=Artist&album=Album", media.getArtUri()); touch(tmpdir + "/folder.jpg", true); EXPECT_NE(std::string::npos, media.getArtUri().find("/folder.jpg")) << media.getArtUri(); remove(tmpdir + "/folder.jpg", true); EXPECT_EQ("image://albumart/artist=Artist&album=Album", media.getArtUri()); } TEST_F(MFBTest, folder_art_case_insensitive) { std::string fname = tmpdir + "/dummy.mp3"; MediaFile media = MediaFileBuilder(fname) .setType(AudioMedia) .setTitle("Title") .setAuthor("Artist") .setAlbum("Album"); touch(tmpdir + "/FOLDER.JPG"); EXPECT_NE(std::string::npos, media.getArtUri().find("/FOLDER.JPG")) << media.getArtUri(); } TEST_F(MFBTest, folder_art_precedence) { std::string fname = tmpdir + "/dummy.mp3"; MediaFile media = MediaFileBuilder(fname) .setType(AudioMedia) .setTitle("Title") .setAuthor("Artist") .setAlbum("Album"); touch(tmpdir + "/cover.jpg"); touch(tmpdir + "/album.jpg"); touch(tmpdir + "/albumart.jpg"); touch(tmpdir + "/.folder.jpg"); touch(tmpdir + "/folder.jpeg"); touch(tmpdir + "/folder.jpg"); touch(tmpdir + "/folder.png"); EXPECT_NE(std::string::npos, media.getArtUri().find("/cover.jpg")) << media.getArtUri(); remove(tmpdir + "/cover.jpg", true); EXPECT_NE(std::string::npos, media.getArtUri().find("/album.jpg")) << media.getArtUri(); remove(tmpdir + "/album.jpg", true); EXPECT_NE(std::string::npos, media.getArtUri().find("/albumart.jpg")) << media.getArtUri(); remove(tmpdir + "/albumart.jpg", true); EXPECT_NE(std::string::npos, media.getArtUri().find("/.folder.jpg")) << media.getArtUri(); remove(tmpdir + "/.folder.jpg", true); EXPECT_NE(std::string::npos, media.getArtUri().find("/folder.jpeg")) << media.getArtUri(); remove(tmpdir + "/folder.jpeg", true); EXPECT_NE(std::string::npos, media.getArtUri().find("/folder.jpg")) << media.getArtUri(); remove(tmpdir + "/folder.jpg", true); EXPECT_NE(std::string::npos, media.getArtUri().find("/folder.png")) << media.getArtUri(); } TEST_F(MFBTest, folder_art_cache_coverage) { std::vector files; for (int i = 0; i < 100; i++) { std::string directory = tmpdir + "/" + std::to_string(i); ASSERT_EQ(0, mkdir(directory.c_str(), 0700)); touch(directory + "/folder.jpg"); std::string fname = directory + "/dummy.mp3"; files.emplace_back(MediaFileBuilder(fname) .setType(AudioMedia) .setTitle("Title") .setAuthor("Artist") .setAlbum("Album")); } // Check art for a number of files smaller than the cache size twice for (int i = 0; i < 10; i++) { const auto &media = files[i]; EXPECT_NE(std::string::npos, media.getArtUri().find("/folder.jpg")) << media.getArtUri(); } for (int i = 0; i < 10; i++) { const auto &media = files[i]; EXPECT_NE(std::string::npos, media.getArtUri().find("/folder.jpg")) << media.getArtUri(); } // Now check a larger number of files twice for (const auto &media : files) { EXPECT_NE(std::string::npos, media.getArtUri().find("/folder.jpg")) << media.getArtUri(); } for (const auto &media : files) { EXPECT_NE(std::string::npos, media.getArtUri().find("/folder.jpg")) << media.getArtUri(); } } TEST_F(MFBTest, album_art) { std::string fname = tmpdir + "/dummy.mp3"; // File with embedded art Album album("Album", "Artist", "2015-11-23", "Rock", fname, true, 1); EXPECT_NE(std::string::npos, album.getArtUri().find("/dummy.mp3")) << album.getArtUri(); // No embedded art album = Album("Album", "Artist", "2015-11-23", "Rock", fname, false, 1); EXPECT_EQ("image://albumart/artist=Artist&album=Album", album.getArtUri()); // No embedded art, but folder art available touch(tmpdir + "/folder.jpg", true); EXPECT_NE(std::string::npos, album.getArtUri().find("/folder.jpg")) << album.getArtUri(); } int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } mediascanner2-0.115/test/test_qml.cc000066400000000000000000000136651436755250000173730ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include "test_config.h" using namespace mediascanner; class MediaStoreData { public: MediaStoreData() : test_dbus(nullptr, g_object_unref) { db_path = "./mediascanner-cache.XXXXXX"; if (mkdtemp(const_cast(db_path.c_str())) == nullptr) { throw std::runtime_error("Could not create temporary directory"); } setenv("MEDIASCANNER_CACHEDIR", db_path.c_str(), true); populate(); test_dbus.reset(g_test_dbus_new(G_TEST_DBUS_NONE)); g_test_dbus_add_service_dir(test_dbus.get(), TEST_DIR "/services"); g_test_dbus_up(test_dbus.get()); daemon.setProgram(TEST_DIR "/../src/ms-dbus/mediascanner-dbus-2.0"); daemon.setProcessChannelMode(QProcess::ForwardedChannels); daemon.start(); daemon.closeWriteChannel(); if (!daemon.waitForStarted()) { throw std::runtime_error("Failed to start mediascanner-dbus-2.0"); } } ~MediaStoreData() { daemon.kill(); if (!daemon.waitForFinished()) { fprintf(stderr, "Failed to stop mediascanner-dbus-2.0\n"); } g_test_dbus_down(test_dbus.get()); std::filesystem::remove_all(db_path); } void populate() { MediaStore store(MS_READ_WRITE); store.insert(MediaFileBuilder("/path/foo1.ogg") .setType(AudioMedia) .setContentType("audio/ogg") .setETag("etag") .setTitle("Straight Through The Sun") .setAuthor("Spiderbait") .setAlbum("Spiderbait") .setAlbumArtist("Spiderbait") .setDate("2013-11-15") .setGenre("rock") .setDiscNumber(1) .setTrackNumber(1) .setDuration(235)); store.insert(MediaFileBuilder("/path/foo2.ogg") .setType(AudioMedia) .setContentType("audio/ogg") .setETag("etag") .setTitle("It's Beautiful") .setAuthor("Spiderbait") .setAlbum("Spiderbait") .setAlbumArtist("Spiderbait") .setDate("2013-11-15") .setGenre("rock") .setDiscNumber(1) .setTrackNumber(2) .setDuration(220)); store.insert(MediaFileBuilder("/path/foo3.ogg") .setType(AudioMedia) .setContentType("audio/ogg") .setETag("etag") .setTitle("Buy Me a Pony") .setAuthor("Spiderbait") .setAlbum("Ivy and the Big Apples") .setAlbumArtist("Spiderbait") .setDate("1996-10-04") .setGenre("rock") .setDiscNumber(1) .setTrackNumber(3) .setDuration(104)); store.insert(MediaFileBuilder("/path/foo4.ogg") .setType(AudioMedia) .setContentType("audio/ogg") .setETag("etag") .setTitle("Peaches & Cream") .setAuthor("The John Butler Trio") .setAlbum("Sunrise Over Sea") .setAlbumArtist("The John Butler Trio") .setDate("2004-03-08") .setGenre("roots") .setDiscNumber(1) .setTrackNumber(2) .setDuration(407)); store.insert(MediaFileBuilder("/path/foo5.ogg") .setType(AudioMedia) .setContentType("audio/ogg") .setETag("etag") .setTitle("Zebra") .setAuthor("The John Butler Trio") .setAlbum("Sunrise Over Sea") .setAlbumArtist("The John Butler Trio") .setDate("2004-03-08") .setGenre("roots") .setDiscNumber(1) .setTrackNumber(10) .setDuration(237)); store.insert(MediaFileBuilder("/path/foo6.ogg") .setType(AudioMedia) .setContentType("audio/ogg") .setETag("etag") .setTitle("Revolution") .setAuthor("The John Butler Trio") .setAlbum("April Uprising") .setAlbumArtist("The John Butler Trio") .setDate("2010-01-01") .setGenre("roots") .setDiscNumber(1) .setTrackNumber(1) .setDuration(305)); store.insert(MediaFileBuilder("/path/foo7.ogg") .setType(AudioMedia) .setContentType("audio/ogg") .setETag("etag") .setTitle("One Way Road") .setAuthor("The John Butler Trio") .setAlbum("April Uprising") .setAlbumArtist("The John Butler Trio") .setDate("2010-01-01") .setGenre("roots") .setDiscNumber(1) .setTrackNumber(2) .setDuration(185)); } private: std::string db_path; std::unique_ptr test_dbus; QProcess daemon; }; int main(int argc, char** argv) { QGuiApplication(argc, argv); MediaStoreData data; return quick_test_main(argc, argv, "Mediascanner", SOURCE_DIR "/qml"); } mediascanner2-0.115/test/test_qml_nodb.qml000066400000000000000000000022131436755250000205640ustar00rootroot00000000000000import QtQuick 2.0 import QtTest 1.0 import MediaScanner 0.1 TestCase { id: root name: "NoDatabaseTests" function test_mediastore() { ignoreWarning("Could not initialise media store: unable to open database file"); var store = Qt.createQmlObject( "import MediaScanner 0.1;" + "MediaStore {}", root); if (store === null) { fail("Could not create MediaStore component"); } ignoreWarning("query() called on invalid MediaStore"); compare(store.query("foo", MediaStore.AllMedia), []); ignoreWarning("lookup() called on invalid MediaStore"); compare(store.lookup("/some/file"), null); } function test_songsmodel() { ignoreWarning("Could not initialise media store: unable to open database file"); var model = Qt.createQmlObject( "import MediaScanner 0.1;" + "SongsModel {" + " store: MediaStore {}" + "}", root); if (model === null) { fail("Could not create SongsModel component"); } // Model is empty compare(model.count, 0); } } mediascanner2-0.115/test/test_sqliteutils.cc000066400000000000000000000053031436755250000211520ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include using namespace std; using namespace mediascanner; class SqliteTest : public ::testing::Test { public: SqliteTest() : db(NULL) { } virtual void SetUp() { int rc = sqlite3_open(":memory:", &db); if (rc != SQLITE_OK) { throw runtime_error(sqlite3_errstr(rc)); } } virtual void TearDown() { if (db != NULL) { int rc = sqlite3_close(db); if (rc != SQLITE_OK) { throw runtime_error(sqlite3_errstr(rc)); } db = NULL; } } sqlite3 *db; }; TEST_F(SqliteTest, Execute) { Statement stmt(db, "SELECT 1"); EXPECT_EQ(true, stmt.step()); stmt.finalize(); } TEST_F(SqliteTest, GetInt) { Statement stmt(db, "SELECT 42"); EXPECT_EQ(true, stmt.step()); EXPECT_EQ(42, stmt.getInt(0)); stmt.finalize(); } TEST_F(SqliteTest, GetText) { Statement stmt(db, "SELECT 'foo'"); EXPECT_EQ(true, stmt.step()); EXPECT_EQ("foo", stmt.getText(0)); stmt.finalize(); } TEST_F(SqliteTest, BindInt) { Statement stmt(db, "SELECT ?"); stmt.bind(1, 42); EXPECT_EQ(true, stmt.step()); EXPECT_EQ(42, stmt.getInt(0)); stmt.finalize(); } TEST_F(SqliteTest, BindText) { Statement stmt(db, "SELECT ?"); stmt.bind(1, "foo"); EXPECT_EQ(true, stmt.step()); EXPECT_EQ("foo", stmt.getText(0)); stmt.finalize(); } TEST_F(SqliteTest, Insert) { Statement create(db, "CREATE TABLE foo (id INT PRIMARY KEY)"); EXPECT_FALSE(create.step()); create.finalize(); Statement insert(db, "INSERT INTO foo(id) VALUES (?)"); insert.bind(1, 42); EXPECT_FALSE(insert.step()); insert.finalize(); Statement select(db, "SELECT id FROM foo"); EXPECT_TRUE(select.step()); EXPECT_EQ(42, select.getInt(0)); EXPECT_FALSE(select.step()); select.finalize(); } int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } mediascanner2-0.115/test/test_subtreewatcher.cc000066400000000000000000000176101436755250000216230ustar00rootroot00000000000000/* * Copyright (C) 2016 Canonical, Ltd. * * Authors: * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include "test_config.h" #include #include #include #include #include #include #include #include using namespace std; using namespace mediascanner; namespace { const char CRASH_AFTER_ENV[] = "MEDIASCANNER_EXTRACTOR_CRASH_AFTER"; typedef std::unique_ptr GDBusConnectionPtr; void copy_file(const string &src, const string &dst) { FILE* f = fopen(src.c_str(), "r"); ASSERT_TRUE(f); fseek(f, 0, SEEK_END); size_t size = ftell(f); char* buf = new char[size]; fseek(f, 0, SEEK_SET); ASSERT_EQ(fread(buf, 1, size, f), size); fclose(f); f = fopen(dst.c_str(), "w"); ASSERT_TRUE(f); ASSERT_EQ(fwrite(buf, 1, size, f), size); fclose(f); delete[] buf; } void iterate_main_loop() { while (g_main_context_iteration(nullptr, FALSE)) { } } } class SubtreeWatcherTest : public ::testing::Test { protected: virtual void SetUp() override { main_loop_.reset(g_main_loop_new(nullptr, false)); tmpdir_ = TEST_DIR "/subtreewatcher-test.XXXXXX"; ASSERT_NE(nullptr, mkdtemp(&tmpdir_[0])); } void setup_watcher() { test_dbus_.reset(g_test_dbus_new(G_TEST_DBUS_NONE)); g_test_dbus_add_service_dir(test_dbus_.get(), TEST_DIR "/services"); g_test_dbus_up(test_dbus_.get()); session_bus_ = make_connection(); g_dbus_connection_signal_subscribe( session_bus_.get(), nullptr, "com.canonical.unity.scopes", "InvalidateResults", "/com/canonical/unity/scopes", "mediascanner-music", G_DBUS_SIGNAL_FLAGS_NONE, &SubtreeWatcherTest::invalidateCallback, this, nullptr); store_.reset(new MediaStore(":memory:", MS_READ_WRITE)); extractor_.reset(new MetadataExtractor(session_bus_.get())); invalidator_.reset(new InvalidationSender); invalidator_->setBus(session_bus_.get()); watcher_.reset(new SubtreeWatcher(*store_, *extractor_, *invalidator_)); } virtual void TearDown() override { watcher_.reset(); invalidator_.reset(); extractor_.reset(); store_.reset(); session_bus_.reset(); test_dbus_.reset(); if (!tmpdir_.empty()) { string cmd = "rm -rf " + tmpdir_; ASSERT_EQ(0, system(cmd.c_str())); } unsetenv(CRASH_AFTER_ENV); } GDBusConnectionPtr make_connection() { GError *error = nullptr; char *address = g_dbus_address_get_for_bus_sync(G_BUS_TYPE_SESSION, nullptr, &error); if (!address) { string errortxt(error->message); g_error_free(error); throw std::runtime_error( string("Failed to determine session bus address: ") + errortxt); } GDBusConnectionPtr bus( g_dbus_connection_new_for_address_sync( address, static_cast(G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION), nullptr, nullptr, &error), g_object_unref); g_free(address); if (!bus) { string errortxt(error->message); g_error_free(error); throw std::runtime_error( string("Failed to connect to session bus: ") + errortxt); } return bus; } bool wait_for_invalidate(int timeout) { invalidate_timeout_id_ = g_timeout_add( timeout * 1000, [](void *user_data) -> int { auto test = reinterpret_cast(user_data); g_main_loop_quit(test->main_loop_.get()); test->invalidate_timeout_id_ = 0; return G_SOURCE_REMOVE; }, this); g_main_loop_run(main_loop_.get()); // The timeout ID will be zero if it was fired and we received // no signals. if (invalidate_timeout_id_ == 0) { return false; } g_source_remove(invalidate_timeout_id_); invalidate_timeout_id_ = 0; return true; } static void invalidateCallback(GDBusConnection */*connection*/, const char */*sender_name*/, const char */*object_path*/, const char */*interface_name*/, const char */*signal_name*/, GVariant */*parameters*/, gpointer user_data) { auto test = reinterpret_cast(user_data); test->invalidate_count_++; // If we're waiting on an invalidate signal, quit the main loop if (test->invalidate_timeout_id_ != 0) { g_main_loop_quit(test->main_loop_.get()); } } string tmpdir_; unique_ptr test_dbus_ {nullptr, g_object_unref}; unique_ptr store_; unique_ptr watcher_; int invalidate_count_ = 0; private: GDBusConnectionPtr session_bus_ {nullptr, g_object_unref}; unique_ptr extractor_; unique_ptr invalidator_; unique_ptr main_loop_ {nullptr, g_main_loop_unref}; unsigned int invalidate_timeout_id_ = 0; }; TEST_F(SubtreeWatcherTest, open_for_write_without_change) { setup_watcher(); watcher_->addDir(tmpdir_); iterate_main_loop(); string testfile = tmpdir_ + "/testfile.ogg"; copy_file(SOURCE_DIR "/media/testfile.ogg", testfile); EXPECT_TRUE(wait_for_invalidate(2)); ASSERT_EQ(store_->size(), 1); // Invalidate called once for new file. EXPECT_EQ(1, invalidate_count_); int fd = open(testfile.c_str(), O_RDWR); ASSERT_GE(fd, 0); ASSERT_EQ(0, close(fd)); // No change, to file so no new invalidations EXPECT_FALSE(wait_for_invalidate(2)); EXPECT_EQ(1, invalidate_count_); fd = open(testfile.c_str(), O_RDWR|O_APPEND); ASSERT_GE(fd, 0); ASSERT_EQ(5, write(fd, "hello", 5)); ASSERT_EQ(0, close(fd)); // File changed, so invalidation count increases. EXPECT_TRUE(wait_for_invalidate(3)); EXPECT_EQ(2, invalidate_count_); } TEST_F(SubtreeWatcherTest, fallback_added_for_failed_extraction) { setenv(CRASH_AFTER_ENV, "0", true); setup_watcher(); watcher_->addDir(tmpdir_); iterate_main_loop(); string testfile = tmpdir_ + "/testfile.ogg"; copy_file(SOURCE_DIR "/media/testfile.ogg", testfile); EXPECT_TRUE(wait_for_invalidate(10)); ASSERT_EQ(store_->size(), 1); // Failed extraction exists in database with its title set based // on the file name, but other metadata absent. MediaFile media = store_->lookup(testfile); EXPECT_EQ("testfile", media.getTitle()); EXPECT_EQ("", media.getAuthor()); EXPECT_EQ("", media.getAlbum()); EXPECT_EQ(0, media.getDuration()); } int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } mediascanner2-0.115/test/test_util.cc000066400000000000000000000032741436755250000175520ustar00rootroot00000000000000/* * Copyright (C) 2013 Canonical, Ltd. * * Authors: * Jussi Pakkanen * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include"../src/mediascanner/internal/utils.hh" #include"../src/mediascanner/MediaFile.hh" #include"../src/mediascanner/MediaFileBuilder.hh" #include "test_config.h" using namespace mediascanner; class UtilTest : public ::testing::Test { public: UtilTest() { } virtual void SetUp() { } virtual void TearDown() { } }; TEST_F(UtilTest, optical) { std::string blu_root(SOURCE_DIR "/bluray_root"); std::string dvd_root(SOURCE_DIR "/dvd_root"); std::string nodisc_root(SOURCE_DIR "/media"); ASSERT_TRUE(is_optical_disc(blu_root)); ASSERT_TRUE(is_optical_disc(dvd_root)); ASSERT_FALSE(is_optical_disc(nodisc_root)); } TEST_F(UtilTest, scanblock) { std::string noblock_root(SOURCE_DIR "/media"); std::string block_root(SOURCE_DIR "/noscan"); ASSERT_TRUE(has_scanblock(block_root)); ASSERT_FALSE(has_scanblock(noblock_root)); } int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } mediascanner2-0.115/test/test_volumemanager.cc000066400000000000000000000161421436755250000214350ustar00rootroot00000000000000/* * Copyright (C) 2016 Canonical, Ltd. * * Authors: * James Henstridge * * This library is free software; you can redistribute it and/or modify it under * the terms of version 3 of the GNU General Public License as published * by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include "test_config.h" #include #include #include #include #include #include #include #include #include using namespace std; using namespace mediascanner; namespace { typedef std::unique_ptr GDBusConnectionPtr; void copy_file(const string &src, const string &dst) { FILE* f = fopen(src.c_str(), "r"); ASSERT_TRUE(f); fseek(f, 0, SEEK_END); size_t size = ftell(f); char* buf = new char[size]; fseek(f, 0, SEEK_SET); ASSERT_EQ(fread(buf, 1, size, f), size); fclose(f); f = fopen(dst.c_str(), "w"); ASSERT_TRUE(f); ASSERT_EQ(fwrite(buf, 1, size, f), size); fclose(f); delete[] buf; } } class VolumeManagerTest : public ::testing::Test { protected: virtual void SetUp() override { main_loop_.reset(g_main_loop_new(nullptr, false)); tmpdir_ = TEST_DIR "/volumemanager-test.XXXXXX"; ASSERT_NE(nullptr, mkdtemp(&tmpdir_[0])); test_dbus_.reset(g_test_dbus_new(G_TEST_DBUS_NONE)); g_test_dbus_add_service_dir(test_dbus_.get(), TEST_DIR "/services"); g_test_dbus_up(test_dbus_.get()); session_bus_ = make_connection(); store_.reset(new MediaStore(":memory:", MS_READ_WRITE)); extractor_.reset(new MetadataExtractor(session_bus_.get())); invalidator_.reset(new InvalidationSender); volumes_.reset(new VolumeManager(*store_, *extractor_, *invalidator_)); } virtual void TearDown() override { volumes_.reset(); invalidator_.reset(); extractor_.reset(); store_.reset(); session_bus_.reset(); test_dbus_.reset(); if (!tmpdir_.empty()) { string cmd = "rm -rf " + tmpdir_; ASSERT_EQ(0, system(cmd.c_str())); } } GDBusConnectionPtr make_connection() { GError *error = nullptr; char *address = g_dbus_address_get_for_bus_sync(G_BUS_TYPE_SESSION, nullptr, &error); if (!address) { string errortxt(error->message); g_error_free(error); throw std::runtime_error( string("Failed to determine session bus address: ") + errortxt); } GDBusConnectionPtr bus( g_dbus_connection_new_for_address_sync( address, static_cast(G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION), nullptr, nullptr, &error), g_object_unref); g_free(address); if (!bus) { string errortxt(error->message); g_error_free(error); throw std::runtime_error( string("Failed to connect to session bus: ") + errortxt); } return bus; } void wait_until_idle() { while (g_main_context_iteration(nullptr, FALSE)) { if (volumes_->idle()) { break; } } } string tmpdir_; unique_ptr test_dbus_ {nullptr, g_object_unref}; unique_ptr store_; unique_ptr volumes_; private: GDBusConnectionPtr session_bus_ {nullptr, g_object_unref}; unique_ptr extractor_; unique_ptr invalidator_; unique_ptr main_loop_ {nullptr, g_main_loop_unref}; }; TEST_F(VolumeManagerTest, add_remove_volumes) { const string volume1 = tmpdir_ + "/volume1"; const string volume2 = tmpdir_ + "/volume2"; ASSERT_EQ(0, mkdir(volume1.c_str(), 0755)); ASSERT_EQ(0, mkdir(volume2.c_str(), 0755)); const string file1 = volume1 + "/file1.ogg"; const string file2 = volume1 + "/file2.ogg"; const string file3 = volume2 + "/file3.ogg"; copy_file(SOURCE_DIR "/media/testfile.ogg", file1); copy_file(SOURCE_DIR "/media/testfile.ogg", file2); copy_file(SOURCE_DIR "/media/testfile.ogg", file3); volumes_->queueAddVolume(volume1); wait_until_idle(); EXPECT_EQ(store_->size(), 2); store_->lookup(file1); store_->lookup(file2); try { store_->lookup(file3); FAIL(); } catch (const runtime_error&) { } // Queue up two operations volumes_->queueAddVolume(volume2); volumes_->queueRemoveVolume(volume1); wait_until_idle(); EXPECT_EQ(store_->size(), 1); store_->lookup(file3); try { store_->lookup(file1); store_->lookup(file2); } catch (const runtime_error&) { } volumes_->queueRemoveVolume(volume2); wait_until_idle(); EXPECT_EQ(store_->size(), 0); // This should result in only volume2 being added volumes_->queueAddVolume(volume1); volumes_->queueAddVolume(volume2); volumes_->queueRemoveVolume(volume1); wait_until_idle(); EXPECT_EQ(store_->size(), 1); store_->lookup(file3); } TEST_F(VolumeManagerTest, add_volume_during_initial_scan) { const string volume1 = tmpdir_ + "/volume1"; const string volume2 = tmpdir_ + "/volume2"; ASSERT_EQ(0, mkdir(volume1.c_str(), 0755)); ASSERT_EQ(0, mkdir(volume2.c_str(), 0755)); const string file1 = volume1 + "/file1.ogg"; const string file2 = volume1 + "/file2.ogg"; const string file3 = volume2 + "/file3.ogg"; copy_file(SOURCE_DIR "/media/testfile.ogg", file1); copy_file(SOURCE_DIR "/media/testfile.ogg", file2); copy_file(SOURCE_DIR "/media/testfile.ogg", file3); // Set an idle function that should be called during the initial // scan that will queue up adding a second volume. function callback = [&] { // We expect that this is called during the scan EXPECT_FALSE(volumes_->idle()); volumes_->queueAddVolume(volume2); }; g_idle_add([](void *user_data) -> gboolean { auto callback = *reinterpret_cast*>(user_data); callback(); return G_SOURCE_REMOVE; }, &callback); volumes_->queueAddVolume(volume1); wait_until_idle(); EXPECT_EQ(store_->size(), 3); store_->lookup(file1); store_->lookup(file2); store_->lookup(file3); } int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }