pax_global_header00006660000000000000000000000064134514502240014512gustar00rootroot0000000000000052 comment=6b5754a122bad3190662263f034425d6cdabde47 fernandotcl-btag-a5d4440/000077500000000000000000000000001345145022400152535ustar00rootroot00000000000000fernandotcl-btag-a5d4440/.gitignore000066400000000000000000000000261345145022400172410ustar00rootroot00000000000000*.o *.swp udisks-glue fernandotcl-btag-a5d4440/.travis.yml000066400000000000000000000012011345145022400173560ustar00rootroot00000000000000language: c++ matrix: include: - os: linux dist: xenial addons: apt: packages: - cmake - libboost-dev - libboost-filesystem-dev - libboost-locale-dev - libtag1-dev - libcue-dev - libedit-dev - pkg-config script: - cmake . -DENABLE_TESTS=1 - make VERBOSE=1 - make VERBOSE=1 check - os: osx addons: homebrew: packages: - cmake - pkg-config - boost - libcue - libedit - taglib script: - cmake . -DENABLE_TESTS=1 - make VERBOSE=1 - make VERBOSE=1 check fernandotcl-btag-a5d4440/CMakeLists.txt000066400000000000000000000051431345145022400200160ustar00rootroot00000000000000cmake_minimum_required(VERSION 2.8 FATAL_ERROR) project(btag) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/") if(CMAKE_COMPILER_IS_GNUCXX OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") endif(CMAKE_COMPILER_IS_GNUCXX OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") list(APPEND CMAKE_PREFIX_PATH /usr/local) find_package(Boost 1.49.0 COMPONENTS filesystem locale system REQUIRED) find_package(TagLib REQUIRED) find_package(LibEdit REQUIRED) option(CUESHEET_SUPPORT "Support for reading metadata from cue sheets" ON) if(CUESHEET_SUPPORT) find_package(LibCue REQUIRED) find_package(Iconv REQUIRED) set(EXTRA_LIBRARIES ${EXTRA_LIBRARIES} ${LIBCUE_LIBRARIES} ${Iconv_LIBRARIES}) endif(CUESHEET_SUPPORT) set(PROGRAM_NAME ${PROJECT_NAME}) set(BTAG_SOURCES src/BasicStringFilter.cpp src/ConfirmationHandler.cpp src/EnglishTitleLocalizationHandler.cpp src/InteractiveTagger.cpp src/main.cpp src/RenamingFilter.cpp src/SimpleCapitalizationFilter.cpp src/SpanishTitleLocalizationHandler.cpp src/StandardConsole.cpp src/TitleCapitalizationFilter.cpp src/TitleLocalizationHandler.cpp) set(BTAG_HEADERS ${CMAKE_CURRENT_BINARY_DIR}/src/config.h src/BasicStringFilter.h src/CapitalizationFilter.h src/ConfirmationHandler.h src/ConservativeRenamingFilter.h src/EnglishTitleLocalizationHandler.h src/InteractiveTagger.h src/number_cast.h src/RenamingFilter.h src/SimpleCapitalizationFilter.h src/SpanishTitleLocalizationHandler.h src/StandardConsole.h src/TitleCapitalizationFilter.h src/TitleLocalizationHandler.h src/UnixRenamingFilter.h src/wide_string_cast.h) if(CUESHEET_SUPPORT) set(BTAG_SOURCES ${BTAG_SOURCES} src/CueReader.cpp src/CueReaderMultiplexer.cpp) set(BTAG_HEADERS ${BTAG_HEADERS} src/CueReader.h src/CueReaderMultiplexer.h) endif(CUESHEET_SUPPORT) configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/src/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/src/config.h) include_directories( ${CMAKE_CURRENT_BINARY_DIR}/src ${Boost_INCLUDE_DIRS} ${LIBCUE_INCLUDE_DIRS} ${LIBEDIT_INCLUDE_DIRS} ${TAGLIB_INCLUDE_DIRS}) add_executable(btag ${BTAG_SOURCES} ${BTAG_HEADERS}) target_link_libraries(btag ${Boost_LIBRARIES} ${LIBEDIT_LIBRARIES} ${TAGLIB_LIBRARIES} ${EXTRA_LIBRARIES}) install(TARGETS btag DESTINATION bin) option(ENABLE_TESTS "Make the check target available" OFF) if(ENABLE_TESTS) enable_testing() add_subdirectory(tests) endif(ENABLE_TESTS) fernandotcl-btag-a5d4440/LICENSE000066400000000000000000000025021345145022400162570ustar00rootroot00000000000000Copyright (c) 2010 - 2013, 2015 Fernando Tarlá Cardoso Lemos Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. fernandotcl-btag-a5d4440/README.md000066400000000000000000000051421345145022400165340ustar00rootroot00000000000000# btag [![Build Status](https://travis-ci.com/fernandotcl/btag.svg?branch=master)](https://travis-ci.com/fernandotcl/btag) btag is a command line based audio file tagger. It retains information about the filesystem structure so that you can tag a bunch of albums with ease. In some ways, btag can be compared to graphical taggers like [EasyTAG][] as both allow you to tag a lot of audio files in batches. [easytag]: http://easytag.sourceforge.net/ ![btag screenshot](screenshot.png) A brief list of features: * Tagging of a vast number of formats supported by [TagLib][] * Tag normalization and filtering * Locale-aware title capitalization * Renaming of files and directories * Reading metadata from cue sheets [taglib]: http://developer.kde.org/~wheeler/taglib.html ## Usage Simply launch btag specifying the directory containing the audio files you want to tag: ```sh $ btag /music/stuff ``` The media files in the directory will be read and their tags will be presented in your terminal. You can choose to edit the value of the tags or to simply confirm the values that were read from the file. In order to extract the most from btag, configure it so that it can filter the values read from the tags before they are presented to you. That allows you to have btag apply title capitalization to the values in the tags, for instance. Use the command line switches specified in `btag(1)` to configure the filters btag will use. You might want to set a shell alias instead of typing all the switches every time you run btag: ```sh $ alias btag='btag -f title -t en -d "%album (%year)" -r "%track. %title"' ``` Note that you can override the switches you specified in the alias. ## Installing The easiest way to install btag is through [Homebrew][]. There is a formula for btag in [my Homebrew tap][tap]. [homebrew]: http://mxcl.github.com/homebrew/ [tap]: https://github.com/fernandotcl/homebrew-fernandotcl If you're compiling from source, you will need: * [Boost][] >= 1.49.0 (filesystem, locale and system) * [libcue][] * [libedit][] * [TagLib][] * [pkg-config][] * [CMake][] [boost]: http://www.boost.org/ [libcue]: http://sourceforge.net/projects/libcue/ [libedit]: https://www.thrysoee.dk/editline/ [taglib]: http://taglib.github.com/ [pkg-config]: http://www.freedesktop.org/wiki/Software/pkg-config [cmake]: http://www.cmake.org/ To compile and install: ```sh cd /path/to/source cmake . make install ``` ## Credits btag was created by [Fernando Tarlá Cardoso Lemos][fernando]. [fernando]: mailto:fernandotcl@gmail.com ## License btag is available under the BSD 2-clause license. See the LICENSE file for more information. fernandotcl-btag-a5d4440/cmake/000077500000000000000000000000001345145022400163335ustar00rootroot00000000000000fernandotcl-btag-a5d4440/cmake/Modules/000077500000000000000000000000001345145022400177435ustar00rootroot00000000000000fernandotcl-btag-a5d4440/cmake/Modules/FindIconv.cmake000066400000000000000000000102321345145022400226220ustar00rootroot00000000000000# Distributed under the OSI-approved BSD 3-Clause License. See accompanying # file Copyright.txt or https://cmake.org/licensing for details. #[=======================================================================[.rst: FindIconv --------- This module finds the ``iconv()`` POSIX.1 functions on the system. These functions might be provided in the regular C library or externally in the form of an additional library. The following variables are provided to indicate iconv support: .. variable:: Iconv_FOUND Variable indicating if the iconv support was found. .. variable:: Iconv_INCLUDE_DIRS The directories containing the iconv headers. .. variable:: Iconv_LIBRARIES The iconv libraries to be linked. .. variable:: Iconv_IS_BUILT_IN A variable indicating whether iconv support is stemming from the C library or not. Even if the C library provides `iconv()`, the presence of an external `libiconv` implementation might lead to this being false. Additionally, the following :prop_tgt:`IMPORTED` target is being provided: .. variable:: Iconv::Iconv Imported target for using iconv. The following cache variables may also be set: .. variable:: Iconv_INCLUDE_DIR The directory containing the iconv headers. .. variable:: Iconv_LIBRARY The iconv library (if not implicitly given in the C library). .. note:: On POSIX platforms, iconv might be part of the C library and the cache variables ``Iconv_INCLUDE_DIR`` and ``Iconv_LIBRARY`` might be empty. #]=======================================================================] include(CMakePushCheckState) if(CMAKE_C_COMPILER_LOADED) include(CheckCSourceCompiles) elseif(CMAKE_CXX_COMPILER_LOADED) include(CheckCXXSourceCompiles) else() # If neither C nor CXX are loaded, implicit iconv makes no sense. set(Iconv_IS_BUILT_IN FALSE) endif() # iconv can only be provided in libc on a POSIX system. # If any cache variable is already set, we'll skip this test. if(NOT DEFINED Iconv_IS_BUILT_IN) if(UNIX AND NOT DEFINED Iconv_INCLUDE_DIR AND NOT DEFINED Iconv_LIBRARY) cmake_push_check_state(RESET) # We always suppress the message here: Otherwise on supported systems # not having iconv in their C library (e.g. those using libiconv) # would always display a confusing "Looking for iconv - not found" message set(CMAKE_FIND_QUIETLY TRUE) # The following code will not work, but it's sufficient to see if it compiles. # Note: libiconv will define the iconv functions as macros, so CheckSymbolExists # will not yield correct results. set(Iconv_IMPLICIT_TEST_CODE " #include #include int main() { char *a, *b; size_t i, j; iconv_t ic; ic = iconv_open(\"to\", \"from\"); iconv(ic, &a, &i, &b, &j); iconv_close(ic); } " ) if(CMAKE_C_COMPILER_LOADED) check_c_source_compiles("${Iconv_IMPLICIT_TEST_CODE}" Iconv_IS_BUILT_IN) else() check_cxx_source_compiles("${Iconv_IMPLICIT_TEST_CODE}" Iconv_IS_BUILT_IN) endif() cmake_pop_check_state() else() set(Iconv_IS_BUILT_IN FALSE) endif() endif() if(NOT Iconv_IS_BUILT_IN) find_path(Iconv_INCLUDE_DIR NAMES "iconv.h" DOC "iconv include directory") set(Iconv_LIBRARY_NAMES "iconv" "libiconv") else() set(Iconv_INCLUDE_DIR "" CACHE FILEPATH "iconv include directory") set(Iconv_LIBRARY_NAMES "c") endif() find_library(Iconv_LIBRARY NAMES ${Iconv_LIBRARY_NAMES} DOC "iconv library (potentially the C library)") mark_as_advanced(Iconv_INCLUDE_DIR) mark_as_advanced(Iconv_LIBRARY) include(FindPackageHandleStandardArgs) if(NOT Iconv_IS_BUILT_IN) find_package_handle_standard_args(Iconv REQUIRED_VARS Iconv_LIBRARY Iconv_INCLUDE_DIR) else() find_package_handle_standard_args(Iconv REQUIRED_VARS Iconv_LIBRARY) endif() if(Iconv_FOUND) set(Iconv_INCLUDE_DIRS "${Iconv_INCLUDE_DIR}") set(Iconv_LIBRARIES "${Iconv_LIBRARY}") if(NOT TARGET Iconv::Iconv) add_library(Iconv::Iconv INTERFACE IMPORTED) endif() set_property(TARGET Iconv::Iconv PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${Iconv_INCLUDE_DIRS}") set_property(TARGET Iconv::Iconv PROPERTY INTERFACE_LINK_LIBRARIES "${Iconv_LIBRARIES}") endif() fernandotcl-btag-a5d4440/cmake/Modules/FindLibCue.cmake000066400000000000000000000016171345145022400227160ustar00rootroot00000000000000# - Try to find LibCue # Once done this will define # LIBCUE_FOUND - System has LibCue # LIBCUE_INCLUDE_DIRS - The LibCue include directories # LIBCUE_LIBRARIES - The libraries needed to use LibCue # LIBCUE_DEFINITIONS - Compiler switches required for using LibCue find_package(PkgConfig) pkg_check_modules(PC_LIBCUE QUIET libcue) set(LIBCUE_DEFINITIONS ${PC_LIBCUE_CFLAGS_OTHER}) find_path(LIBCUE_INCLUDE_DIR libcue/libcue.h HINTS ${PC_LIBCUE_INCLUDEDIR} ${PC_LIBCUE_INCLUDE_DIRS} PATH_SUFFIXES libcue) find_library(LIBCUE_LIBRARY NAMES cue libcue HINTS ${PC_LIBCUE_LIBDIR} ${PC_LIBCUE_LIBRARY_DIRS}) include(FindPackageHandleStandardArgs) find_package_handle_standard_args(LibCue DEFAULT_MSG LIBCUE_LIBRARY LIBCUE_INCLUDE_DIR) mark_as_advanced(LIBCUE_INCLUDE_DIR LIBCUE_LIBRARY) set(LIBCUE_LIBRARIES ${LIBCUE_LIBRARY}) set(LIBCUE_INCLUDE_DIRS ${LIBCUE_INCLUDE_DIR}) fernandotcl-btag-a5d4440/cmake/Modules/FindLibEdit.cmake000066400000000000000000000016121345145022400230620ustar00rootroot00000000000000# - Try to find LibEdit # Once done this will define # LIBEDIT_FOUND - System has LibEdit # LIBEDIT_INCLUDE_DIRS - The LibEdit include directories # LIBEDIT_LIBRARIES - The libraries needed to use LibEdit # LIBEDIT_DEFINITIONS - Compiler switches required for using LibEdit find_package(PkgConfig) pkg_check_modules(PC_LIBEDIT QUIET libedit) set(LIBEDIT_DEFINITIONS ${PC_LIBEDIT_CFLAGS_OTHER}) find_path(LIBEDIT_INCLUDE_DIR histedit.h HINTS ${PC_LIBEDIT_INCLUDEDIR} ${PC_LIBEDIT_INCLUDE_DIRS}) find_library(LIBEDIT_LIBRARY NAMES edit libedit HINTS ${PC_LIBEDIT_LIBDIR} ${PC_LIBEDIT_LIBRARY_DIRS}) include(FindPackageHandleStandardArgs) find_package_handle_standard_args(LibEdit DEFAULT_MSG LIBEDIT_LIBRARY LIBEDIT_INCLUDE_DIR) mark_as_advanced(LIBEDIT_INCLUDE_DIR LIBEDIT_LIBRARY) set(LIBEDIT_LIBRARIES ${LIBEDIT_LIBRARY}) set(LIBEDIT_INCLUDE_DIRS ${LIBEDIT_INCLUDE_DIR}) fernandotcl-btag-a5d4440/cmake/Modules/FindTagLib.cmake000066400000000000000000000016171345145022400227150ustar00rootroot00000000000000# - Try to find TagLib # Once done this will define # TAGLIB_FOUND - System has TagLib # TAGLIB_INCLUDE_DIRS - The TagLib include directories # TAGLIB_LIBRARIES - The libraries needed to use TagLib # TAGLIB_DEFINITIONS - Compiler switches required for using TagLib find_package(PkgConfig) pkg_check_modules(PC_TAGLIB QUIET taglib) set(TAGLIB_DEFINITIONS ${PC_TAGLIB_CFLAGS_OTHER}) find_path(TAGLIB_INCLUDE_DIR taglib/taglib.h HINTS ${PC_TAGLIB_INCLUDEDIR} ${PC_TAGLIB_INCLUDE_DIRS} PATH_SUFFIXES taglib) find_library(TAGLIB_LIBRARY NAMES tag libtag HINTS ${PC_TAGLIB_LIBDIR} ${PC_TAGLIB_LIBRARY_DIRS}) include(FindPackageHandleStandardArgs) find_package_handle_standard_args(TagLib DEFAULT_MSG TAGLIB_LIBRARY TAGLIB_INCLUDE_DIR) mark_as_advanced(TAGLIB_INCLUDE_DIR TAGLIB_LIBRARY) set(TAGLIB_LIBRARIES ${TAGLIB_LIBRARY}) set(TAGLIB_INCLUDE_DIRS ${TAGLIB_INCLUDE_DIR}) fernandotcl-btag-a5d4440/man/000077500000000000000000000000001345145022400160265ustar00rootroot00000000000000fernandotcl-btag-a5d4440/man/btag.1000066400000000000000000000147661345145022400170430ustar00rootroot00000000000000.TH BTAG 1 2012-17-06 "btag" "btag Manual" .SH NAME btag \- A command line based multimedia tagger .SH SYNOPSIS .B btag [\fIoptions\fR] \fIpath1\fR [\fIpath2\fR] [\fIpath3\fR] ... .SH DESCRIPTION btag is a TagLib-based command line multimedia tag editor that attempts to automate the process of tagging a lot of files at once. It uses the tags found in the supplied files as well as interactive user input to determine new values for the tags. It can also optionally rename files and directories based on those new values. You can supply paths to files or directories to btag. Directories are recursively traversed and all files found are tagged. Directories are also handled differently in the sense that btag will attempt to keep information about the previously tagged files to provide sane defaults for all other files in the same parent directory. Only files with file extensions supported by TagLib are considered. Multiple cue sheets can be specified. btag will then search for track information in all cue sheets in the order they have been specified. Track numbers are automatically adjusted assuming that all cue sheets have track numbers starting from 1. This is particularly useful for tagging albums that span multiple CDs. Note that a single encoding is assumed for all cue sheets specified. .SH OPTIONS .TP 33 .B \-\-always-ask-track Always ask for confirmation for the track number .TP .B \-C\fR/\fB\-\-cue\-sheet \fIfile Read any existing metadata from the cue sheet \fIfile\fR .TP .B \fB\-\-cue\-sheet\-encoding \fIenc Assume encoding \fIenc\fR for the cue sheet (often \fIUTF-8\fR or \fIISO-8859-1\fR) .TP .B \-D\fR/\fB\-\-dry\-run Don't do anything, just show what would have been done (dry run mode) .TP .B \-d\fR/\fB\-\-dir\-rename\-format \fIformat Use \fIformat\fR to rename the directories where the multimedia files were found .TP .B \-i\fR/\fB\-\-input\-filter \fIfilter Use \fIfilter\fR as the input filter .TP .B \-f\fR/\fB\-\-filter \fIfilter Use \fIfilter\fR as both the input and the output filter .TP .B \-h\fR/\fB\-\-help Display usage information and exit .TP .B \-n\fR/\fB\-\-renaming\-filter \fIfilter Use \fIfilter\fR as the renaming filter .TP .B \-o\fR/\fB\-\-output\-filter \fIfilter Use \fIfilter\fR as the input filter .TP .B \-r\fR/\fB\-\-file\-rename\-format \fIformat Use \fIformat\fR to rename the multimedia files .TP .B \-t\fR/\fB\-\-title\-locale \fIlocale Use \fIlocale\fR for proper (although lax) locale\-specific title casing .SH INPUT AND OUTPUT FILTERS btag supports input and output filters that are applied to the text fields (artist, album and song title). Those filters can protect against basic mistakes such as duplicate whitespace. Input filters are used on the tags as they are loaded from the multimedia files. This filtered information is used to provide suggestions to the user when the interactive tagger requests information for those text fields. If an output filter is configured, the user input is then filtered, and if the filtered text does not match the user input, the user is asked for confirmation. In most cases, the input filter should match the output filter (which is why the \fB\-f\fR option is handy). You may choose to specify only an input filter, in which case the user input is not filtered. If you don't specify an input filter, though, the default input filter will be used. The currently available filters are: .TP 14 .B basic Provides basic filtering by removing duplicate or trailing whitespace, is the default input filter and the base for all other filters .TP .B first_upper The first character in the field is uppercased, while all others are lowercased .TP .B lower All characters are lowercased .TP .B title The first character of each word is uppercased (with exceptions), while all others are lowercased .TP .B upper All characters are uppercased .PP The title capitalization algorithm will follow locale\-specific context\-insensitive rules depending on the value of the \fB\-t\fR parameter. Note that strict title capitalization rules often depend on the context in which the words are used, the precise analysis of which is much beyond the scope of btag. The currently supported title locale specifications are: .TP 4 .B en English (default) .TP .B es Spanish .SH RENAMING FORMATS If a format is specified with the \fB\-r\fR option, the tagged multimedia files are renamed accordingly. Likewise, if the \fB\-d\fR option is used, the directory in which multimedia files were tagged is renamed according to the specified format. The specified format is converted to a file or directory name using the following substitutions: .TP 9 .B %artist Artist name .TP .B %album Album name .TP .B %year Year of release .TP .B %track Track number (only replaced by the \fB\-r\fR option) .TP .B %title Song title (only replaced by the \fB\-r\fR option) .PP Renaming happens after the tags are written, and it's relative to btag's working directory. For directory renaming, the last known artist, album and year information is used. Only directories that contain files that were tagged by btag are renamed. btag does not prevent you from overwriting existing files using the formats described here. .SH RENAMING FILTERS Renaming filters are used to ensure that the file and directory names generated using the renaming formats (if specified) are valid (safe) in the context of the current file system. The following renaming filters are currently available: .TP 14 .B conservative Conservative character replacements are performed, recommended for FAT32 file systems .TP .B unix Generates file and directory names that should be valid in an Unix environment (default) .SH NOTES You can specify any encoding supported by \fBiconv_open\fR(3). .SH BUGS There is no standard encoding for cue sheets, so btag assumes ISO-8859-1 if no encoding is specified. Although you can specify any encoding for cue sheets, the body of the cue sheet will be read using the system's locale. Only the text fields in the cue sheet are converted from the specified encoding. To prevent trouble, use an ASCII-compatible system-wide charset (preferably UTF-8) and stick to ASCII-compatible encodings for cue sheets (preferably ISO-8859-1). .SH EXAMPLE Using title casing with English rules and sensible renaming formats generating FAT32\-safe file and directory names: .nf $ btag \-\-file\-rename\-format '%track. %title' \\ \-\-dir\-rename\-format '%album (%year)' \\ \-\-filter title \-\-title\-locale en \\ \-\-renaming\-format conservative /path/to/myalbum .fi Using an input filter only: .nf $ btag \-\-input\-filter lower /path/to/myalbum .fi fernandotcl-btag-a5d4440/screenshot.png000066400000000000000000002621421345145022400201450ustar00rootroot00000000000000PNG  IHDR E(^iCCPICC ProfileX YuXTݷL1 C ݝ%C )b" &(=s̙^kUrtt8@DdHS =`O68v dwt) 0) @UP80;DwP\Ɲ;oܑMv|u2@"<6~ HD@8 0)|<ƼѸCɦSd#P80=ƚbMX3$ uՃNmןVȈd+;ֈA?7/^^8""ARF_VSxᇴpobwUFh]ψlh. VAY-`@f'@P+ 8! 8Y #8@-h-2=x ^)0>EC3 C4i@z)d9B/ EB^hTCuA}=14 e'`a ao8S.kfO)( %EiQP GjPPXCMPkh,-E"ivAcBt%ݎ@ߢ["#Xb1DL.Si "yŲaűhbSjl+&1v8q8].Uq7p ?'M$M6M)MM/(,:/un# ~VV֙66 KگAIp PrE][NΘ΋.MID"Qh@$[rU_ {RJ0G+SaG9;8_qNr r-ppksss_~H8 FV]c3 ;7̯O?@@ `au!q!lVW´ADŽEElEy. -#*&.&v@ClNCR{K+k[;={0}b|||7Ȼ5?K~e χ臔,P)PSa΅mFDFtE""GKGFOhYZScX8;/?m^BUD+ILIICRyɳ)f)gSѩ{f}fv&JKɘ4lȢ z]m۾ޜ̜Rs>p pr^EV~@҂B. *>v#G63Oؖ8qJUJOіŗM۔wVTب |ZeTzDމѓ'/=Upi3gkjJk \8XU_Py.Tc@zccO^#-&-d/iek-._xeW4\*zDs[~;Ԟܾ1˪[ܵs==UYݾrcfͅ~o=p{;wnսsO^}=T}66HQHcǽ}c&cX>yqg^Ϧ&&&''<_2ÀG#.Gh֞L?}8~YD}/z_::ۑw] f|?<1i_] ][V*Mr{c?lpnomo@)EHnp6F@bq89W~|5m!ΝDT@j`a&Qc[7,@t:*<"J3O|,MQVBNE^SASQYIROQR6Vw5+u tSBw˙r1c--g&lzm/*qu$;;Hraܖ_zWwoS_23+Aa!&ЧaɑQjѤԒ؀8xLx$d˩{x>wg r6AF~\E9 HCCE/Y>V;q\T̤ܾbwepDZŧN>wfvd׹M[͂-BZ]m;]kz{Uo߬{yi`rNݻzg?jy6Sy4gć/_:J~}͝wWfgek?Y_K _eWHkO׶(O ؁ @r2pBQhօϠh# phhiO:g+3*ϲe{;I6!+ /DhFȲ"rj<+OSF^OTRP\UVWIWuQQ'4Z4\7uFtk V ʙa^_ȷR&XOٴy#+~ҡ1Y]J0wm S,ټkMf'O]O 0 d |TMASC|%×":#3̢C1TXx$T'{,ތL,ެ}ws.9q5O/_XRЭǎdM,DC1e1*NV'mNyN>S]3X,k9FS_,ՄBSWyo2=|W^'CRG\?Y?85)wN|X?0:vgp=&m@8k@ף% cEK( :"7ֆ}}p< Eq Q!# ;4&гV)&ɺ&tXCl"v'Ľ Hm! Q: 3D18M@ǠPx ϴ1y,,WL&أ8h889s~ʓ+߄S[0EHO#Hh8IĀdTtR>doSSɫeӨFͰ草im .`e6 v} C(9N߻r9y &z9xe +f(fEH #y:4G[BsdTi]9jk0Lϛ-0,9V4u]c*VZTݮv8ݚ޺e )M /^z{-cO絽;nkDο[qnuwSg^|{96M!vY^[.ƳҾsc}!ܬĀRDAP TdK0'l'$g7Fvjfh-B+) Sy%`ͱ8, w7A#BK3'i5iOh Y:wDcb};C>m$G$YXXXY'Y9<9af.n1QXrVn`ZP_z6l5&6j#ƃ7yBrjԟiZYYB+gT~Ƞh#+&%5Բ 4Q'O랩e˫Ǟ+jkl2ݚtpU+{xp(y |2qvd(؍Ds^x4]{홅ٓvW~^rqҥs߶s*p^i_D2@#[$L EnQ(WTr#-EW0 LuV`dqiG4b4Y4o˴"U6q:6J"?^ r``j&ْ+XLXYk\ })j߹3x,yّX_?.@EN *\"hdxdO_|ݐ')(Z!3DWf֨^Q 4왅mk ;=prq~Zﱲ7Gwٯ*@:%X*>T,1B>+~ۓgM*=/~7ʾK4p^c§ [_ZXXMx{jkY?I?Oc'76*667o>ޢll-nl'm_ۉl :#W1pl^ެE 2?1n4ʸEiTXtXML:com.adobe.xmp 511 268 {a@IDATx]Uo>:i>$Dҋ  *EDT҂"tB :$$!̤rZ眹33wVs3M>NxKK*KbX:z芥[ӢږP x".y A/nYZұ:U ʲED@ {bD(]^trF5 gX pC|SPwbDZMA߶?"a2@fX]9.r$e2) A  \K r8M>+- @í,&">A8-lEs`̘4w.2`N'܂F%H T`_\+v"p"Eu34kx[r8j" @+6)ǔJ.iޘf9j*D 0eX tB$"qLvMɁz{Z0)5SHE<ib)uИL,UX,Ģgˀ5^ xtS3%)dG_dHsdwBZ<,c\* &on>]XLl%rÆ(T30,:-$T[ Uh}J'12 )qGDTs$2 T<4s@Ϲv,F2{FmeJP`.+vִ޴I)aH] Wl@*.Y2b0pq9qd,V;DV܂ $@3A+]ɴdnhOIÆ`$cM)ha)!WV"";L^R^j6: p槙T]23HDY8t0xAK/ʞ (ې#o+eY1]Q@Yuؚ\#0Ya)kn AP"#Bcb @72!Q pejq:N)퐖ski |+%Afm:R[x5kӎ%"imQ8cCNH>2#)&M.r݊'9Ȟ[$QhO L^ !r=ȸB1(gUf4JybJŏu2pM`W;զ)۲pxȰ 1a'mVZbmsl { pbAT&)>pG4(Fu-`PD,Xw= 87@ybaG LQ3-RkStvcKIҴ[ȇHq{W8q$T/YkiPY3Knʞ@n񈭥A}CK/\U㪃6mX;(\[4A3st4h5HeKsOfՖ:ŌeZJ300-DR:8i Vywt҅us*.BɛVi@tyf@ܖ ^մ&ʭ>\Ƒ Y>p.Z”)reEm8Ě4.=Wh&沆z]Ir<e6yqg%+v< /ڙȍ)BWZl|)ےWK8j$PXfT*! s!s mmL< \UWEcB3EDq#gf ړ\ܚhnیaBSyO3j(,bd[Һu:qKhłVl/-`K)sfU-ɶrVx@AÞ`&H`4ߑS)v5MhK5ԂdJX d+s6͔(|(cJYlGS ˇ6GôfFB 5\)Rԯ6f 2gW&>2Xnd,P&ExBa˒$3T7*sr CP2H:4S5WkUSݦ3qR4(#`di]#ЪK CY>=(Ѕ"i>o11̂|qbsN] &Sdڔ̲܆ @ӻEn*S: ՛JvQ{}b Ȏ/Iad,;-mf0J^(e8Z)!ySb;UyAۈ"Yb2ryQǐ屙E qVV'C.At@)1kԁU$PpSȧX&n[܎g`X?e;]a{Q)uj<8fW*I<PV#sLh$3^+& `"8z 3MUh2iزu .cMWr3pĔ|l댺#Oԅ[0jQCdO sk6¸4`(hxNB6iȩ@sq n<_6-eԖ&)maR:e9l]@&v̠,`롹]8;GV-).]41likk3JTq [e =jƢ/@d%vM6\ʡ\S)o j1r_]"i c9[B+Iy1H)抬4t AycqOvTa¦r͸ )c3LC'İ2O^(-աyXo{ù? B1r,0,* `@ zL@D)¶N*1=jDM^GZ\&c ,.~ʤˏ9Nۻs)*7BCr,cY,˲ w'H@,D0/%LfYW(Rt"]+Ҋ#™·"_gS4lEI]}\nD4+2K=U%'#E2C,3e[?(PHuk8{(2i̜@ ӵ(\L,(ST@L1Ayc"dAH&A@!7]t@:LJu˴?k}1ѹ2GSDhK0@  `1zS)t9)If:#+ 1<ܤh:V_ثvPsa%Dr je;8Q.<#,XWVmփ*zWeLlsVi  峋B#:wܙHD+yAzX_8daOQ+(mYM ĩܺ-rI 6=/""{B E^3t%DFJ Z]g|YǠ]EA>6s= &bN %~%U# w"|{hj-)vr6M?#C'jXj&/9Iʐ 7G@ڐaHpDqINv]u&1 d̆u|I((s54 #z\qך;#{%uZZd Y(BOӣB^y%@')vXLC+ ln4:fYwŞ`7 s()~LX0lذSHFk4e9FgD-"ZIbriVp>̭ܒ i2`(1&~+]J8c S>4RI8#NQE^4H%L-k E+C8q)M$E 8bmEҢ)m +md|SЦeDM6.d(sT\%47_#b/%Hu".quR/wmKžyk(扼R*0&@4u:VPZ&υ]Ғ -ι-a *RBONa`G1a9UtXsW/ 3 u3D8F4,K &8mW)TNe1W>E6d6թ@1D#p&3jI4Եn7a0^F{aۖQ; O:C[8rtzZ%mҮlr tarE3$ni|mՁ h eۿ:#xg\l!@Xw>B6hbfF#xAnۆ!otभ<P$hQS*A+~暦el$n Bxn $EaQf4CSˤ%B9{\ji^EhGKt0 b=%\A.SMHTLx-a.Y<xپ(ŃÏbh=v5]`&^212*{ݛ6_b5QuPA= osP R:p^|JU:9~56AhIfh~໥ X++٘`k"BGr܎8Nx8-r_O8Ɗ%EvMD ?=6NPg¦>'дA,|(џY9 hngÉ"فZSOD(-W6f![<]L}G@ZhtPl>߄T? |et DY BX XK\OPF}먩);+)v4|VbO3lQ I \=6Q}ý𦄧|-+{gox!c (h °-j)($[IV#K&§ĚR俇J?(܀bh.! Ңa'[2 1| Y{Nż8j|ȇ-R ^M{#hBۀ9(cv@FQY/'(Z_-*Şm(nd E‘ۆHd:gIk4RQe%M_v) DTamJ GKJ y k@i2h)R"6Iͥ15[FGC|DH/Bc^$,h*jS:ؼ]@^G( !.tPKk3 M/*yU*;ۀL[`vb . {~R3M-D[EN;AWh4PWR&':gwXMwdr"԰5$y%y&e|(M9fH^&d]lݖ4auٔN׆-S,ja a& 'JO@Z>C$B1StEt@L ljNkfӍhv8y$P[rڼqy1ќ"WrIˎ/۔,#i]pR'tM?%M{$ 4LIG>pUrk;$gi_R1ks4u;4J+?WF&J<9LT(#mRlUKz>wu(lvC施5s|Aʒы7if2-kPp^chp\Ƶ`A8(ZL`Gp`m8'm0vNֆ+YlwQ'׼#P20HBr` OW&:lNDg\CCT\QtNüıhx{DiBhu{CYYvWĶs1uc뮞U ؔT]2`dUWcaCl@k/$àd.0\S fͤsUyJkGiC 7YEf {et*..t ԑH0 ^\* 'b$`/ OyplȎT!V x1gڣve±945fv(]a9` 9x*vQB Z+1K2V)|79m[.0 $ ^-{Qh#vchc -!F`xjOr ǡ:*dgjyK(utω!JRg "G{~'8N<5Q6ʚ~z6457^fD.CNfHxFe(K톜ΔwMPq{#uŠu .k!3BT":fX;H]C8)#t3>Ois&79)4]0#1@"ڵhtČ`v__7=h`ɭ=i-ǶXo. Oz)f7O.HW&咈9ӑ2Y]hLo"  t2|8.KF N885 ̧ ua:rs@8H;ǙEǏENՙ>O?.!p>_@˦ ̀F2EGԉhw."|IO/DU\}Mf -!"OShzW&| Pޣ@idV(3b #ޚ*NxS{n(e@s@j8niuӑA@=h.h0!#$hh嚕}Ku? ;O6%QН?>iR<9Q 8s?Zٟ.x׿HŅ/Q,MLbϻ<07ZK}gՒ%4 6dNڧ[߾4)q7]e Y|1AÆ0j/wՏ^Df[k5o?k9ʗC ɸ!gaRD,ػ53}wE8]vs.O#ah|C .f|Ġ#]./HCGPUv']׽B ϞiVZj{X[qō,Ba.]r1G%=`9;o ..ܗm7|3x$BD~-[48!%= GﲲUVnnQQqCtM|8鏼_6-_Ð}cD%ӵޛ钮Vo.NGZ~wIw@G,0OhE424#]ֵLFEMqXq ٛrdd_7%@ GƉH˅%@lwԧwח$^^V?.qh 80vǯ|Gm_t\o\4קMo}/z$iKa0iӼ&_켪Xˁ?E tjV9=vAIW>)>W9ͷwqr0ոx#Ͼ=)?3?-c𴇚Kz-ͺGb9r$a3@T[6Yڽn>~_^74x| >;tG%%}!I(D@|V7gw㱂=C^a`ߧh=est.{8lƴB&5bQ\I^@>MD]48zccH[j"?G#]N9aB tuxN.#G]?]kj&창ޙ>ݿW1CHfɻRiII&e|o4^ZߘNg? \'ů4wSϞ=x*м $|'>&-˖Mprc* r1G!p7.nc_kz%/&ckuK5'+hz3}w\--]ov]?6ƺw&z~7B V לsϻ~67o0Utt#Xc(j;z6 IkCar8cF;;zTɐAɥKc;vɏ:fo/9hI-cq!;nUKѸtʞI_<ny!fyƊqCGO:tޜ;~'@(pK+B-\ƭS;(|7A ].M'\'] 6aujJ$  dzYr-aqX9؅ɒ0g]CC}CCm]]]}}mm}}c#:CBSOn ]>؋;]jjᣏ@YŨdXQs>6Cg=@ͮXҧbB=KOBC}>4=TNtEoHrzK;09Y?k߽kN_]v9|y;w^xg DkL}Ed~|w~v>cZ^Ϡ]kztL=a¡x^FhΧI0>`։GsxNrS?,\%,ţl4]j n{ s_S{˯uFki>?#Ԉ\KA@}ӊwԭka^Em!hOwJUoz̀bu(O}'޸W.ٽ)V'UN?`{ݚoNϰkIߚYYmCM"mP^dC]VhDbľgo$G{j){ѻYVy_4kXvYΊ>'-4*Wr5U-9kXVR=j-Lh*?6hz +֢M^8vB6@LDdP|L-+_T[amOзQ=n{?_Q]èUeˑ"8ۧj+ꙛȊ'xdbtLUT]Nvu{ x& TS!ސȋ -0X붥I3=..FMiTy(D >.4 抎[?Ћ$t>P0)n]aRԑ$+5'hs?{{>3}8!lI,c@b113]|AC{jtL:bG/4,[ϖXEc݆|dfig^2y؋7{-3'pdA{~NٳWAsW^24{m]uϪƏ䓏/=t`~owz}.nX~٣jU|_#u0|O_^۲~`ە 6|ؠ2ܰl}[pyo9MمɵS%c㺇ngt䮏_?;xxvbnӽϸdaN~_.6+QW=NQ:sᏝ,]_0+sx3Mz;~ILt:_beC niMTaO&{ښpJnƪxMMK.='0{60yܩB'76ǝ{wkGĔ_|{w'ɯ]N~ ۴iS|خr !Ļj⚛zd%XJ&us$Ge  ~̘t}umc_zɇ[߱$ Q![X2zD7W~]jwp*?rKS\{Dfr5u/yfƒJ_pcyKb) ';cwgox`n|,܎GDH((Y^DscKvnC'KiX]A>k`]U.lH$.^>|>zٙ\`V8ZJO/B%Fн4DӻD-6Op@=6yp9DqoH};Up).YѰaEnݎ89w>:#byyDT*?Zxp zl\P((Ų 7$ͱA,M_E}OW%suZmfm6ٸY9i۵?hOPc=&/A$*D Ѥ:Ld-I# | *p"]hoT @D I< 02[ץF&2Es|6'FMWwHfd[5MYB "Ix j?5qWw[/lc~tŇȩ'Uņ,Sl +֬xyE0y񖆺͕54XY;+'[_ ꚺ"COquӧnV5m^pU._ n8& iZ;Chuqq]v.}!}SN?vdw[L#I +{vn&*:WVkH>laR햳Bw8{M,ӑ䭭:K%fdz_nl`RyՊO 0>Aٮ~G;xJ Mfog"jÞ Y3cƮڨ^ {t/2c?/-_QKAH ]g5U6d%sE\{>I209 ?0'%0-Kav7`<6֥ ){%}CJvXv٧O^+g Ɋg }G,Ջ #Fw[o@=S-&Qn456$?jR󉉺͵~{,Hn5u^Vr]椟Srtb5xgx :򼺡!?BW еkʓE;#P[K\rO]2Wͱ BaFk7T'ψsܵ̂C@r9_sɭ9!wd_/P]ъDfy J:![1'ԭX >@ӦǞdS UODěGH>Fucs)uQŹ`koi^?߽~r<뻻?VK8{ܤa޼WL\osVAۏأO""u7TYvO,Ofgd$6<=ӠsIe-yT.䉥}'#V?>86|"Y94@IDAT vLd~ڊuRUMZ=wѢ͹9ONq10clv~n>W]zy}s=>\^|ej{SF)uv܂]z[feŊTOUU?Z1އ ] ݣ{A\ ?i xB^4¬7zH-tAD4¸ޠ%e x 1sghCM]%8 5]'\ ;1Nd>M"@(#&ra"ml<|Aѻ"=^Hr )57 QKC?fN>k^Y]pDu}|@i^ޫ3k8G3^%J]aҺ947ޞ7-ګFCqߒfM{39wX>g_1c; _|JʚƦts XR}̉GH}^5f!{v]koo.o\w y7[G='џJfN{ZT4-⋳Vn4jNS^}eiG[S|mCξxϮUݦiɴYoч_pޫ.ט/:*kYZ9~ƛZ #jӔni^[/;oTcmΗ>fzfפ.8l_pE_{VLډǢk! oekǜ=vKj-}8DU+v-կrɂރ;={$k֮\_CY] mԭ_]WZڧvRUJL 20U[7%[6XU/:oSe]V^|iEK%ݛ1TQZpt+ZTd}қݥ^\H67&VՔ /ͯ[*6hbү~ْт}[j[EYT^QNGKVs ԫeϐ}{&T:'?|UN^C5us \WVv) kbJJzjѳ]9<#UO>5p@S}8LsTK!=j80NoXΧPKv!%6&WTl ]t7fEӄӭߠV>Cz&kRFMVn-D~O+7vk,X= θW֩7|AztEH>{ɖ6<^9FvP@bZXQ+SdeZXXإK éw ܂ۦ@3(ܳu5/iwԊ#eׯXXV+=ܴM?',VdsC}cO',>UI77425}0O5}}".9ung5@Or0}Yop;eg[f9O|m Nl|Y VL> ʖA/t!i` ]ٶa6$=.]1MPS\ݭ@`"DtˆEj7Ⴣ0-rս1$a}!WѪ*饠?QȺ{dʮإZ#zš}^DQ>[4M{ l Pv[f31`'Ѓi]p줯G=6$&c8mn1/6(kWl~ xMψ٩ޙ;l'moDɈ~3 l qК#s/ )N:3Йme@>wvoEt/ .vG\HG~lg[I @s"Qws\Cv3dgzP:ED}k¯vh1XW]],e6J)ZU_ۦ׮pBm4w6RkD`3n?=íNST ~ꫫ]W ;|VggM`MevJo4oG~6yA=B=wKq}G1h5/, eW+-4M糨~޿n-%2ue'\Y]!uy|-'l6}_iQm0%;m++/V>gYrwi~lC2]_=:BWڽtlct Qťc\~͗SЯ%5P*e>ψ1G]ɯߧlzNΘˇޓG/woa7[n!q87p귣ֵ>v';$tAz+AFAb')36~4w;Ƣ߉{-{ ON|gRet}㷳?*:Hv^{OX2\5)fvɬ]c;p(͜_QY[I5}1C [+q5mH~8/N_rÈ [0wκg?4~ꡪ/Ox{&=u_aw輇Yu }g?=Pu.zֶ2l5.?Կ_ȉFLg3_lmn1[tc$˽mM:c!c?mZ8rr}Sid؏~|~ݣJeg};)(VPr2~Oӓ7ܚ|2;/nM}ռ'Id|=!c2:k3Ǫ˦'Ỵ/q] J5BD㒪83.ya'OOnNҞX({:'Ђ~?^ېb7RG9^c^YgtGGvF5]~Y2lƠ⢂\/#rw&V}R,V<^y?6K_c=w]Wm=GuցJ]R4d¡/m3Ǟw(?L[.T'љϜ&W|2l?k7>lwpžooAg~ٷ<(eȞ.yX?^9ɷ=?Zy'&8`٢g^[?I+|Prmc)lWp~?:iUC;G4vw~yߟC|Ų%,zк$c?uE]4L9HlSݦwv0X^p3*]AFiL֮v0gqV͟rU^ߺkb_:KUS|܃ԍ+.]PYIbOo= ፪w`چ>d޼WoH `>*m?[ΠZaL;V`#I~GzUKhNߏh:,w0evYٲ^'i^Ⱦ]\+8sOܛ Cs3aMZw'u3\p& c\‡>Ժ[>b_M[,~3plߜtli:3 Gw[tjg/hoO=_ %kv/^4=7s${1Y]{'[Aulijvaۏ(fM]zO:kS moX(Cv_=7u.5n]hr|o/:4ZS#UXJ/GJ-N9n{֛=.S;:xe_>9鼽nӼ+qfw뙑:"Z{}IguVe•djXe{4ҁXVQ39btN:=Snۧwر%8`[TB.GHHքx>ǶV$^x)y7I %dZ?_>>x7Olv䓷 {S6 mp׿%wβWlhlK{*vacߡB:dvf}4M}Du{O|Lԯ|>vU '@`.Y};Wt×=JRu8+ۗ6_s%\ OT:RiG{zlYٻw>dXk*f,;E;+)זgqy曓PT=gwt˧<^zo{6bgG2m}`"Z~wȩzƶVR]M}P;tm\y5Yhmjih;+1>ՂaR2;zZ.tV;V%Gtzv66Fd~Y|^}wKWϿvy7zig{ b~df&QSgWtYr?kgyrwϞqnCFv6:3A|㳲xחS~׃?gk m~W ;{ MkX= k ͟>};?ʂFPe(ʩzꑊ]biOܩm=F cv?[<8[ 3_N9,{kW[z(MeK6 wɝ'xk![FW?6ږ.dLâby=o3WD0aMk{zȥaص/-k\=nji_ۇsUyOA 9{M}]ʖF2Krw;^̧nݾs|(*ۛ;h} p&풧eJx{Ͱ4~{7~spZ@&]ph,vxݷb߸~C}Vaw#<}CG ~B[?VϾ)6iϡeP$6g>` s?p砯yί W}}׭mDA;=n6_sa}KG3ُtߔt^Gw/y}9Ec3񾁼;p׋Ffu>#^=}#'TP2|w/ɾ?۱؈ӿoSWю%;]1v)(K:~|^!_R^v`sy_ࡰ[lS's*޺tw}M_,C JJKϼدY|QKF(ulayu׻%m&CI&ҸCO8.3)rbK[3y킐\7CJGaw뢒 7]scťXRȅ ! - %"{S@EEAEE}*DAQ,(bCH =@ȑ+3{{!|f۽ߵ]%=#ǀ_]K*ֺks"~2ozoI~jǂ8.d73uI/{-@X];{iFy`؀ߨ;kDt p4$ɃI܁?v]qWtjkO`X@rA,#RήlgCYh!‰6}GLkYC#\3~v^i>y|W!g]1Nۭ9ggFvk F3''B4}BYM9wF;}=˧\r˞]W?\k<.2 +x=N%V?/u֟O2WyyCCg܀^{Z ZP,lhr,Rl_ʋ_dɺ2-zӽe/0X/;/a߱=g# apO." ^t(n+h*= XS[ <6Eׅ(o^ NZFė\zwRn[p4t'ZD=i>/3}<3 >y=*g(gӨ-5p=heIJKԒi+[ͦI!v{x~O^N: I,leĐסIeOyضٱs',JfHg۩#bF1DT& kѽͥ-wiѾa3(1N[4wK̭v^k`oH0QЖ];ZxjOW8E+0Ub|{:y7']k>x^_G_NGiT,!_ƚk]5 ~0-!"1v&7<ǹ8 '@-%aǖVӶ8 ~2"a̷_a<4*/ 6C/L[*tPҠm]"N~y7y+2L͒=:`\_g6ϝly|{z2u8Hg<pV- NX&g7=mc[^"1t4Ydo=e:v'2ʟl4Ow EoZrJh|]r0a#mQ|:gm! U`5!V1Tܴ*}TȲ4,KŴZخoմ`2V.Ju܇+aG`RjxvKxTr>J0Ya\: mv}0a"ioޜtt lq)=:)nA̓=`)芮kX| I~' DPxbS0uyyyFXE)ohQccO-~\+VY3-7kT>Av aG'5fkh$C׎w>ؓd?"6izk5#\B//@ޅͿ} S 3,"3FȺ-hn7Eë̮*VL)m=lHC2t9HyD)0%>LO3 ";١`T~+ 05*~mۑ۪"tyPؤe{fp 'Bf+TձHsv|t^pUKI& N: iv v<ұ#n>e0;~Wn赵2_ \ ݲdSO<mN.WY*hwb>VZ'RE^Gi♌{UM 돶ț]4X?#ßtVof]exkN=Zr: m6br3ߕv`˪W+G5֑K\l]#[yzJp6x,p_+1 7G׬umTז{%6|f,?$qPs'{mGVM3qh:Ф7̃ )NgPi/PtnHrz_~X.IEpPDW_fŚW|I?Й%xⷍȗݺs:!h3t?YٿLoRF} DQsJ:8:Bwp>3:J'0v`g#8kke<ԅH _"d˪;^j\9wvf̸w`Z:CХfѯ-#D9-oiKo_xEv\rx/>~K[V&x8Ύfݦ]o$\5V_ӱʖ˛::@w88\ <%CVNX7gU|pٶW6?ml$Y:x7; 7׮WpQ7B[6{yـiyuw|B?/]/ Y * 1j#B@8>*<*aU]aNDa"ؿHN|z-/ #bn9i(Ɠ<1)Ɠsbk'K%32B mg,L @ms?Yӛr_ ip8cuF.iNxr3:"0MY񴽘U]qV12J,ҫ7R 並vyBBy>C0ӄzErb("VDȘjO 'C}OSRŸƵøztxB*׀Pz.fΐuF 8kLj rYe8.W1thW!@. Gx p_V+Dٴ>MbVzsfɔ}> wm+#(*BF'~?MUAA֍Z|9M:]NulǑ'OGѺ"~}s:`y7kQAZLq3q) *hDZO-6C ρh8spp.8YfIػS537ZRgɬЈpmDpcn]>y܀F#Ȉ;ol0#Аe `UݸDv0SvFT\P@!qy Gr࿮KݚmYmx&{z^4lӜuW}`dkrj6^wpf)g_k !g\vCczl W/x勇m$ ~' Do|]2:- 2fkӨaF*i]dy"Mxcɠ : :T6kTh`< zܷh\|.JM:5piZqU6xzԱ9{Gp[(oa|xIQ}ԧK=ؒƴ/AG֕'^ݮlݶx}1yQy`m>{XQol5s=M6NA!\5}u<~3j4 y]OFd Nl賬Ps+E0nv0ڼ>.1x^7-Ozvvkw|TfӖ_&oSYv,: Y:=M@DGpa|LZ^|B3V_!*1Lʂ:y-iP( ]=>'i͑7<ΜP6?kT5 Wӟu%c@rB 6d o,Rc#`7d*q*`qdl}@rTm]qyKL|9g*d#f"˚],;;yb}0{D'-cǠ_ԯ5*(d SoH]˖&_V=?#JѤ6SXj" o?Xp;>􅰗 >j#_ڶG3gԮP~@|4{㸔=ϸl0`?ByXPTa,5ֲ$x,Os"nQ%_wo⭣J_w;xy ޅ;݃LN[$-U-5IS"5OIF,j@ȓxv<' TZRݶ`6G?qwI lJϲ>'73L<}p,oATrxow#h$T] 2@I\EU2Y{-Dx4O^໿k{[ y7XS]'deJDfܨz!S %ԋ=mdecErIǵ#Ab{t⠲"I8`*=C,~S3˜?@as7C@MﷻsK NRv hrsdR, PQbXar?qT}i7Juxf5*& C.`E( ka4 ĢBl "fZ `y΋ TmI4uig)=lv@1ìz*Us^J{ ]WD|_  T Ho$5#8bnX+jCou:|1~lxrJ[ }/9l'+H0mb$KlU*3yb>{E&<qC%wƘ .r|ĄX?q yW=ݩ?XBxTZwuɄQ uGΥklqn8O'Q[ap:q,.ն[?g|wٳ΍'m^FiD^@m7\o?MHݫ&MT7;u<*ybj}.lj3 !rXpp4t|HeQNӱ0tVf~4xevlm|JGu սgIOmkbXBqw<`Xefs\a#LE^BO vi,(StDetV:ҡG& JV/oGZR byٴ}S@LY4Dsط))"U`B_+Gw=$p_Yt@\5_y}5?޴Ё4O5zrtplvp2<CZBV7+Glp8- O*.0x9ࣕgݷ߮Y4-pt:""%[ۯ}nD58OFƎ)}Κ}?9,LnȈ ȈS*wLf0w!X XQ!.#wض Sf1'&sic{yϱk|MU:#x,fC5ڭF.c%uuܶAtCe_aDW^aGmN&V_GrֆRs@|C磀7Lȥ<&r'Ziz7(`G>K-[-*$j.o;Idtm ^,&~f(5901MpȄǿ{?oMߨ _}J( %J 0S"@WuvE[D(ݼnb5":}̒:Y?ĩ?>Ä\rZi_=N yKf̀~Y>%yyGą g]>>|w@_ ,XZm5?/ֹ͢+ӌcL"\U?2HxgVH]Ǯe=au^1۟G/%gN %C@IDATR9ΝJ?I\ZF$cgugѪGk!B^,Dg-~Ԛ/_dE=v?R8DF/س8խ#g?Wb*Ѯu/;5O*rUIbp~My]ًddg*`G]#`G#]"Gd@S^sח!G=usǧ,9i\FM9w:=''Xg&BW3Bo:_ѐ#T$j#=DZu[ <x:12b.hzQ:s71wB*׉]_Vf^.&nNEɑH["C#80G sJ8jM2BiQ*@_w_/~Ƀ|/³kޘalgDXjzWs"A=|@­;;YًvBڊm֟,MyWE8č9~ʃ1Pڨd^2W!2⥌R6QV+.#ݗG"׬Ѝ=1FD#*L]рQr,N8t\㘹o28d2" :D9Pv O6`4eND_nCؽb'xH@]U[Ny M pVĤI0~6^ u6Z2]+ v-аJ^ v`D*(SNn>8 ;i=[Ĥ{˻RȺwHq; N\Hs<'R{j4 D/VAnU-.Iw=8bxl-l9|JwLGr5:t{f0q49<@9|QVƘ>= gK]o|4nZP{冼bl[( wl\ X|~rtE?yy,bMWnp}Oc|rڇ atªJ*dN{؄ۻI^|ǻhcԹx':m|Ću65r>uI2~[N!m1 WT^A1u0tNPTBKɐ@ '3A?JdwN~ZM] :IVټ˳f32: *\tJ}G^b0!~js;WzO/MrǩG!ZBKna 0+8$t5W`wqIwC.SgxCwCHT\*Nϼi6HDxBF~ s+a!('/gv2UtB=Co_kx5$.40O7gUd¥-MmC\sÿMWoy<~z7F]oBWGΐ~_VSP!2iĮ`Jgt쁎k> R am.'=aIH*dİ+F_i/ᰐrԒHTK40;:5C XG Ʒ5}XgHN w:9 Q:wlPa3=hH-;n]BZ);{誮-JS#8mZzϋ#C0n5VQ\7c+9خq7WP0:% \|&U08ZOpqΪ*5sw6ⵌHg&k^Dr'P^8:dzR (՜m6dɘ-ϓ0n! Wxxp=5q/@L۱c)Q@9yzX{+:,5 @MȈEz{Pot+,']N1'ˈV4aG'm wSoM8BE~幢r dg&jq$p "OFŀ*J`@ߵY YVs2nWinepha&);>q&ۑhԎh旽tvTµ?|OM/_ \ѯ|A?%5K-]8 st;Ժ@krlwS\'QSUxu_b[8/7s/.u Wj05YR*0pE/tk8ꀭ[\1 FӶxLT#nt C5>/xl{(8 +7מ8gsy;r[pt$6!2Ms^^gϗDg6\íAwX|y&gy yvFft6 ؖ3c݇wbUudxR"Զ90O}efItQQR|9$wߞQ:_$t^;N#1{,GAi0xI;< ?rA^bALB`A-LkKE["}A`%*%txpuA^-!J6u8Gbj-AȝV5g??|n3@)e<>t]Ǒ-Abׂ:[c)s"E8WSp巽OMT5/;[`sѼۋ ɁGUۓ 7H'UYvΆGzU|vƶ<ھmO6 V{MFFvpGx R.H~~G^ms$qU(*Y([L ;4d"Dv  S9YFqu/MxpA 5q^{`IwD=t`!"fj~/^0͊ sw⊰g w 91u/ZRA|$HٻiU?kG}X2WpÜ!)pH6q&mYl!˺˶-[8d,3yE 0r&(Oh=#[<3m%νnz0zx'쎚-'f1b.Mg.P3F]ԼAY>:D1cuzעw~HZmZdP[!_Sk\\|nӗ[8Ѕ?}y/ +"۝kd-7۝ ;MDD^ú/ElT_1>WX-~3 TmD씒r[sG" oؙpgPB:ݧgD阒: ׋3L<=TzڿYͪDGFԺk7HxǃR ْapu!CN벿l_fS|DŽ}xS?+/Trm\9i/kS'>/{ߠm][ןړ6!yu~2\>7祙['%Du"\pX;in|뱌oy{+Y:JR@{R 6~zSe7+Ũk/]Q>]XxWQ)>} 'I>o\'s@^XK(Ko䔑KZyw'ǾlpR/g#G#"\`>?{GV e:rYb7u da6ZGø!>2}2J҉Qr,J N=1xċaGOL Eo# ?!OH~ 'Kiಂ ]TO*@ f̂V%x,~D}k[ `g"P% k +}=0 `u^c f)4H"Gd-jl(]cq]9W<0%>@|µ>즬gL砡D ~J&קeK497.:N`>Pdb=: xP谱H/DKPzA䜍&K]L%VQK`]AFq IUGUBAcۇ R@?=wZIpݏ!?v<!u~ |Q#% -8-xO;tvf -;8 X\EObN? 8^^=b_ftf< wr<.|(Pí5|ShNʽ"kT>v-#A|/[tsp |EY |h s0c7/cB!:PyE@Mi#Mz _?X(fޢ9?LAhfЬ]~?`}mYqhPcܹL\p:T~5@o"ߢr\9è:M6,%c\s!ɥ@>(m?m:)Z+jB%@p˸~XwrI1RjM @pEH?ڢ'!z_o&٣O~ܮS:A$nv,QE*Z@ m>Ƕxa4sRD̄z]\s'2[8Δgў8X h}@ꤱ3 ENٿ<J:}AAgˊSpUx)6(29),YXZz.uPbQ$vCW=m3quMV윒ru0m[Ml]8zTHZn]量9߬x{aЛsqq/vo޽lX)qNϺYh^W-6jƴ'|GbKo$.|pFDm,F+ֆ-FoLf}g}Z=7 ^٨\{ øvԟb/b}vmM;gB-%[ EKB.7ؗwPaꯧlݽ|8.Z P +.s:bOεh,bߦ2 ? r?<߃vrNp O H&^ԗO}*o;fnU" #_2\ OcFDFQzu 6F֭^<9ōA_.y4\E0$"Gc0*4u ~ԧא>zM(vŠKUp{O -yx.GP]܄4 {#Vi4K;?m[{VF-<.u1޽iJUq9$,Lkjdz-D蘻l[\PKK4>3_L>dȁE@E`ڑ-dH"b n,%BJ_@ڀC7}'<*tOܙÛ~25Zi~9OCw f%靪ߗ7:kM-^~p/ Z5GdC+TNۈ}>iiC~qsOÅmawX: :pȧfkѩ xv~c R}P4}pn&Vݲcе9[ /.PY -od79QrWyYltIQ4K1>6V{AzB4ZW ^yHm//z c7ؘk?&eABai2Bk]暵dS"H"z*LH?Zi?{{InC>5n^Ht"j)Fk٩(a,F54V,p0NRBwCq ٢Q%* v*A gYLi8",VtXș[zo ~&@@~J5ן.!y4qx<^CdP i^0 Iw8c;jd¥?dQM$ã>-&рkЛWF%6᪨Fߤ׳uï=07`GP/[7<7WIjܶߪY=XΆaou0[5bip4QHL?>~WD%$@,8Y`Ij#~X%q D+Mi>{Fz4?4uï|XCT MOY@R6vߧStuV9U" ' S8#"?Ӥ8gsYD6ntY>d4C%a?-c/'{? xv(sr]5zXrrNF7¨Urɔ4 tjR gBS?U')#*#[çCKآU=ۇs_U: P~"@E!eRV%^Cb$ :ʐ DT갆bZ[btO!fA-0R` 4ufF_ ^.Q>""x/ePFuֺ3ƧT tZ-p+ezIx4}f1/.6G@3IE4{AJ_w8"p^ms-^uӠTh:j4[X @@zІ45l6 w6~R|#WBd4"*?@yJ 쳨˱/dn8+e45WqN[QI+(rEttϬQ58ss='z"t1(RZH#NӁI^Nb~o6ٟjttK(⭈Ā̼󠪏&\0BGo+gGEY Аc*RUubSr,=4Zf`.J\o9f ) _p+PB-Ft&)G~F {ï$qr 쇮 KC,B](!q(N]>L/ {]ťt8&1At^pXd!S*6.$]:j{nUQ3HP $jNYR 0J]Tӟs9InrVan(n,UPyã;HC@DlcNP k?u*cJ닷yK^O<x2:1}>KCS]臄0W(#7Bi@|O.o9ka)p!+H^rOËa9V}\xRRZElEK;H$!q5ylDMQoMT%qD'6Rb*D6JEmyVs߭<6>?"c!g0# z& D+8~>5o8aSFK#r+;cV_s-og'+|ewd+uxya8TҺbxIXGz@oiBoW  ee3F)krJa|Mb)t4DaY0aɝ+TG p6&PpO9t =o`d?+02c <)Pk,bcәCm +/R?TD<(XADr&d}ew۽fΈkzL 8hDT9*ɛzc;>]Ms~'ipVz ~{ޟv*La emh3n1W5} v9hGs)o8+'#;`G0 4Wsoʾ؃3b2qmĈJ?G2"D4G}+" {Y(=DUUDiN[J*Qf! :9@|P"esz(EbVJH@&sDž븥 ~pC}9wD$a|.ݼSm_-y0Bpg3o *;|$>j\~xrrHX'"#K>, _>~B>gf1_pshwj_j4T) 9q>Yɕ/aGo3aGAJ%TwK@ds"P\Tʖ&tU*$?!ӉTD^y ҌtA&P٤R kZjoP%vB VhꁂCEdddAuN%Z/>K*XB"K{s+*&= uX<U_Q?uYE#CˣX" 0|S-=(*8, 3Gum 0+&5 H`P>pnk%T$L ;:EQWJ҆)Jᆄ)~uIjY"U/o}|j# S3Ҩ`[͜Kf%h@lVV]P$w(LZYxXZߢ]ӉPWtK%dɈy ̸gО(Lڮ#^.W xO1[ 4\"I]ֲ Wj#P+JnvWɄ'agt_I8 R γ/~B8_uˢU%cH<.tv`)(O@Y$^b7yK=2RM^;M ]@a0dŸt1BBi,C"uM@+tUιI@$SpW4|Hqbg2{>́2ymjJ>/V3xEϓ.הH1N 8 qi鲅݂7v"pCUr:WK&>iFE]=׮gl>Pa r$D̯ "Pص CRt&ݜzj\e֜++!ո:& *YlY}йQ':w= ?ư;j $; Ҙ|$5MU?òufb]XGA9s#. "EXAX)"r9U ƌUTuL֜ #f:v-fb Ĥ* s^~n^nGڨJ'_f}ѽ<( B;;BrGd^RMl8H ϷjyA`z;@YbLO*d8ubnd!lm6 Ac?Yg9яzIC^%2ʝRn`CVFKVm4]nj7iCǩ60F}z.>fGb8YNmWP&q>D\VPmR֥&0"?g3b{:=}aYT}厍E⢳Xw"),tL 8<΋*WRš+OD؈B)<0#24DIt"1v욪Ҋ5q*fC^z vokB.F}?󭞌p"cY *P`ܾ}?NAo$'NN 2ܤ9wAzI^wK͸訸O4gX|`=7ffuBf,X{CQMTpEUsh O-Q3J.B>BjU9eu~@)cJBQ0]AMԊ =T)s"3xa,&(ഒ׉GiRڝzQ(>ҵ?>MV (:UݟyѐMn6UPj^ܵi!&흱y]'4~?+mysk .U= ]7f}E3j@yypecW}fًVD .3k)C9h)/.z\`mɢ4tգtVAM)r^8i #ٻn[;[,ݧѡvZtbK*9HT VXn(NU 8mU3ߛ5KM'9ByӞU: {#/K}]|UL$@O3}ta6d|Z'ĉLc/de\;OR3y IG_S?6ėǭGjNP5:;^Vݱm$ӞC@ 4[a2U)%] 9Z)m!'KK73G/~/:N_"qQ.ӁNbS_**1cj֡b ө5.Pq`[P#NǽiO|IJ&$e2zrl\xU,؛M+/^ dYҸY?}:~;mÖQڗw (/xǭFzM{kvjaNTa|PBW41yff[g޻IdFoצJpu}Uݗ*ukb2L Tz "ά*NT_"^t$9F]ˊ`&n\߱'7hܱ(a'B&Ρa.W^‘y~MԓK?*Nn"@ݕb.vyvL4isErBtDSMj{u%x8؁ˆhdsq¨X.1ûb {=lw&|plw|6M~ZfR6T(Cm{I5UzK{QσЉΝkEOtO b;l j!"${3ɺM&Ѭ٭H>hXh>|`$O8?~o@Ϣ#, _lԶ%sWWkF'xͿkvof=z Q } 9}4H#k j]m~+4ݟޮ}c1{ |wd[mb[/mK}zĂǨ "ْM{iE)VBG-b {r(hTvqγV=p-MT ykzy$5IhȡLq`ڋ>#d噇 )?eS }9~w#__g ,?AdFDꀚp)̢&9|+Lxrr`yȠ3kֆ-jV[Ё9l1P94]; @;Pr.AAI_|0U&3q4cb#HƯiZ] 3MnimIV{vFDh=U駶8{ 0?TZ9|LKbvd)ͪIr܇)@OG Dyk0f[ibz\[2E44tE uPΡ`Gү`nT+SRg3+ pe P 26c~?- nPRf~\x f|d[?R= (](։&ݠ_oT'p)N?pb/CR :)z*JuwxL}͕JACrfƂ)ABBDEBmcSEg^I4Pߊ"P Qx>7–n׾ @aU`I')7`ўt?A‚mD@UJXyH^/%#ఴɰ5ٷ߫{E8]DmMrq|dQyx [VV‍c R={-Y~ʝR췷%]-̒_ =ǼOw`Lj$b58C:7˚C0D!T/&7YVY|Z5bgkLc=@'77EХ9e1ʍx&^ܦMvY j#S+(!d]I9h2E5mUD3bS Ɔ/Tnp+A?E>]!u:)a  |?TL6ϾЧ w6I\%uJ}u_5rag(3G]uJ$/ɦ1+Z\D.fe@i@OVM+~$Qq`5tw^{!j 1u/4ݴwnrQFk5 +lM?e *oΓKAIZUl]>mdn6B% NGHTK*xဝ^$&S)ToNW-Xi˜LHAU,PSA |bBj d(>遐g{Dyr"*W:$j^I/ADu"PH-W7r]\GyħdT:)lZ- |/yjmMؖ7H ]o LM2j-eiIA>E(c<. Cc%7L<PWj;?ngLիVP0bUP-E0m99pԨ jvY s}!@IDATl;ܚФიo|[6nM%_sQ0ڭ]O|KLKKG<7]wCCWRpzm+ y(y9lm-TT]b,uC$u{e* ?\ҾX,I\׬7b g|;ALR{Qe.]e6o"+' B9;꤀r$z MuޫIsNwȝzjuZQ~El+ȯn]t:@Ç+?\2ƿ/^}U]]v (Q1b{LXXkMl`CB/vc4T4*= R썒]vYrfΜs}m5/{f~39{{w;ŸYU}\{XwSXKjq jcpXIVOYN_SσvWB" w؜Ŧ ef8r9:(on&O^83p(T)Fjno!L.lÚ'fI3rX3`nbVט+Pk|#}P@h= ѩVVh*ZƔsۊMITZr Λ.2 29vA32b8CkqnAZ7_ш=5PG$5:UcHBxMKd$RaG)a}^MC(; } rrö\[VjLJk^6շr{@8KҞseNiQ4Q"4I !#ɊbbP;d@j`p^&@|uj鍌4`@64D[$u9ںtF?ψazo: - {kJoN8L3*r^l2MnqeL M:Chci@8G:1B1Tͮ#ffsq΂$T{dɗ!qiɱ#to3 pǕC%\#i4n_K-MMZ'@D f7Sa<|%xp")l|fk1YTd4˻_g|gehip2v[f0oCL%qk9ݢE9 <3^9Pڞ0Z7R5S:9€s$lڿZi#=U҉3X3ϻ51r2jN`|pO e+TCMxw= IzDO>8xkr;LJ]>gufB4ƛNƌ[ ICkZ̙smޖ~6KY\u%i["i %T+nRkI@U[%#cf`hdf`Byvɤ- { i>XF+"cNr4YD&8\rVGP7pj[80&^M&_rXsU 6{Y?z؈ .R?OG9u[b|g-f#@y%K\ 2n]_ī58tgI[\w%r@FP.w ZsJIb$U7U@U_#zm<`8`J$N"DgK ]fVrD;nv 2|Y6$Y %Nw<#R$32G/G?}b8.b ^?WXnstf3r5E΃ޤl7BgcPͤ86VA 7 qqf4ƚ!H]"y clwSu@PY(uiQUM !uZ<;b՛Í#A"E^wܹݻϜ$W]2_:mw|I[Y|[Ç>߹oM<֛slCS'=]'TN`Q'׽=tiC#c;O|AˎsяIOs^玺}&{/,~1k#N8O.}x \#PZUR"dazu^+oìAQ"Y=o$8Md [ZS!0ǩPuu%|8|2x5~N\qĽˤn3V*?Qt4BnFS"\ءSUM^NUU]ܥ[EUu 4'P#X`$+8+11I?vۀ UJYrw+z$4`?V$ ڧteG=;.ˎꀭ+ʫGy{Irnw/ؽ?_Juе+V=踋>r M#r֒%P`>EPFGU 05y]J N&: U-гwQLQ: pvs"Y 4bEf<#nO"]v?\LCVz^+⭱v!dVcrɕM_wuy`Ox*Og>$3u%%"XR7wϹ Zv=z|` )X;7١vmzX1sxZx(/o#Z$وVh¯ {VfvOR,bdF .Y u:CE&zrW V꿆 *ZH\flz|;+:V3>K 4:`nhO|IT=̆I&J 3hd.njD{KqS>u.UMsO{k|ncݣf/ܷg/y#w̤;G5I<O{W%hߚ hDGjժ}d e1ߥ y'n~NM1S0\9oas]S~z}ۮ9w䇥X.Q_pZQ2ąBZarոws:|FM8Mx}\Y>Q0p7O* -cAaQ +=K&)6 Odo_w[_Rtm:;p}ϟ\:;׎=naC{⌺=7~e7֣niKTٕʾcgI簓}tIeͭh ̲1suyb쑻lI~\8 &/roչFfg;(ãKU큢^OJbh_Z,4犡_PzѼAwbb J}D|Jۼ#"Dh2Tp%0$p Ε(SRvU0RVKqHi~=wh^˓\ ʛ5b?qOp~u*t ^R3=BRx8/-7W G`[$2/+ˎ|06-nHjl07,1AT{*PfA-BU)Yܕij5b3*G(H fiS7q Q< <[02q< dD9y^qqUe_ ^x6Ai#Nsɴ<*gzv+p!16z۹h~׾aWF JMBEh'I+i_(,]M\q*ԉSAvofWqySA8#L1q%lѠɈֳu#X;O4pVMʁRyKfQ ZWF 52.~|15 yVxBVaa7)byrZxen8 ]VPlE_}ӬDTF<)#FcN`Aru֣)uN!T e.FS4ޠFHFL%]Mh3|W &2."J+x%|^=.@ha&-[qك7@-HHhRk7i 96;\ˊ,|Ern$x54*%7`Nj,-\.WudeT9La20VC0:|8t%R`~D?~x6XUP@ H-v)E SX IǸ~.+{wf/lhNϙ `mj `o[UK/!׳RZxQ!oz_CJ9cNj!UZ7q h}`(Mi_X1sg4-3(FM.=cA&'ܓF00P9&5)Z!"!n8 D!q༜+%@R([@!Oq"!b<8L̃Fo,M,1\X ~z!o`? BhfZvtU*]sݼwȜ 2|4@_~fY*, 6h^7}k-~sFf?učy! wF=*GNy2 <~\ub[(}֠nokM{4]#5=5h*dY4X30O{ULyy*.Pu>㊲=>bx26ؤzܸzS8ڇ'y#oM]`=1$k;譛x3z/Lh=Lrr,ZO<{UVYk)OLKX ?l٭铓s~T7镏LOləv䰌 HMm|2"NR.v[V)}g҃WZSf=sydF Θxz7REOgҢCtSd,~w=?7:=zfdO: [NvWיƄL2Jtϡ ;[aM4OLa@GÔ'c% З^Y)4lbT2\XC Z4,jed~PɎL@Ɲ}ӖMj=UvyU/M(TI( QJɻǒ;}/OẁŷdҝWAlu !.^ l7YOu.n\pQq.^쟺0aқ9JAVeGx`91l&qNu?ݲYq-וY9pu.[uAz7,zП7;^K/g֣7{cݾ2k&ITw–,ezg\k78?v0ޥ]猚A@$O4㡧Wy#<[kt3]h=S F6,Dj5M?{5=Iyk7s ݏd ò2Xu]`f4p}Ï l8ɒwqj7{M?gd5Uwzxhm5*Wꞙ=uV=;a|[0ǥLx}7x}c)O{v/INѱ#vkjX8cgt)<[l4w ClgM#vG?bNy[/{R;-*iS3W)蔙Gs7 3,r8\F+θxASŧʲ_W*ZE]_ rKH9Kq ]{Fa EQz*0;y?Fi_x<-(b!y>/(U&G JMu_"g>kTa!I_bڭ_t xꞭ ^HvNjdq\6 MeD6XU =ϿKQ'@eDZ,GRHv;wޒ[wkqup\&@MC1ջNfs'٠B4}T^Otxჟ'9g O`Pʷ8P8%*Zo?S=k~ya)Ere6e-*v~|Knbp5ި3 Mb9o4ide_'q v>hU1h!*e4^T$QPey9֌b6U'_?05[ Ģ^9B>9j8ϊйsK-JLm$ePD-/PTɦ[ѹX;n4BuN''ٹA7!Hv86€^h H Vvpn<+!<Q(HV+CUL\IժH e0g@QT}}iBX W|J>7 G pv(26a. 5xߣQLEc[ YNKhL:lon]"|atS!\ @֋P5q,>Ef¯HR#G"r7egD"YZi4; ؂r* k*Hmʱ徠VU)$JXg]x]hFYIˀ}^h7}-Gwo=Oޑzh=7=d7ɹ7|PIa16 [fY}"~#n _MAhf[+4י6>0Z2t 6۱ۓ^|Ϝ5%?9WoХ@ 9} Kog6ћH` yNOC8$& =^d/(Uߠl酵 !}lYfM,΅DU*EZDa%"H Y3-)T+F* YRNޤ!?|W0._^ukoX.]<{ko_v:gFkқq1^=O숁8lz Ow=2^>,k˗󃿻I~?tY3]5X@UqIKZ7t5`Q'cOv9\>}bG\au܉GO _GwrJ V?wz~+i ( ,&+ ;CX'a *룶5A0w E3"1Xù@ bkHbFJSlް%QM:n=Cc&#yݴIwc]_?Oz3-*Zs:Kt.sӟ:y{e?rfOǗ;}s :R⩻'54M{ᩓ6ŴUUr:{vӽO13 [Oݕ+by5.mO+;Vic7~۸G?ዧyq=˶~Fi_N{{l VNj饴j) 51ӗA,Pབྷty/uηL{oB:7`GrI="U%Tc#ܦ*N!!$T} $*h<@NZLcJQ988"co܃þPw-GqcD7ѐ8D:( xNF,U-CvB } PΠYrc*z6|yV#Q.*?X!TUWVQU-͇қ3v|w-7IV5#<^aJûHxsvQ?> O~gWz U'o8=OfZʝ:J%>pt?Rx73+Z:¿a;{e_7:g쀶͑Nߺ(Gܤ@5'=CPLYg6y T-|i @ /Hy*!҆ 8;B7ȁTؙQ 5Q  !SCaQܰK" q~0x \F!Og> /߫ˀmN?&cQI ZU:%V*ṗ.;fԡ" %c>kz4_> wȻLG `I˽/3s9tgc+rTO9Szo${ Z8LsfWǯOϗ5*.;vx:Q$mz!Pe o)AAUMirdEY.;'@Y=%ݓC,$3#Ҡ@upa^`Q%G$ 2f O/eOU ֡8)QD`aܡD yWæ e^fH`^ytfro.'-z䎙LM AQـ+JvM|M|I=_uО=.&Xl9}z^/|;(IiVݰoMgFuuR w5 Iƽ0{$z۫W'kNauњ~)k-`A͊S!=J7H^y/GzOP s\pơc/~E[ASX,fT&">Y`_SgaDiQU'ME1|,(&-Y& SsCT4(p }0 _}]Bxox Rحsʾ( ]i:1:r4JWI[UKjc7zh6y1{GlwިB ZĿ7%P]VšZ77cY9޴qxZa~P:۽X~Uu$(L »ng~!~]ka-1ú'ژ mȉU@[(ܦs {3K6*ynoC~tv=,G+Ml:-)>قNW{[n]9o!>=Ir0t ub-t ഢ>A!*c:|aq'T: nҲ!؛LsFIVMBTE jHjI^>e$ys_]{\އqK;|w@; AYZ_ jhohhV k_bK0Kk|6UUWL+V9|/8<+br{eGˊn)Zn+\% aDtMU+<֫MrVAXK=^V ֡[Y~T9?Ԛuc wx 1ˣٗB܎YQpjBU<-UyX=#@oQ'10A6q$ 8؊NV#Bq X\I0OO}HIhM?|D6v@Mҟ[V숊ڜBи *lUv\<]UB"<,@f M^0!ňήEN*Hyp-@%+!JR%kEP+/p}Ф~ J(ÆLj62ܹ!4ਝ(xUrZƣ,+6\7;_HRjP+:aHOu'ŋ_ż͆i -[NܤbG1[V=v@o mW_,!oos@ؐTe8!eX i`+A 8{0OKq;)4GQ-+JN] +eÔp͎(u8_.B";PXF;'AB Egς7 mF@)1uz{JIy_bq!mVNLj!I,Y  G56Y#'fqOx~$>Bsb c8!'@S B5m+K]zZ`Z@n KAT ?ؙ߆ojf^j])Ĉ8r?td7"BwP&b)'s8z:*V%2kx+e@@8@ 蛳-DKx2;MMgio|_? \y{F @9~fWqj@6[d)Q5ߊz+4BKGUiiUAl _xY\wIN_~_6K\7oG\+la6\y1ųpJ-! lS-OTj\K:+AJo:' &"A,8.J2dfc\"ܨ č=ބ>u\ГR;bP]K(^Ƅ3VAJj ERCzH~ N5>o?{DשzX!jW>=׃kXC/҅*i;p&<aXV1 _{VذۨOk5fPq1  Ӟⲣ\=giQϛۨ#~|S>797ȓ(ulrUcүu1rt>i,D#i ws6] T\`B=UF~>@͂^sZN5iht/#vu؄ HYØJK/u%X`3;t23YPF~ #?Ttyy/>Ze#|'S?@IDAT>p#Moxd^*d:Gw\2}{dgNY;CoN4s%.#>hlNSsN}yݲ[F($䌁o|k;32aY5BPXP^ ^~ˆ*\:O_FT$7b $ a3T,dx1GTwGPQnآ@9< @KgbĬm*= 5=P s n> " =F{3Xr/q TTk|2h'2Yz|~Ƴ[PAP%;ҳNߤw^=: ܧ뚛x²]<):#ӱzw ܜ,ĥ7]uvNއ|~`DwDkmZ<'?k%-;z~K~-|Vg-P=WJ^jZ񬴪ELk|x?I7VڅUR-^x4`>[4-m{͝Mr}Q ޫT*\_lm9>p /y'V6AE׎-*{D z+a!n^6ipz$=iK {l M(WA;ift!2w76G l ./ܑ}Ieot 9%3+ͩf5`p_\ h uҁn6fHUV켆G(ԏ53a-:#3Actb< 0J_!H Wq)6pq+%>A[ 2ۯyEyOYcɸ] 3,1'53A~}$ٹ7,,T͍OW?z B {wY{{7|\߭k.U{ } =}YvTl1b"Y4  :J%ʑ{NJӃ MOQQ.r6vC$g# 6i5hSC2Mjqa:k΋X$EwGKCӈ ܊zj e r23r?F,h"ZIav1k|"k|nMX9W['Wkoԡc$g^307~]]![Qlw*;u)z˻?KaԬ o /QYs~6?v.+O:lc6y;Q~Ys'-;igI/𲣰@vcrX{Ȟ{[ kIٗ;H[DgNxA$ R !L8I.-Q"Y4 F1%@ sPp /rS" (6ɔ$#Al}*e*h_03%xR H"Jȧy~M`s~w65/_yaOXg~ 1֬ xY VTdqR, b.u.murc򊪊\yvf5ÀlC'COsaPoctt1(s4% &8LZ AdYnlQD,[BÇt^qp3mkP~'U"˒,QmuJ 2XQ]K`ug6Vm Q4$VY}.K|@W$;Yjʱ\W bf90Sw<)~E=fvD=3K쭐l'V تbs1HW,x=XL[Ѳ$Gm+aqۚ=0sŤc~ LZNkRa?Š (v:ك!m^|KUUk ɽw2FApĤN7A) '0"6G{EdYHxZ_ ?ƭ/͑eÌ ER#Mh'[iXAt9e3azZKڇŕK 3I%2w\pr(bu*5yբW/ 24EaAU H#l 'FRL")LJǚH1xp[=A3809bt>| DKS`2;MK-Z f"C\=rb&w&5D8gVsb uI?"'el I8Jl( -ܛjh޴^N.v}뉡Ka{^9g;Ƃxg7'7 ?XrXՐ[Ĵ"[1/B*cxY.%ULHofZ"?b@ɍpG{BD(*NOD9g*?$n>AD$SZQn^N@y'%X4zgO`lD%5l[&䷎;#7 #(lg<ʼn1~+6Ԙ@ƺeU͇M듾@[x1(ݚ>n>K9 l}"aKz 3grd'M%/)B6wk/Rҁ]Z. Cy IMLGH @)Z >^RH͈xpˡN= r 1pnX $9M$I e$pEDUwYgcG \` ompV7{u D->dޛl Ϋs=q6]mȖkpq~΍c~U=p5̥-zoj]nؾX9 Xڊ>Y:.S"Ra_zhBBd>ę83n0/*9 _}",(zjҮx!$cDKz8r0ԝ(1",ͺ9 sf4IVSP~GРϡR$ E] wݣ u:?gO[ ooc]]qD+G{{_~1S%O.s[ ?̻_n氭Ӛ`eԜ~]sk2jȚk۶(ff\g-´J2HHDFmlMeK:@RG^QZeNɅU[ b*K z1E^pHhTޥ P=5,;~C]F&@iSg9R A b;A@KyÿL$f)zzl#;/|c1A|y/ΐut7>kᡛΪ^g0<8wp/{akUy}&'$i5gls兗2pٯgMr>P7Sش÷J:/{λ׻ .KGTtvMVܵaoWY끊h/U0b2dq}O9LžO[KI ?NLnASA+JY[\\6RCj*Xf {x󃞉8_9{hˁRO;!sfZ vo\[)*T:!XgK[X=@j_dw˷vrɿA;a K4yr;o>.lu{sw4.{|u9i=Ys>:..穧Ϙ!5,-,l,ðeON~:~+ì^^O:M\3ns&%pdamJ?#ν֝,.g1>4"˒D@<K(2Aq(d˄;L9ӡdșJffj~SVY@Ukr>X:seSxwʯTzZd~ P5nXyUKN[į#hpW.XV40`0c+{g{E ކ5o$LI]+;T$\j/_eᢅɇ0'ŰY?X Z"PɕOʻTgkQW޺=0jqwκ}l)$jpއ;@ a^p{_iH@8^#왏i$rϪiOne\c%:2jWOs([I ,@&g> Z_ ZGL P;vbNMb$Y!ڢ޹#P /oK;kp AJ?p'u޳V7ctO+]?2ɲm?|yϘpՓ^(H$yoo[_yįƟ5-:y(I tKJTom=O.kZj)^P<{[:.<1S.LX4'k56v3Tŝ2(8HIFF c2,#5-Y[Q][ F0"Q HB< N5pJ!Z$`QlMX_N<@G[+{pxi%:{ j~[UT5K[lاSć.y i:f6^;xӓSgѬGHLޤ2iیBM͸Ⲵį ؎J q svI]n^N4 ˇ6n!4n/.E#a PB6=#T~iVů& )̍9tϞ?G(EVW;:8wɔ`yݲ\- & `R-Zj}U=qa&w[,8 &6IH(dx?^&R- 鏏Co{e754@UU3ɓ|oMLc+9i܌^8jJ9]7 M.OhVq5GSOI҈˒WEPz`d*0žvn"-K (٤c( $:9R`F\2ǐ?\4!Dho %D 6󧇝 }g"0+6N[ >=F?Z-{>P6+BkX& ЦM¡h̨rZ,Jj,W}M񕘷CDjJJ굯HƒWJG e%x{,NeZ@ ]?~z4ŰhS>&cs   FtV? `NԮ :jnHg/aG*pR0ŶD:X1+<;;f1/ԁd3=+\ 2<aQL6ZD[f͛JFJCnqHQoJt&\$hQSAR K DHz9YY3j8ZRHȠbڌvP#zIQ CyX [&6q l əNfRSgf@ry\`LFx".XFbJ!A(&SMS[Yܩ npI6/ "m!LO1":EϞ9VC"+#(ȫfq~ g ͻQ <⥔T&A5vbk~y~W6:Lِ2FIhNu+&!?nZ^h)]+534׬ +Q%%ՙ3dKITs_QYKQܩGxfZv088$ZBFp8nw2#!++ä ,HAb0d`k&”a-l nGOWtGЦѻq $i )SϒLҁ@^PM([ڶ-ĿU} /:Cr8-A-g)[}5_-πe-7*zw1b,,:fDq[3jEߍ CE*VtfgUݏӑ:dRTx!1}Of AҠ4X:A?.2'4XO{- h급5aB߱OX^=%NfAH'R? dѓz;!A!TKJJ`bzz>T1nmm.nt}34sFf sElDy٧`mƦ[9/Y ]LACr3#*}x01<7Hq5I0n+dA8 a Ɋa $T,nFqH:)Z!*蒲) #23'h$`LCӘ‚̏҆RXfQxTiJ6k^mt('^}]>L*7Umhc;7- dKCb#6 mK #{2|{dcf$(/[;e#LJp޺KkTY>6ha穜E BxN`s8)ĸȌwF&шRl2"IT1KqZ7k8 Qy/YHf6br/iJOCWop:3@^qɔP%d:^P r|wk~*o)6|ǿ'Pӫǀ>'I5zugU0Gq.,.MgZ>']7g >g6 r#n59sïxP5,ys_\Ǥgpϯj36[yࡆiV R7j!sZk$2X< D!<)yRπTx: EV p [&, |$^|DW !*&Xɘvϐ(q$|YlrtDԎLJ?=_¸'O)cul YgrIۈLY '㏻/NbA4 04sj[+w^)̜0OڢYSn/@QMw_9n4skNyjɘvJ?vwe+[}Gi~,]0 bxn[B*ONU!UPTSp 5c#R8| .X$ӮGq$5>B,-&ݬ̠psn6Q:z!̱tev *-tEJ:0Pic296:>0 i(܊Q2|j2^qBI ɹ(S(yWyo4wk|.5> M8x ju_ȑ;%z\ӿRԤԿŸal~0#eRǯH~ 5-r̭T\ CIE:,2ֈQȐ (k'.RiRƹ  oPւ*s5b}ߣiɷO3V^}13{aꇅ?_M9]i:q0XݦGˮIk|޾6x/֬iUtE7>z_6P2Ax{wRYSE OlFK~ĝ]P,_^"q@%;?oǨ jȀƴhUt tzaMX8As:H v%q$r ¼o†&T&49Ħ$D HiL^;!+]c⤔ "\TŇ{ Мtj͎|6'l6#9(Sa1F:h1?8-k+3tp+%/*?Ƽk/ -? 3 )!G0 @홻_|`各-qW9<&6-2aC9ڲMXe R#+UgW`&Z9&$әѰ۴%P1%"եt>TƊ=:9bGx* Cr>uY_i7_{co-N̅`gj>!Xt@ nVV0d7fں|vNj 2O({S2*;<+eZx V+ [Yܓ' rT+TâB$sR0UɁt":phsf {jB9/0*łx;Q8x*TP7J@C`Ȓ$[aOulWU}$K ѡTq 0*1tpx(t Zbh)/A :0q EK_9AB=׷^{}Zo:sO#/3X6"̦Oz ,C2yu#+?|_A\y^{3>f/]wܵ[nutǓ)oN#A>.8o,; yЦGo\kL3O1@jEx,oy#rل>渟nwsOzǬ;/S \ǫjj)^/ig9ؐ5 {!d Y^- Y1R}q4vyVt2Җ< NI7]Y|0ndi8>edDEFb3ӈA5g[QWVy2KlboɚQ 4ȺtDH !6[1Bw}y<_˥l,86?uU_=}d=綄g|.;n_?% .ޱ& g ǽEog7fYܜtT,:61K??MՕ@:?TZtl|/=ö?V᱉ b#aH!O,3/jx3U: " ,xnK5V(x o:n)&+5HWj9N|)O0{Z3,I!d-~&QI:a\U\C4W/mCiwr 5hf#Ev)6)9ˮ_(8=1V$>LwV}}.?`3>W3>ƁڼcNx܏gzե8^A<)/}ηzVO{~ʿY{'W WlyOy_Q~X:C }h+P xv]o畮^߫%ᩥ/;p^u9VP:O~ιƫ_z*;J)_plཿ!RFW!<ĝo&C$]B"OCCK:nc%~I?^dg cz`Vtϓ$BT6*ȨڒC2{Z4\rѓӡ\&%KҀdD;b VG*F>.*6wiEڪ*EV }$khwA 4nfZ9Z#b;ũl{f +5LMoq6P/oWe_'A K_{n9s0.\xa6ԺSOg/;bϨknÿo}a[y<=zh~/r;ox'yk_*~o^=Xe#\zq+׾h#?~f~@-w] yYBRG~ g_ g&C׽ᔓG6ּ_ȍ`~eE V|zA"h+͔fx]@] ue╨>f!*uJFDvf64Qn"RٹYlJR jF3%іz-rL K3ٻK"3H`E,1ik:F%O ZE- *5WWSË,zāJX4d}WKpX2l{[у"tԛ~>/__99]{gDO=}2~^t/9OQRK撮|5^Y{O/ꢏxv>t0P%cGV>mh|Qk0 .~~ 5c2 c/P?GetCLٞ;+}l,\}O3`҉i[x$ydϓY[cSKWl$$렩 &&k&qe(eӮ̩mT=Kr>~ӷ|^q Jg {M/n"1uNL๥}sB(V> kڵ O7<KnC3`GT XXeX++e$)7!T\Uhw'"i1Jee:F6(iָ6fKTgMmzE`0k(P2" qpocѪ-䥃|+.7 WG+~%Y-_sѬ>tgnHD7#_}*~"A|fd{ Ave[v/Y2>&3VM3M8EN|R$&L)$C0YjAõ)JTsp(R5`XƚTQ,{HZx=TG4_U3*,lF4!hD,5٨M>KQ-ƑJ_,|X'25&5E(TmG.GRK\ }C_ҡC~irӂ^]#a{OXWsѱ^╯<68W\zpf0U^w[޸LGb7d\#ˍttDGz'Sa'lϽ?Rx%LsN˯?}x?AĊCCIG]v}Vqu?@I9k=R*jMl`~(8!kzLHv|ic/NX}VYOO>}V٘wREEf6 \^QsUH e k}J/XH5d:]pVf{(GN+0QwtD2"X%UM1(JZW=&F8F<{yDBl|ƈyHQ$"ZM53( KۏRY^R.4"ꆱ9<-@H$` _+>#|_Hjvb:ܖES,yKn_<=yhefЭ_<Kj>l9s%6D yߞ,ރqOz!~<[.r=.>k90OeZ6tTe󋀖^TXp H.6cdxA -5 >Wԗ(mxFqlYTPhSP1iQ'KNa%2iKVF#fjNt=UWq CGƓw]rQka TDʠ;d>G7px0 .rPL0OW!*fGzWMxй4ꐦցkҲ)TTPeiCN-YbS4fȈlByە|x:“]-v :7uJ$~jX=O0Q562 bpHwL|mp |Y `w]fj.'$+E-D7$v\ N & 29  L. UڔN`I5$cNu\WMRYQrAd/* 9ծw2/W,ܽ20FsFT5aS ˜I2ٓyL]aB\ӺU_~!CVr J9F!gQF8-zq|QA'XZYdFgi">Tee6 m"6Ki SfzO-cWF{mYlBW8Սf:@P;bPknؔCCޓ&Y@./OZn/!U:';m8JtNeA vQf uBLQ!aV'TBʳ^6wqXZ<ےa!g02RON$&Q@H<AeXɒ83"HtS.T3Pk@7^,P` &6K pg^+ BT6Z5jHyBmVbI٭SpeRGL-M-,ѲJ;-;j)T[=ql, ҷ&;jJ&%9qQ3l9Bk&X<$ {_(Afh?6*wЊ:py?%Wx\7H1.":dW*JXRr!s7rEX5]')88(%'́Mł ,0hb ɍ=XZqblI}g䬰LzmMlaYLq$+iL(kV\c>FZ1R8] eDص%mDt:ᆅeLC'm"p& KI= %&ɽЂQv0PcEhPɞd\ieSvSYDɳM@nQ`{Nk7-3dP ~|-~S(0$:s22݅&_MFV^+W -266-LP$tKgF&FIԫ U$eN%ԈNp's;O,!YJ*%./)0b$&bhiN80bTC!h@(|JR2N\K3K3' xpEY!97MΙ$qPC+(H-L](J+nirwUa/ EG2YV->IWA. %0Z~4w7=iW i=;U iYg ';KIDE",z9iYETɍMB$+r %Ep&ՙ#:ȎtMzin/RR5`EH3􎊠BvieV#< )L() JBE ))<t}0,@d吃qxv,"*ŽwAHz)afȳ JGkbob)̖t2<&@-U$]b~)d 9aSvP2> 䱆(#PyF y,ӑ%#QBY[39O7AM Z[r6cu"}l#|N)gjj77c-( v:ه@SHQd7+UdJB ƽ3~xNb5 _LD-}g2㺫mzQWIp={]$UqEZ#4 BM,dg_>t3tQm1 QQxX\_{S -Nq<ڋ 1WAMЦY pPweDMhT A|3D^ӛwܢr*NjVnT. %%ĭ[Q+{%g5L]uQ"֩^@uCE`/eϸ쁟ZItdmn&d^C67z@XN"ҍ~7' ^QTX'c-A 93 >$) #include "BasicStringFilter.h" std::wstring BasicStringFilter::filter(const std::wstring &input) const { std::wstring res; res.reserve(input.size()); bool started = false, in_whitespace = false; BOOST_FOREACH(wchar_t c, input) { if (started) { if (c == '\n' || c == '\r' || c == '\t' || c == ' ' || c == '\v') { in_whitespace = true; } else { if (in_whitespace) { res += ' '; in_whitespace = false; } res += c; } } else if (c != '\n' && c != '\r' && c != '\t' && c != ' ' && c != '\v') { res += c; started = true; } } return res; } fernandotcl-btag-a5d4440/src/BasicStringFilter.h000066400000000000000000000007351345145022400215760ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 - 2011 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef BASIC_STRING_FILTER_H #define BASIC_STRING_FILTER_H #include class BasicStringFilter { public: virtual ~BasicStringFilter() {} virtual std::wstring filter(const std::wstring &input) const; virtual bool requires_confirmation_as_output_filter() const { return false; } }; #endif fernandotcl-btag-a5d4440/src/CapitalizationFilter.h000066400000000000000000000013251345145022400223350ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef CAPITALIZATION_FILTER_H #define CAPITALIZATION_FILTER_H #include #include "BasicStringFilter.h" class CapitalizationFilter : public BasicStringFilter { public: bool requires_confirmation_as_output_filter() const { return true; } protected: wchar_t lowercase(wchar_t c) const { return std::use_facet >(std::locale()).tolower(c); } wchar_t uppercase(wchar_t c) const { return std::use_facet >(std::locale()).toupper(c); } }; #endif fernandotcl-btag-a5d4440/src/ConfirmationHandler.cpp000066400000000000000000000041101345145022400224700ustar00rootroot00000000000000/* * This file is part of btag. * * © 2011 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include #include "BasicStringFilter.h" #include "ConfirmationHandler.h" #include "InteractiveTerminal.h" ConfirmationHandler::ConfirmationHandler(InteractiveTerminal &terminal, BasicStringFilter *input_filter, BasicStringFilter *output_filter) : m_terminal(terminal), m_input_filter(input_filter), m_output_filter(output_filter) { } void ConfirmationHandler::set_local_default(const std::wstring &local) { // Set the filtered local default m_local_def = m_input_filter ? m_input_filter->filter(local) : local; } void ConfirmationHandler::reset() { // Erase the local default and the previous answer m_local_def.erase(); m_output.erase(); } bool ConfirmationHandler::do_ask(const std::wstring &question) { // Ask the question boost::optional default_answer; if (!m_global_def.empty()) default_answer = m_global_def; else if (!m_local_def.empty()) default_answer = m_local_def; std::wstring answer = m_terminal.ask_string_question(question, default_answer); // The answer complies if it's the same as the previously entered answer // (the user confirmed it) or if it's the same as the default answer m_complies = answer == default_answer || answer == m_output; m_output = answer; if (m_complies) return true; // Check if the output filter rejects it if (m_output_filter) { std::wstring filtered = m_output_filter->filter(answer); if (m_output_filter->requires_confirmation_as_output_filter() && answer != filtered) { m_local_def = filtered; return (m_complies = false); } } // Nope, so it's good return (m_complies = true); } bool ConfirmationHandler::ask(const std::wstring &question) { // If the answer complies, we got a global default if (do_ask(question)) { m_global_def = m_output; return true; } return false; } fernandotcl-btag-a5d4440/src/ConfirmationHandler.h000066400000000000000000000017471345145022400221520ustar00rootroot00000000000000/* * This file is part of btag. * * © 2011 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef CONFIRMATION_HANDLER_H #define CONFIRMATION_HANDLER_H #include #include "BasicStringFilter.h" #include "InteractiveTerminal.h" class ConfirmationHandler { public: ConfirmationHandler(InteractiveTerminal &terminal, BasicStringFilter *input_filter = NULL, BasicStringFilter *output_filter = NULL); void reset(); void set_local_default(const std::wstring &local); bool ask(const std::wstring &question); bool complies() const { return m_complies; } const std::wstring &answer() const { return m_output; } private: bool do_ask(const std::wstring &question); InteractiveTerminal &m_terminal; BasicStringFilter *m_input_filter, *m_output_filter; std::wstring m_global_def, m_local_def, m_output; bool m_complies; }; #endif fernandotcl-btag-a5d4440/src/ConservativeRenamingFilter.h000066400000000000000000000020551345145022400235140ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 - 2011 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef CONSERVATIVE_RENAMING_FILTER_H #define CONSERVATIVE_RENAMING_FILTER_H #include "RenamingFilter.h" class ConservativeRenamingFilter : public RenamingFilter { private: bool is_character_allowed(wchar_t c) const { if (c >= L'a' && c <= L'z') return true; if (c >= L'A' && c <= L'Z') return true; if (c >= L'0' && c <= L'9') return true; static wchar_t allowed_chars[] = { L' ', L'%', L'-', L'_', L'@', L'~', L'`', L'!', L'(', L')', L'{', L'}', L'^', L'#', L'&', L'.' }; for (unsigned int i = 0; i < sizeof(allowed_chars) / sizeof(wchar_t); ++i) { if (c == allowed_chars[i]) return true; } return false; } wchar_t replacement_character(wchar_t c) const { return L'_'; } }; #endif fernandotcl-btag-a5d4440/src/CueReader.cpp000066400000000000000000000101111345145022400203770ustar00rootroot00000000000000/* * This file is part of btag. * * © 2012, 2015 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include #include #include #include "CueReader.h" #include "validators.h" CueReader::CueReader(const std::string &filename, const std::string &encoding) : m_cd(NULL), m_iconv((iconv_t)-1) { // Get a file pointer FILE *fp = fopen(filename.c_str(), "r"); if (!fp) throw std::runtime_error("Unable to open cue sheet for reading"); // Parse it m_cd = cue_parse_file(fp); fclose(fp); if (!m_cd) throw std::runtime_error("Unable to parse cue sheet"); // Create an iconv descriptor m_iconv = iconv_open("WCHAR_T//TRANSLIT", encoding.c_str()); if (m_iconv == (iconv_t)-1) { cd_delete(m_cd); throw std::runtime_error("Unable to create iconv descriptor"); } // Fetch the performing artist Cdtext *cdt = cd_get_cdtext(m_cd); if (cdt) m_artist = cdtext_string(cdt, PTI_PERFORMER); } CueReader::~CueReader() { if (m_cd) cd_delete(m_cd); if (m_iconv != (iconv_t)-1) iconv_close(m_iconv); } int CueReader::number_of_tracks() { return cd_get_ntrack(m_cd); } boost::optional CueReader::cdtext_string(Cdtext *cdt, Pti pti) { // Reset the iconv descriptor iconv(m_iconv, NULL, NULL, NULL, NULL); // Read the string const char *cdtext_str = cdtext_get(pti, cdt); if (!cdtext_str) return boost::optional(); const size_t cdtext_len = strlen(cdtext_str); // Copy into an input buffer std::vector inbuf(cdtext_len + 1); memcpy(&inbuf[0], cdtext_str, cdtext_len + 1); // Create the output buffer size_t inbytesleft = inbuf.size() * sizeof(char); size_t outbytesleft = inbytesleft * sizeof(wchar_t); size_t buffer_size = outbytesleft; wchar_t outbuf_data[buffer_size / sizeof(wchar_t)]; char *outbuf = reinterpret_cast(outbuf_data); // Do the conversion char *inbuf_ptr = &inbuf[0]; size_t rc = iconv(m_iconv, &inbuf_ptr, &inbytesleft, &outbuf, &outbytesleft); if (rc == (size_t)-1) return boost::optional(); // Create and return a wide string return std::wstring(outbuf_data, (buffer_size - outbytesleft) / sizeof(wchar_t)); } boost::optional CueReader::album() { Cdtext *cdt = cd_get_cdtext(m_cd); if (cdt) { boost::optional album = cdtext_string(cdt, PTI_TITLE); return album; } else { return boost::optional(); } } boost::optional CueReader::year() { // Get the REM entry Rem *rem = cd_get_rem(m_cd); if (!rem) return boost::optional(); // Get the DATE entry const char *value = rem_get(REM_DATE, rem); if (!value) return boost::optional(); // Find the year int year; try { year = boost::lexical_cast(value); } catch (boost::bad_lexical_cast &) { return boost::optional(); } // Validate and return it YearValidator validator; boost::optional error_msg; return validator.validate(year, error_msg) ? year : boost::optional(); } boost::optional CueReader::artist_for_track(int track) { // Get the track Track *t = cd_get_track(m_cd, track); if (!t) return m_artist; // Get the CD-TEXT Cdtext *cdt = track_get_cdtext(t); if (!cdt) return m_artist; // Get and return the artist boost::optional artist = cdtext_string(cdt, PTI_PERFORMER); return artist ? artist : m_artist; } boost::optional CueReader::title_for_track(int track) { // Get the track Track *t = cd_get_track(m_cd, track); if (!t) return boost::optional(); // Get the CD-TEXT Cdtext *cdt = track_get_cdtext(t); if (!cdt) return boost::optional(); // Get and return the title return cdtext_string(cdt, PTI_TITLE); } fernandotcl-btag-a5d4440/src/CueReader.h000066400000000000000000000015551345145022400200600ustar00rootroot00000000000000/* * This file is part of btag. * * © 2012, 2015 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef CUE_READER_H #define CUE_READER_H #include #include #include extern "C" { #include } class CueReader { public: CueReader(const std::string &filename, const std::string &encoding); ~CueReader(); int number_of_tracks(); boost::optional artist_for_track(int track); boost::optional title_for_track(int track); boost::optional album(); boost::optional year(); private: boost::optional cdtext_string(Cdtext *cdt, Pti pti); Cd *m_cd; iconv_t m_iconv; boost::optional m_artist; }; #endif fernandotcl-btag-a5d4440/src/CueReaderMultiplexer.cpp000066400000000000000000000034321345145022400226420ustar00rootroot00000000000000/* * This file is part of btag. * * © 2015 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include "CueReaderMultiplexer.h" CueReaderMultiplexer::CueReaderMultiplexer(const std::list &filenames, const std::string &encoding) { BOOST_FOREACH(const std::string &filename, filenames) m_cue_readers.push_back(boost::shared_ptr(new CueReader(filename, encoding))); } boost::shared_ptr CueReaderMultiplexer::cue_reader_for_track(int track, int &offset) { int accumulated_tracks = 0; BOOST_FOREACH(boost::shared_ptr reader, m_cue_readers) { int number_of_tracks = reader->number_of_tracks(); accumulated_tracks += number_of_tracks; if (accumulated_tracks >= track) { offset = number_of_tracks - accumulated_tracks; return reader; } } return boost::shared_ptr(); } boost::optional CueReaderMultiplexer::artist_for_track(int track) { int offset; boost::shared_ptr reader = cue_reader_for_track(track, offset); return reader ? reader->artist_for_track(track + offset) : boost::none; } boost::optional CueReaderMultiplexer::title_for_track(int track) { int offset; boost::shared_ptr reader = cue_reader_for_track(track, offset); return reader ? reader->title_for_track(track + offset) : boost::none; } boost::optional CueReaderMultiplexer::album() { return m_cue_readers.empty() ? boost::none : m_cue_readers.front()->album(); } boost::optional CueReaderMultiplexer::year() { return m_cue_readers.empty() ? boost::none : m_cue_readers.front()->year(); } fernandotcl-btag-a5d4440/src/CueReaderMultiplexer.h000066400000000000000000000015411345145022400223060ustar00rootroot00000000000000/* * This file is part of btag. * * © 2015 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef CUE_READER_MULTIPLEXER_H #define CUE_READER_MULTIPLEXER_H #include #include #include #include #include "CueReader.h" class CueReaderMultiplexer { public: CueReaderMultiplexer(const std::list &filenames, const std::string &encoding); boost::optional artist_for_track(int track); boost::optional title_for_track(int track); boost::optional album(); boost::optional year(); private: std::list > m_cue_readers; boost::shared_ptr cue_reader_for_track(int track, int &offset); }; #endif fernandotcl-btag-a5d4440/src/EnglishTitleLocalizationHandler.cpp000066400000000000000000000056411345145022400250160ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 - 2011 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include "EnglishTitleLocalizationHandler.h" TitleLocalizationHandler::word_hint EnglishTitleLocalizationHandler::word_hint_for_word(const std::wstring &word, size_t index, size_t count, wchar_t after_punctuation) const { // Uppercase acronyms if (is_acronym(word)) return WORD_STYLE_UPPER; // Check for the "O'" prefix if (word.size() > 2 && word[0] == L'o' && word[1] == L'\'') { std::vector uppercase(word.size(), false); uppercase[0] = true; uppercase[2] = true; return uppercase; } // Check for hyphens if (word.size() > 2 && word[0] != L'-' && word[word.size() - 1] != L'-') { boost::optional > uppercase; for (size_t i = 1; i < word.size() - 1; ++i) { if (word[i] == L'-') { if (!uppercase) { uppercase = std::vector(word.size(), false); (*uppercase)[0] = true; } (*uppercase)[i + 1] = true; } } if (uppercase) return *uppercase; } // Uppercase the first letter of the first and last words if (index == 0 || index == count - 1) return WORD_STYLE_FIRST_UPPER; // Uppercase the first letter of a word after major punctuation static const wchar_t major_punctuation[] = { L'.', L':', L'?', L'!', L'\u2014', // Unicode EM DASH (—) L'-', L')', L'(', L'"', L'\'', L'\u201c', // Unicode LEFT DOUBLE QUOTATION MARK (“) L'\u201d', // Unicode RIGHT DOUBLE QUOTATION MARK (”) L'\u2018', // Unicode LEFT SINGLE QUOTATION MARK (‘) L'\u2019', // Unicode RIGHT SINGLE QUOTATION MARK (’) 0 }; if (after_punctuation != 0) { for (int i = 0; major_punctuation[i]; ++i) { if (after_punctuation == major_punctuation[i]) return WORD_STYLE_FIRST_UPPER; } } // Lowercase some specific words (context insensitive) static const wchar_t *lowercase_words[] = { L"a", L"an", L"the", // articles L"and", L"but", L"for", L"nor", L"or", L"so", L"yet", // coordinated conjunctions L"as", L"at", L"by", L"for", L"from", L"in", L"of", L"on", L"to", L"with", // short prepositions L"'n'", L"o'", // contractions L"is", L"vs.", L"etc.", NULL // some exceptions }; for (int i = 0; lowercase_words[i]; ++i) { if (word == lowercase_words[i]) return WORD_STYLE_LOWER; } return WORD_STYLE_FIRST_UPPER; } fernandotcl-btag-a5d4440/src/EnglishTitleLocalizationHandler.h000066400000000000000000000007671345145022400244670ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 - 2011 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef ENGLISH_TITLE_LOCALIZATION_HANDLER_H #define ENGLISH_TITLE_LOCALIZATION_HANDLER_H #include "TitleLocalizationHandler.h" class EnglishTitleLocalizationHandler : public TitleLocalizationHandler { public: word_hint word_hint_for_word(const std::wstring &word, size_t index, size_t count, wchar_t after_punctuation) const; }; #endif fernandotcl-btag-a5d4440/src/InteractiveTagger.cpp000066400000000000000000000360621345145022400221640ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 - 2013, 2015 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include #include #include #include #include #include #include #include #include "InteractiveTagger.h" #include "number_cast.h" #include "validators.h" #include "wide_string_cast.h" namespace fs = boost::filesystem; InteractiveTagger::InteractiveTagger() : m_input_filter(NULL), m_output_filter(NULL), m_dry_run(false), m_ask_track(false) { // Load the extensions supported by TagLib TagLib::StringList extensions = TagLib::FileRef::defaultFileExtensions(); for (TagLib::StringList::ConstIterator it = extensions.begin(); it != extensions.end(); ++it) m_supported_extensions.push_back(L'.' + it->toWString()); } void InteractiveTagger::set_file_rename_format(const std::string &format) { m_file_rename_format = boost::lexical_cast(format); } void InteractiveTagger::set_dir_rename_format(const std::string &format) { m_dir_rename_format = boost::lexical_cast(format); } #ifdef CUESHEET_SUPPORT void InteractiveTagger::load_cue_sheets(const std::list &filenames, const std::string &encoding) { try { m_cue.reset(new CueReaderMultiplexer(filenames, encoding)); } catch (std::exception &e) { m_terminal->display_warning_message(e.what()); m_terminal->display_warning_message("Ignoring CUE files"); } } #endif void InteractiveTagger::tag(int num_paths, const char **paths) { assert(m_terminal); // Build a list with the normalized paths std::list path_list; for (int i = 0; i < num_paths; ++i) { char *real_path = realpath(paths[i], NULL); if (!real_path) { std::string errstr("\""); errstr += paths[i]; errstr += "\" is not a valid path"; m_terminal->display_warning_message(errstr); continue; } path_list.push_back(fs::path(real_path)); free(real_path); } // Sort it and tag the paths path_list.sort(); BOOST_FOREACH(const fs::path &path, path_list) { // Check if the path exists if (!fs::exists(path)) { m_terminal->display_warning_message(L"Path \"" + path.string() + L"\" not found, skipping..."); continue; } try { // If it's a regular file, just tag it if (fs::is_regular_file(path)) { if (!is_supported_extension(path)) { m_terminal->display_warning_message(L"Path \"" + path.string() + L"\" has no supported extension, skipping..."); continue; } tag_file(path); } // If it's a directory, tag its contents else if (fs::is_directory(path)) { tag_directory(path); } // It's none of the above, do something about it else { m_terminal->display_warning_message(L"Path \"" + path.string() + L"\" is not a regular file or directory, skipping..."); } } catch (std::exception &e) { m_terminal->display_warning_message(e.what()); } } // Save the unsaved files if (!m_unsaved_files.empty() && (m_dry_run || m_terminal->ask_yes_no_question(L"=== OK to save the changes to the files"))) { if (m_dry_run) m_terminal->display_info_message("=== Not saving changes (dry run mode)"); BOOST_FOREACH(TagLib::FileRef &f, m_unsaved_files) { std::string message(m_dry_run ? "Not saving" : "Saving"); message += " \"" + std::string(f.file()->name()) + '\"'; m_terminal->display_info_message(message); if (!m_dry_run && !f.save()) m_terminal->display_warning_message("Unable to save " + std::string(f.file()->name()) + "\""); } } // Perform the pending renames if (!m_pending_renames.empty() && (m_dry_run || m_terminal->ask_yes_no_question(L"=== OK to rename the files"))) { if (m_dry_run) m_terminal->display_info_message("=== Not renaming files (dry run mode)"); std::list >::const_iterator it; for (it = m_pending_renames.begin(); it != m_pending_renames.end(); ++it) { const fs::path &from((*it).first); const fs::path &to((*it).second); m_terminal->display_info_message(from.string()); m_terminal->display_info_message(L"-> " + to.string()); if (!m_dry_run) { try { fs::rename(from, to); } catch (std::exception &e) { m_terminal->display_warning_message(e.what()); } } } } m_terminal->display_info_message("=== All done!"); } bool InteractiveTagger::is_supported_extension(const fs::path &path) { std::wstring extension(path.extension().string()); boost::to_lower(extension); BOOST_FOREACH(const std::wstring &supported_extension, m_supported_extensions) { if (extension == supported_extension) return true; } return false; } std::wstring InteractiveTagger::replace_tokens(const std::wstring &str, const std::map &replacements) { std::wstring res; res.reserve(str.size() * 3); bool parsing_token = false; std::wstring current_token; BOOST_FOREACH(char c, str) { if (c == '%') { if (parsing_token) { std::map::const_iterator it = replacements.find(current_token); res += it == replacements.end() ? current_token : it->second; current_token.erase(); } parsing_token = true; } else if (parsing_token) { if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { current_token += c; } else { std::map::const_iterator it = replacements.find(current_token); res += it == replacements.end() ? current_token : it->second; res += c; parsing_token = false; current_token.erase(); } } else { res += c; } } if (parsing_token) { std::map::const_iterator it = replacements.find(current_token); res += it == replacements.end() ? current_token : it->second; } return res; } void InteractiveTagger::tag_file(const fs::path &path, ConfirmationHandler &artist_confirmation, ConfirmationHandler &album_confirmation, int *year, int track) { m_terminal->display_info_message(L"=== Tagging \"" + path.filename().string() + L"\""); // Get a reference to the file TagLib::FileRef f(path.c_str()); // Ask for the track bool ask_track = m_ask_track; TrackValidator track_validator; if (track == -1) { track = f.tag()->track(); boost::optional error_message; if (!track_validator.validate(track, error_message)) { track = 0; ask_track = true; } } if (ask_track) { track = m_terminal->ask_number_question(L"Track", track > 0 ? track : boost::optional(), &track_validator); } f.tag()->setTrack(track); // Ask for the artist artist_confirmation.reset(); bool set_artist = false; #ifdef CUESHEET_SUPPORT if (m_cue) { boost::optional artist = m_cue->artist_for_track(track); if (artist) { artist_confirmation.set_local_default(m_input_filter->filter(*artist)); set_artist = true; } } #endif if (!set_artist && !f.tag()->artist().isNull()) artist_confirmation.set_local_default(m_input_filter->filter(f.tag()->artist().toWString())); artist_confirmation.ask(L"Artist"); while (!artist_confirmation.complies()) artist_confirmation.ask(L"Artist (confirmation)"); f.tag()->setArtist(artist_confirmation.answer()); // Ask for the album album_confirmation.reset(); bool set_album = false; #ifdef CUESHEET_SUPPORT if (m_cue) { boost::optional album = m_cue->album(); if (album) { album_confirmation.set_local_default(m_input_filter->filter(*album)); set_album = true; } } #endif if (!set_album && !f.tag()->album().isNull()) album_confirmation.set_local_default(m_input_filter->filter(f.tag()->album().toWString())); album_confirmation.ask(L"Album"); while (!album_confirmation.complies()) album_confirmation.ask(L"Album (confirmation)"); f.tag()->setAlbum(album_confirmation.answer()); // Ask for the year boost::optional default_year; if (year && *year != -1) default_year = *year; #ifdef CUESHEET_SUPPORT else { boost::optional cue_year; if (m_cue) { cue_year = m_cue->year(); if (cue_year) default_year = *cue_year; } if (!cue_year && f.tag()->year()) default_year = f.tag()->year(); } #else else if (f.tag()->year()) default_year = f.tag()->year(); #endif YearValidator year_validator; int new_year = m_terminal->ask_number_question(L"Year", default_year, &year_validator); f.tag()->setYear(new_year); if (year) *year = new_year; // Display the track number closer to the song title if (!ask_track) m_terminal->display_info_message(L"Track: " + number_cast(track)); // Ask for the song title ConfirmationHandler title_confirmation(*m_terminal, m_input_filter, m_output_filter); title_confirmation.reset(); bool set_title = false; #ifdef CUESHEET_SUPPORT if (m_cue) { boost::optional title = m_cue->title_for_track(track); if (title) { title_confirmation.set_local_default(m_input_filter->filter(*title)); set_title = true; } } #endif if (!set_title && !f.tag()->title().isNull()) title_confirmation.set_local_default(m_input_filter->filter(f.tag()->title().toWString())); title_confirmation.ask(L"Title"); while (!title_confirmation.complies()) title_confirmation.ask(L"Title (confirmation)"); f.tag()->setTitle(title_confirmation.answer()); // Reset the comment and genre fields f.tag()->setComment(TagLib::String::null); f.tag()->setGenre(TagLib::String::null); // Add it to the list of unsaved files m_unsaved_files.push_back(f); // Add it to the list of pending renames based on the supplied format if (m_file_rename_format) { std::map tokens; tokens[L"artist"] = m_renaming_filter->filter(artist_confirmation.answer()); tokens[L"album"] = m_renaming_filter->filter(album_confirmation.answer()); tokens[L"year"] = number_cast(new_year); std::wstring track_str(number_cast(track)); if (track_str.size() == 1) track_str = L"0" + track_str; tokens[L"track"] = track_str; tokens[L"title"] = m_renaming_filter->filter(title_confirmation.answer()); fs::path new_path = path.parent_path(); new_path /= replace_tokens(*m_file_rename_format, tokens) + boost::to_lower_copy(path.extension().string()); if (new_path != path) m_pending_renames.push_back(std::pair(path, new_path)); } } void InteractiveTagger::tag_file(const boost::filesystem::path &path) { // Tag a file without recording the global default ConfirmationHandler amnesiac_artist(*m_terminal, m_input_filter, m_output_filter); ConfirmationHandler amnesiac_album(*m_terminal, m_input_filter, m_output_filter); tag_file(path, amnesiac_artist, amnesiac_album, NULL, -1); } void InteractiveTagger::tag_directory(const fs::path &path) { m_terminal->display_info_message(L"=== Entering \"" + path.string() + L"\""); // Create lists of files and subdirectories std::list file_list, dir_list; fs::directory_iterator end_it; for (fs::directory_iterator it(path); it != end_it; ++it) { // Normalize the path (needed here so that we follow symlinks) char *real_path = realpath(it->path().c_str(), NULL); if (!real_path) { m_terminal->display_warning_message(L"\"" + it->path().filename().string() + L"\" is not a valid path, skipping..."); continue; } fs::path boost_path(real_path); free(real_path); // Add to the right list if (fs::is_regular_file(boost_path)) { if (is_supported_extension(boost_path)) { file_list.push_back(boost_path); } else { m_terminal->display_warning_message(L"\"" + it->path().filename().string() + L"\" has no supported extension, skipping..."); } } else if (fs::is_directory(boost_path)) { dir_list.push_back(boost_path); } else { m_terminal->display_warning_message(L"\"" + boost_path.filename().string() + L"\" is not a regular file or directory, skipping..."); } } // Sort those lists file_list.sort(); dir_list.sort(); // Tag all individual files ConfirmationHandler artist(*m_terminal, m_input_filter, m_output_filter); ConfirmationHandler album(*m_terminal, m_input_filter, m_output_filter); int year = -1; if (!file_list.empty()) { int track = 1; BOOST_FOREACH(const fs::path &p, file_list) tag_file(p, artist, album, &year, track++); #ifdef CUESHEET_SUPPORT // Done using the CUE file, if any m_cue.reset(NULL); #endif } // We'll ask confirmation to descend into the subdirectories only if there are files BOOST_FOREACH(const fs::path &p, dir_list) { if (file_list.empty() || m_terminal->ask_yes_no_question(L"Descend into subdirectory \"" + boost::lexical_cast(p.filename()) + L'"', false)) tag_directory(p); } // Add it to the list of pending renames based on the supplied format if (!artist.answer().empty() && m_dir_rename_format) { std::map tokens; tokens[L"artist"] = m_renaming_filter->filter(artist.answer()); tokens[L"album"] = m_renaming_filter->filter(album.answer()); tokens[L"year"] = number_cast(year); fs::path new_path = path.parent_path(); new_path /= replace_tokens(*m_dir_rename_format, tokens); if (new_path != path) m_pending_renames.push_back(std::pair(path, new_path)); } } fernandotcl-btag-a5d4440/src/InteractiveTagger.h000066400000000000000000000050321345145022400216220ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 - 2012, 2015 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef INTERACTIVE_TAGGER_H #define INTERACTIVE_TAGGER_H #include #include #include #include #include #include #include #include #include "BasicStringFilter.h" #include "config.h" #include "ConfirmationHandler.h" #include "InteractiveTerminal.h" #include "RenamingFilter.h" #ifdef CUESHEET_SUPPORT # include "CueReaderMultiplexer.h" #endif class InteractiveTagger { public: InteractiveTagger(); void set_terminal(InteractiveTerminal *terminal) { m_terminal = terminal; } void set_file_rename_format(const std::string &format); void set_dir_rename_format(const std::string &format); void set_input_filter(BasicStringFilter *filter) { m_input_filter = filter; } void set_output_filter(BasicStringFilter *filter) { m_output_filter = filter; } void set_renaming_filter(RenamingFilter *filter) { m_renaming_filter = filter; } void set_dry_run(bool dry_run = true) { m_dry_run = dry_run; } void set_ask_track(bool ask_track = true) { m_ask_track = ask_track; } #ifdef CUESHEET_SUPPORT void load_cue_sheets(const std::list &filenames, const std::string &encoding); #endif void tag(int num_paths, const char **paths); private: bool is_supported_extension(const boost::filesystem::path &path); std::wstring replace_tokens(const std::wstring &str, const std::map &replacements); void tag_file(const boost::filesystem::path &path, ConfirmationHandler &artist_confirmation, ConfirmationHandler &album_confirmation, int *year, int track); void tag_file(const boost::filesystem::path &path); void tag_directory(const boost::filesystem::path &path); BasicStringFilter *m_input_filter, *m_output_filter; RenamingFilter *m_renaming_filter; InteractiveTerminal *m_terminal; boost::optional m_file_rename_format, m_dir_rename_format; bool m_dry_run, m_ask_track; std::list m_unsaved_files; std::list > m_pending_renames; std::list m_supported_extensions; #ifdef CUESHEET_SUPPORT boost::scoped_ptr m_cue; #endif }; #endif fernandotcl-btag-a5d4440/src/InteractiveTerminal.h000066400000000000000000000026401345145022400221660ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef INTERACTIVE_TERMINAL_H #define INTERACTIVE_TERMINAL_H #include #include class InteractiveTerminal { public: template class Validator { public: virtual bool validate(const T &, boost::optional &error_message) const = 0; }; virtual bool ask_yes_no_question(const std::wstring &question, const boost::optional &default_answer = boost::optional()) = 0; virtual std::wstring ask_string_question(const std::wstring &question, const boost::optional &default_answer = boost::optional(), const Validator *validator = NULL) = 0; virtual int ask_number_question(const std::wstring &question, const boost::optional &default_answer = boost::optional(), const Validator *validator = NULL) = 0; virtual void display_info_message(const std::string &message) = 0; virtual void display_info_message(const std::wstring &message) = 0; virtual void display_warning_message(const std::string &message) = 0; virtual void display_warning_message(const std::wstring &message) = 0; }; #endif fernandotcl-btag-a5d4440/src/RenamingFilter.cpp000066400000000000000000000011371345145022400214560ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include "RenamingFilter.h" std::wstring RenamingFilter::filter(const std::wstring &path_component) const { std::wstring new_component; new_component.reserve(path_component.size()); BOOST_FOREACH(wchar_t c, path_component) new_component += is_character_allowed(c) ? c : replacement_character(c); if (new_component.empty()) new_component += replacement_character(L'A'); return new_component; } fernandotcl-btag-a5d4440/src/RenamingFilter.h000066400000000000000000000010051345145022400211150ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef RENAMING_FILTER_H #define RENAMING_FILTER_H #include class RenamingFilter { public: virtual ~RenamingFilter() {} std::wstring filter(const std::wstring &path_component) const; private: virtual bool is_character_allowed(wchar_t c) const = 0; virtual wchar_t replacement_character(wchar_t c) const = 0; }; #endif fernandotcl-btag-a5d4440/src/SimpleCapitalizationFilter.cpp000066400000000000000000000023061345145022400240420ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include #include "SimpleCapitalizationFilter.h" SimpleCapitalizationFilter::SimpleCapitalizationFilter(capitalization_mode mode) : m_mode(mode) { } std::wstring SimpleCapitalizationFilter::filter(const std::wstring &input) const { std::wstring processed = BasicStringFilter::filter(input); std::wstring res; res.reserve(processed.size()); boost::char_separator separator(L" "); boost::tokenizer, std::wstring::const_iterator, std::wstring > words(processed, separator); BOOST_FOREACH(const std::wstring &word, words) { std::wstring new_word; new_word.reserve(word.size()); for (std::wstring::size_type i = 0; i < word.size(); ++i) new_word += m_mode == CAPITALIZATION_MODE_ALL_UPPER ? uppercase(word[i]) : lowercase(word[i]); if (!res.empty()) res += L' '; res += new_word; } if (m_mode == CAPITALIZATION_MODE_FIRST_UPPER && res.size()) res[0] = uppercase(res[0]); return res; } fernandotcl-btag-a5d4440/src/SimpleCapitalizationFilter.h000066400000000000000000000012711345145022400235070ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef SIMPLE_CAPITALIZATION_FILTER_H #define SIMPLE_CAPITALIZATION_FILTER_H #include "CapitalizationFilter.h" class SimpleCapitalizationFilter : public CapitalizationFilter { public: enum capitalization_mode { CAPITALIZATION_MODE_ALL_LOWER, CAPITALIZATION_MODE_ALL_UPPER, CAPITALIZATION_MODE_FIRST_UPPER }; SimpleCapitalizationFilter(capitalization_mode mode); std::wstring filter(const std::wstring &input) const; private: capitalization_mode m_mode; }; #endif fernandotcl-btag-a5d4440/src/SpanishTitleLocalizationHandler.cpp000066400000000000000000000054611345145022400250320ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 - 2011 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include "SpanishTitleLocalizationHandler.h" TitleLocalizationHandler::word_hint SpanishTitleLocalizationHandler::word_hint_for_word(const std::wstring &word, size_t index, size_t count, wchar_t after_punctuation) const { // Uppercase acronyms if (is_acronym(word)) return WORD_STYLE_UPPER; // Check for hyphens if (word.size() > 2 && word[0] != L'-' && word[word.size() - 1] != L'-') { boost::optional > uppercase; for (size_t i = 1; i < word.size() - 1; ++i) { if (word[i] == L'-') { if (!uppercase) { uppercase = std::vector(word.size(), false); (*uppercase)[0] = true; } (*uppercase)[i + 1] = true; } } if (uppercase) return *uppercase; } // Uppercase the first letter of the first and last words if (index == 0 || index == count - 1) return WORD_STYLE_FIRST_UPPER; // Uppercase the first letter of a word after major punctuation static const wchar_t major_punctuation[] = { L'.', L':', L'?', L'!', L'\u2014', // Unicode EM DASH (—) L'-', L')', L'(', L'"', L'\'', L'\u201c', // Unicode LEFT DOUBLE QUOTATION MARK (“) L'\u201d', // Unicode RIGHT DOUBLE QUOTATION MARK (”) L'\u2018', // Unicode LEFT SINGLE QUOTATION MARK (‘) L'\u2019', // Unicode RIGHT SINGLE QUOTATION MARK (’) 0 }; if (after_punctuation != 0) { for (int i = 0; major_punctuation[i]; ++i) { if (after_punctuation == major_punctuation[i]) return WORD_STYLE_FIRST_UPPER; } } // Lowercase some specific words (context insensitive) static const wchar_t *lowercase_words[] = { L"el", L"la", L"los", L"las", L"una", L"unas", L"un", L"unos", L"al", L"del", // articles L"a", L"con", L"de", L"en", L"para", L"por", L"sin", // common prepositions L"y", L"e", L"o", L"u", L"que", L"ni", // common conjunctions L"me", L"te", L"se", L"nos", L"os", // reflexive pronouns L"mi", L"mis", L"tu", L"tus", L"su", L"sus", // short possessive adjectives L"vs.", L"etc.", NULL // some exceptions }; for (int i = 0; lowercase_words[i]; ++i) { if (word == lowercase_words[i]) return WORD_STYLE_LOWER; } return WORD_STYLE_FIRST_UPPER; } fernandotcl-btag-a5d4440/src/SpanishTitleLocalizationHandler.h000066400000000000000000000007671345145022400245030ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 - 2011 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef SPANISH_TITLE_LOCALIZATION_HANDLER_H #define SPANISH_TITLE_LOCALIZATION_HANDLER_H #include "TitleLocalizationHandler.h" class SpanishTitleLocalizationHandler : public TitleLocalizationHandler { public: word_hint word_hint_for_word(const std::wstring &word, size_t index, size_t count, wchar_t after_punctuation) const; }; #endif fernandotcl-btag-a5d4440/src/StandardConsole.cpp000066400000000000000000000105731345145022400216370ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010, 2013, 2015 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include #include #include #include "config.h" #include "number_cast.h" #include "StandardConsole.h" EditLine *StandardConsole::m_editline; std::wstring StandardConsole::m_editline_prompt; wchar_t *StandardConsole::editline_prompt_callback(EditLine *editline) { return const_cast(m_editline_prompt.c_str()); } StandardConsole::StandardConsole() { m_editline = el_init(PROGRAM_NAME, stdin, stdout, stderr); assert(m_editline); el_wset(m_editline, EL_EDITOR, L"emacs"); el_wset(m_editline, EL_PROMPT, &StandardConsole::editline_prompt_callback); } StandardConsole::~StandardConsole() { el_end(m_editline); } bool StandardConsole::ask_yes_no_question(const std::wstring &question, const boost::optional &default_answer) { std::wstring yn; if (default_answer) { yn = *default_answer ? L"Y/n" : L"y/N"; } else { yn = L"y/n"; } #ifdef __APPLE__ printf("%S [%S]? ", question.c_str(), yn.c_str()); fflush(stdout); #else std::wcout << question << " [" << yn << "]? "; std::wcout.flush(); #endif for (;;) { wchar_t c = 0; el_wgetc(m_editline, &c); if (c == L'\n' && default_answer) { std::wcout << std::endl; return *default_answer; } else if (c == L'y' || c == L'Y') { std::wcout << c << std::endl; return true; } else if (c == L'n' || c == L'N') { std::wcout << c << std::endl; return false; } } } std::wstring StandardConsole::ask_string_question(const std::wstring &question, const boost::optional &default_answer, const Validator *validator) { for (;;) { m_editline_prompt = question + L": "; if (default_answer) el_wpush(m_editline, default_answer->c_str()); int length; const wchar_t *response = el_wgets(m_editline, &length); if (length > 1) return std::wstring(response, length - 1); std::cout << "Please enter the requested information" << std::endl; } } int StandardConsole::ask_number_question(const std::wstring &question, const boost::optional &default_answer, const Validator *validator) { for (;;) { m_editline_prompt = question + L": "; std::wstring default_answer_str; if (default_answer) { default_answer_str = number_cast(*default_answer); el_wpush(m_editline, default_answer_str.c_str()); } int length; const wchar_t *response = el_wgets(m_editline, &length); if (length <= 1) { std::cout << "Please enter the requested information" << std::endl; continue; } int number; try { number = boost::lexical_cast(std::wstring(response, length - 1)); } catch (boost::bad_lexical_cast) { std::cout << "Please enter a valid number" << std::endl; continue; } if (validator) { boost::optional error_message; if (validator->validate(number, error_message)) return number; if (error_message) #ifdef __APPLE__ fprintf(stderr, "%S\n", error_message->c_str()); #else std::wcout << *error_message << std::endl; #endif else std::cout << "Unknown validation error" << std::endl; continue; } else { return number; } } } void StandardConsole::display_info_message(const std::string &message) { std::cout << message << std::endl; } void StandardConsole::display_info_message(const std::wstring &message) { #ifdef __APPLE__ printf("%S\n", message.c_str()); #else std::wcout << message << std::endl; #endif } void StandardConsole::display_warning_message(const std::string &message) { std::cerr << "WARNING: " << message << std::endl; } void StandardConsole::display_warning_message(const std::wstring &message) { #ifdef __APPLE__ fprintf(stderr, "WARNING: %S\n", message.c_str()); #else std::wcerr << L"WARNING: " << message << std::endl; #endif } fernandotcl-btag-a5d4440/src/StandardConsole.h000066400000000000000000000025101345145022400212740ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010, 2013 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef STANDARD_CONSOLE_H #define STANDARD_CONSOLE_H #include #include #include "InteractiveTerminal.h" class StandardConsole : public InteractiveTerminal { public: StandardConsole(); ~StandardConsole(); bool ask_yes_no_question(const std::wstring &question, const boost::optional &default_answer); std::wstring ask_string_question(const std::wstring &question, const boost::optional &default_answer, const Validator *validator = NULL); int ask_number_question(const std::wstring &question, const boost::optional &default_answer, const Validator *validator = NULL); void display_info_message(const std::string &message); void display_info_message(const std::wstring &message); void display_warning_message(const std::string &message); void display_warning_message(const std::wstring &message); private: static wchar_t *editline_prompt_callback(EditLine *editline); static EditLine *m_editline; static std::wstring m_editline_prompt; }; #endif fernandotcl-btag-a5d4440/src/TitleCapitalizationFilter.cpp000066400000000000000000000123501345145022400236720ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include #include #include #include "TitleCapitalizationFilter.h" #include "TitleLocalizationHandler.h" bool TitleCapitalizationFilter::is_punctuation(wchar_t c) const { wchar_t *punctuation_list = m_handler->punctuation_list(); for (int i = 0; punctuation_list[i]; ++i) { if (c == punctuation_list[i]) return true; } return false; } std::wstring TitleCapitalizationFilter::filter(const std::wstring &input) const { std::wstring processed = BasicStringFilter::filter(input); boost::char_separator separator(L" "); boost::tokenizer, std::wstring::const_iterator, std::wstring > tokens(processed, separator); int num_words = 0; std::list > elements; BOOST_FOREACH(const std::wstring &token, tokens) { if (token.empty()) continue; // Look for all punctuation to the left of the token std::wstring punctuation_left; BOOST_FOREACH(wchar_t c, token) { if (!is_punctuation(c)) break; punctuation_left += c; } // Handle hanging punctuation if (punctuation_left.size() == token.size()) { boost::shared_ptr e(new element(ELEMENT_TYPE_PUNCTUATION_HANGING, token)); elements.push_back(e); continue; } // Add the punctuation to the left to the list if (!punctuation_left.empty()) { boost::shared_ptr e(new element(ELEMENT_TYPE_PUNCTUATION_LEFT, punctuation_left)); elements.push_back(e); } // Check if this could be an acronym std::wstring acronym(token.substr(punctuation_left.size())); bool is_acronym = m_handler->is_acronym(acronym); // Look for all punctuation to the right of the token std::wstring punctuation_right; if (!is_acronym) { BOOST_REVERSE_FOREACH(wchar_t c, token) { if (!is_punctuation(c)) break; punctuation_right = c + punctuation_right; } } // Find the word between the punctuation and add it to the list size_t word_size = token.size() - punctuation_left.size() - punctuation_right.size(); std::wstring word(token.substr(punctuation_left.size(), word_size)); elements.push_back(boost::shared_ptr(new element(ELEMENT_TYPE_WORD, word))); ++num_words; // Add the punctuation to the right to the list if (!punctuation_right.empty()) { boost::shared_ptr e(new element(ELEMENT_TYPE_PUNCTUATION_RIGHT, punctuation_right)); elements.push_back(e); } } std::wstring res; res.reserve(processed.size()); bool last_was_punctuation_left = false; int word_index = 0; boost::shared_ptr last_e; BOOST_FOREACH(boost::shared_ptr e, elements) { if (e->type == ELEMENT_TYPE_WORD) { // Lowercase the string BOOST_FOREACH(wchar_t &c, e->text) c = lowercase(c); // Get a hint wchar_t after_punctuation = last_e.get() && last_e->type != ELEMENT_TYPE_WORD ? last_e->text[last_e->text.size() - 1] : 0; TitleLocalizationHandler::word_hint hint = m_handler->word_hint_for_word(e->text, word_index++, num_words, after_punctuation); // Apply it switch (hint.style) { case TitleLocalizationHandler::WORD_STYLE_LOWER: break; case TitleLocalizationHandler::WORD_STYLE_UPPER: BOOST_FOREACH(wchar_t &c, e->text) c = uppercase(c); break; case TitleLocalizationHandler::WORD_STYLE_FIRST_UPPER: e->text[0] = uppercase(e->text[0]); break; case TitleLocalizationHandler::WORD_STYLE_CUSTOM: for (size_t i = 0; i < hint.uppercase->size(); ++i) { if ((*hint.uppercase)[i]) e->text[i] = uppercase(e->text[i]); } break; } } // Append to the result switch (e->type) { case ELEMENT_TYPE_WORD: if (!last_was_punctuation_left && !res.empty()) res += L' '; res += e->text; last_was_punctuation_left = false; break; case ELEMENT_TYPE_PUNCTUATION_RIGHT: res += e->text; last_was_punctuation_left = false; break; case ELEMENT_TYPE_PUNCTUATION_LEFT: if (!res.empty()) res += L' '; res += e->text; last_was_punctuation_left = true; break; case ELEMENT_TYPE_PUNCTUATION_HANGING: if (!res.empty()) res += L' '; res += e->text; last_was_punctuation_left = false; break; } last_e = e; } return res; } fernandotcl-btag-a5d4440/src/TitleCapitalizationFilter.h000066400000000000000000000021001345145022400233270ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef TITLE_CAPITALIZATION_FILTER_H #define TITLE_CAPITALIZATION_FILTER_H #include "CapitalizationFilter.h" #include "TitleLocalizationHandler.h" class TitleCapitalizationFilter : public CapitalizationFilter { public: TitleCapitalizationFilter() : m_handler(NULL) {} void set_localization_handler(TitleLocalizationHandler *handler) { m_handler = handler; } std::wstring filter(const std::wstring &input) const; private: enum element_type { ELEMENT_TYPE_WORD, ELEMENT_TYPE_PUNCTUATION_LEFT, ELEMENT_TYPE_PUNCTUATION_RIGHT, ELEMENT_TYPE_PUNCTUATION_HANGING }; struct element { element_type type; std::wstring text; element(element_type t, const std::wstring &x) : type(t), text(x) {} }; bool is_punctuation(wchar_t c) const; TitleLocalizationHandler *m_handler; }; #endif fernandotcl-btag-a5d4440/src/TitleLocalizationHandler.cpp000066400000000000000000000025641345145022400235050ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include "TitleLocalizationHandler.h" bool TitleLocalizationHandler::is_acronym(const std::wstring &word) const { if (word.size() < 4 || word[word.size() - 1] != L'.') return false; bool should_be_dot = false; BOOST_FOREACH(wchar_t c, word) { if (should_be_dot && c != L'.') return false; else if (!should_be_dot && c == L'.') return false; else should_be_dot = !should_be_dot; } return true; } wchar_t *TitleLocalizationHandler::punctuation_list() const { static wchar_t punctuation[] = { L',', L'.', L':', L'-', L';', L'?', L'\u00bf', // Unicode INVERTED QUESTION MARK (¿) L'!', L'\u00a1', // Unicode INVERTED EXCLAMATION MARK (¡) L'-', L'\u2014', // Unicode EM DASH (—) L')', L'(', L'"', L'\'', L'\u201c', // Unicode LEFT DOUBLE QUOTATION MARK (“) L'\u201d', // Unicode RIGHT DOUBLE QUOTATION MARK (”) L'\u2018', // Unicode LEFT SINGLE QUOTATION MARK (‘) L'\u2019', // Unicode RIGHT SINGLE QUOTATION MARK (’) 0 }; return punctuation; } fernandotcl-btag-a5d4440/src/TitleLocalizationHandler.h000066400000000000000000000020671345145022400231500ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef TITLE_LOCALIZATION_HANDLER_H #define TITLE_LOCALIZATION_HANDLER_H #include #include #include class TitleLocalizationHandler { public: enum word_style { WORD_STYLE_LOWER, WORD_STYLE_UPPER, WORD_STYLE_FIRST_UPPER, WORD_STYLE_CUSTOM }; struct word_hint { word_style style; boost::optional > uppercase; word_hint(word_style s) : style(s) {} word_hint(const std::vector &u) : style(WORD_STYLE_CUSTOM), uppercase(u) {} }; virtual ~TitleLocalizationHandler() {} virtual word_hint word_hint_for_word(const std::wstring &word, size_t index, size_t count, wchar_t after_punctuation) const = 0; virtual bool is_acronym(const std::wstring &word) const; virtual wchar_t *punctuation_list() const; }; #endif fernandotcl-btag-a5d4440/src/UnixRenamingFilter.h000066400000000000000000000010171345145022400217640ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef UNIX_RENAMING_FILTER_H #define UNIX_RENAMING_FILTER_H #include "RenamingFilter.h" class UnixRenamingFilter : public RenamingFilter { private: bool is_character_allowed(wchar_t c) const { return c != L'/' && c != L'\0'; } wchar_t replacement_character(wchar_t c) const { return L'_'; } }; #endif fernandotcl-btag-a5d4440/src/config.h.in000066400000000000000000000001571345145022400200700ustar00rootroot00000000000000#ifndef CONFIG_H #define CONFIG_H #cmakedefine CUESHEET_SUPPORT #define PROGRAM_NAME "@PROGRAM_NAME@" #endif fernandotcl-btag-a5d4440/src/main.cpp000066400000000000000000000170651345145022400175030ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 - 2012 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include #include #include #include #include #include #include #include #include #include "BasicStringFilter.h" #include "config.h" #include "ConservativeRenamingFilter.h" #include "EnglishTitleLocalizationHandler.h" #include "InteractiveTagger.h" #include "RenamingFilter.h" #include "SimpleCapitalizationFilter.h" #include "SpanishTitleLocalizationHandler.h" #include "StandardConsole.h" #include "TitleCapitalizationFilter.h" #include "TitleLocalizationHandler.h" #include "UnixRenamingFilter.h" static void print_usage(std::ostream &out) { out << "\ Usage: \n\ " PROGRAM_NAME " [options] path1 [path2] [path3] ...\n\ " PROGRAM_NAME " --help\ " << std::endl; } static BasicStringFilter *select_string_filter(const std::string &filter) { if (filter == "basic") return new BasicStringFilter; else if (filter == "title") return new TitleCapitalizationFilter; else if (filter == "lower") return new SimpleCapitalizationFilter(SimpleCapitalizationFilter::CAPITALIZATION_MODE_ALL_LOWER); else if (filter == "upper") return new SimpleCapitalizationFilter(SimpleCapitalizationFilter::CAPITALIZATION_MODE_ALL_UPPER); else if (filter == "first-upper") return new SimpleCapitalizationFilter(SimpleCapitalizationFilter::CAPITALIZATION_MODE_FIRST_UPPER); else return NULL; } static TitleLocalizationHandler *select_title_localization_handler(const std::string &locale) { if (locale == "en") return new EnglishTitleLocalizationHandler; else if (locale == "es") return new SpanishTitleLocalizationHandler; else return NULL; } static RenamingFilter *select_renaming_filter(const std::string &filter) { if (filter == "unix") return new UnixRenamingFilter; else if (filter == "conservative") return new ConservativeRenamingFilter; else return NULL; } int main(int argc, char **argv) { // Set the global locale for case conversion purposes std::ios_base::sync_with_stdio(false); boost::locale::generator gen; std::locale locale(gen("")); std::locale::global(locale); std::wcout.imbue(locale); std::wcin.imbue(locale); struct option long_options[] = { {"always-ask-track", no_argument, NULL, 0}, #ifdef CUESHEET_SUPPORT {"cue-sheet", required_argument, NULL, 'C'}, {"cue-sheet-encoding", required_argument, NULL, 0}, #endif {"dir-rename-format", required_argument, NULL, 'd'}, {"dry-run", no_argument, NULL, 'D'}, {"input-filter", required_argument, NULL, 'i'}, {"file-rename-format", required_argument, NULL, 'r'}, {"filter", required_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, {"output-filter", required_argument, NULL, 'o'}, {"renaming-filter", required_argument, NULL, 'n'}, {"title-locale", required_argument, NULL, 't'}, {NULL, 0, NULL, 0} }; // Create the interactive tagger InteractiveTagger itag; boost::scoped_ptr input_filter, output_filter; boost::scoped_ptr title_localization_handler; boost::scoped_ptr renaming_filter; // Create the interactive terminal StandardConsole console; itag.set_terminal(&console); // Cue sheet filenames and encoding std::list cue_filenames; std::string cue_encoding("ISO-8859-1"); // Parse the command line options int opt, option_index; #ifdef CUESHEET_SUPPORT while ((opt = getopt_long(argc, argv, "C:Dd:i:f:o:hn:r:t:", long_options, &option_index)) != -1) { #else while ((opt = getopt_long(argc, argv, "Dd:i:f:o:hn:r:t:", long_options, &option_index)) != -1) { #endif switch (opt) { #ifdef CUESHEET_SUPPORT case 'C': cue_filenames.push_back(optarg); break; #endif case 'D': itag.set_dry_run(); break; case 'd': itag.set_dir_rename_format(optarg); break; case 'f': input_filter.reset(select_string_filter(optarg)); output_filter.reset(select_string_filter(optarg)); if (!input_filter.get() || !output_filter.get()) { print_usage(std::cerr); return EXIT_FAILURE; } break; case 'i': input_filter.reset(select_string_filter(optarg)); if (!input_filter.get()) { print_usage(std::cerr); return EXIT_FAILURE; } break; case 'h': print_usage(std::cout); return EXIT_SUCCESS; case 'o': output_filter.reset(select_string_filter(optarg)); if (!output_filter.get()) { print_usage(std::cerr); return EXIT_FAILURE; } break; case 'n': renaming_filter.reset(select_renaming_filter(optarg)); if (!renaming_filter.get()) { print_usage(std::cerr); return EXIT_FAILURE; } break; case 'r': itag.set_file_rename_format(optarg); break; case 't': title_localization_handler.reset(select_title_localization_handler(optarg)); if (!title_localization_handler.get()) { print_usage(std::cerr); return EXIT_FAILURE; } break; case 0: if (!strcmp(long_options[option_index].name, "always-ask-track")) itag.set_ask_track(); #ifdef CUESHEET_SUPPORT else if (!strcmp(long_options[option_index].name, "cue-sheet-encoding")) cue_encoding = optarg; #endif break; default: print_usage(std::cerr); return EXIT_FAILURE; } } // We need at least one path if (optind >= argc) { print_usage(std::cerr); return EXIT_FAILURE; } #ifdef CUESHEET_SUPPORT // Load the cue sheet if needed if (!cue_filenames.empty()) itag.load_cue_sheets(cue_filenames, cue_encoding); #endif // Add the title localization handler if (!title_localization_handler.get()) title_localization_handler.reset(new EnglishTitleLocalizationHandler); TitleCapitalizationFilter *filter = dynamic_cast(input_filter.get()); if (filter) filter->set_localization_handler(title_localization_handler.get()); filter = dynamic_cast(output_filter.get()); if (filter) filter->set_localization_handler(title_localization_handler.get()); // Add the filters if (!input_filter.get()) input_filter.reset(new BasicStringFilter); itag.set_input_filter(input_filter.get()); itag.set_output_filter(output_filter.get()); // Add the renaming filter if (!renaming_filter.get()) renaming_filter.reset(new UnixRenamingFilter); itag.set_renaming_filter(renaming_filter.get()); // Perform the interactive tagging itag.tag(argc - optind, (const char **)&argv[optind]); return EXIT_SUCCESS; } fernandotcl-btag-a5d4440/src/number_cast.h000066400000000000000000000006471345145022400205240ustar00rootroot00000000000000/* * This file is part of btag. * * © 2012 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef NUMBER_CAST_H #define NUMBER_CAST_H #include #include #include template inline std::wstring number_cast(T arg) { std::wostringstream oss; oss.imbue(std::locale::classic()); oss << arg; return oss.str(); } #endif fernandotcl-btag-a5d4440/src/validators.h000066400000000000000000000021031345145022400203570ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef VALIDATORS_H #define VALIDATORS_H #include "InteractiveTerminal.h" class YearValidator : public InteractiveTerminal::Validator { public: bool validate(const int &year, boost::optional &error_message) const { if (year > 2500 || year < 1000) { error_message = L"The year should be a number between 1000 and 2500"; return false; } else { return true; } } }; class TrackValidator : public InteractiveTerminal::Validator { public: bool validate(const int &track, boost::optional &error_message) const { if (track > 1000 || track < 1) { error_message = L"The track should be a number between 1 and 1000"; return false; } else { return true; } } }; #endif fernandotcl-btag-a5d4440/src/wide_string_cast.h000066400000000000000000000012021345145022400215360ustar00rootroot00000000000000/* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef WIDE_STRING_CAST_H #define WIDE_STRING_CAST_H #include #include #include #include #include namespace boost { template<> inline std::wstring lexical_cast(const std::string &arg) { std::wstring result; BOOST_FOREACH(char c, arg) result += std::use_facet >(std::locale()).widen(c); return result; } } #endif fernandotcl-btag-a5d4440/tests/000077500000000000000000000000001345145022400164155ustar00rootroot00000000000000fernandotcl-btag-a5d4440/tests/CMakeLists.txt000066400000000000000000000034151345145022400211600ustar00rootroot00000000000000include_directories( ${Boost_INCLUDE_DIRS} ${PROJECT_SOURCE_DIR}/src) add_executable( english_titles_test english_titles.cpp titles_base.cpp titles_base.h ${PROJECT_SOURCE_DIR}/src/BasicStringFilter.cpp ${PROJECT_SOURCE_DIR}/src/BasicStringFilter.h ${PROJECT_SOURCE_DIR}/src/CapitalizationFilter.h ${PROJECT_SOURCE_DIR}/src/EnglishTitleLocalizationHandler.cpp ${PROJECT_SOURCE_DIR}/src/EnglishTitleLocalizationHandler.h ${PROJECT_SOURCE_DIR}/src/TitleCapitalizationFilter.cpp ${PROJECT_SOURCE_DIR}/src/TitleCapitalizationFilter.h ${PROJECT_SOURCE_DIR}/src/TitleLocalizationHandler.cpp ${PROJECT_SOURCE_DIR}/src/TitleLocalizationHandler.h ) target_link_libraries(english_titles_test ${Boost_LIBRARIES}) add_test(english_titles_test ${PROJECT_BINARY_DIR}/tests/english_titles_test ${PROJECT_SOURCE_DIR}/tests/data/english_titles.txt) add_executable( spanish_titles_test spanish_titles.cpp titles_base.cpp titles_base.h ${PROJECT_SOURCE_DIR}/src/BasicStringFilter.cpp ${PROJECT_SOURCE_DIR}/src/BasicStringFilter.h ${PROJECT_SOURCE_DIR}/src/CapitalizationFilter.h ${PROJECT_SOURCE_DIR}/src/SpanishTitleLocalizationHandler.cpp ${PROJECT_SOURCE_DIR}/src/SpanishTitleLocalizationHandler.h ${PROJECT_SOURCE_DIR}/src/TitleCapitalizationFilter.cpp ${PROJECT_SOURCE_DIR}/src/TitleCapitalizationFilter.h ${PROJECT_SOURCE_DIR}/src/TitleLocalizationHandler.cpp ${PROJECT_SOURCE_DIR}/src/TitleLocalizationHandler.h ) target_link_libraries(spanish_titles_test ${Boost_LIBRARIES}) add_test(spanish_titles_test ${PROJECT_BINARY_DIR}/tests/spanish_titles_test ${PROJECT_SOURCE_DIR}/tests/data/spanish_titles.txt) add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure) fernandotcl-btag-a5d4440/tests/data/000077500000000000000000000000001345145022400173265ustar00rootroot00000000000000fernandotcl-btag-a5d4440/tests/data/english_titles.txt000066400000000000000000000023651345145022400231120ustar00rootroot00000000000000# # This was initialized with a list of best-sellers found on # Wikipedia (though only those that we should handle well). # Feel free to extend it with whatever content. # # Blank lines and lines starting with a hash are ignored, but # our parser is very basic, so don't get creative. # # Keep this file in UTF-8. Not sure if having the BOM is a # good thing (avoid it if possible). # A Tale of Two Cities The Lord of the Rings The Hobbit Dream of the Red Chamber And Then There Were None The Lion, the Witch and the Wardrobe Think and Grow Rich The Catcher in the Rye The Alchemist Steps to Christ Heidi's Years of Wandering and Learning The Common Sense Book of Baby and Child Care Anne of Green Gables The Name of the Rose The Hite Report Shere Hite The Tale of Peter Rabbit The Ginger Man Harry Potter and the Deathly Hallows Jonathan Livingston Seagull A Message to Garcia Angels and Demons How the Steel Was Tempered War and Peace You Can Heal Your Life Kane and Abel Sophie's World The Very Hungry Caterpillar The Girl with the Dragon Tattoo The Thorn Birds The Purpose Driven Life One Hundred Years of Solitude The Diary of a Young Girl Valley of the Dolls In His Steps: What Would Jesus Do? The Revolt of Mamie Stover Gone with the Wind To Kill a Mockingbird fernandotcl-btag-a5d4440/tests/data/spanish_titles.txt000066400000000000000000000036031345145022400231220ustar00rootroot00000000000000# # This was initialized with a list of best-sellers found on # Wikipedia (though only those that we should handle well). # Feel free to extend it with whatever content. # # Blank lines and lines starting with a hash are ignored, but # our parser is very basic, so don't get creative. # # Keep this file in UTF-8. Not sure if having the BOM is a # good thing (avoid it if possible). # Historia de Dos Ciudades El Hobbit Sueño en el Pabellón Rojo Triple Representatividad Diez Negritos El León, la Bruja y el Armario Ella El Principito El Guardián Entre el Centeno El Alquimista Heidi El Libro del Sentido del Común del Cuidado de Bebés y Niños El Nombre de la Rosa Ángeles y Demonios Así se Templó el Acero Guerra y Paz Usted Puede Sanar su Vida Matar a un Ruiseñor El Valle de las Muñecas Lo que el Viento se Llevó Cien Años de Soledad Una Vida con Propósito El Pájaro Espino Piense y Hágase Rico Los Hombres que No Amaban a las Mujeres La Oruguita Glotona ¿Quién se Ha Llevado mi Queso? El Viento en los Sauces 1984 La Prostituta Feliz Tiburón Siempre te Querré La Habitación de las Mujeres Qué Esperar Cuando se Está Esperando Donde Viven los Monstruos El Secreto Miedo a Volar Adivina Cuánto te Quiero Los Siete Hábitos de las Personas Altamente Efectivas Cómo Ganar Amigos e Influir Sobre las Personas El Perfume: Historia de un Asesino El Hombre que Susurraba al Oído de los Caballos La Sombra del Viento La Cabaña Guía del Autoestopista Galáctico Martes con mi Viejo Profesor Donde el Corazón te Lleve Rebeldes Charlie y la Fábrica de Chocolate La Peste Indigno de Ser Humano El Mono Desnudo Todo se Desmorona El Profeta El Exorcista El Grúfalo Trampa-22 La Isla de las Tormentas Historia del Tiempo El Gato con Sombrero Desde mi Cielo Cisnes Salvajes La Noche Cometas en el Cielo La Mujer Total Historia del Futuro, la Sociedad del Conocimiento ¿De Qué Color Es su Paracaídas? fernandotcl-btag-a5d4440/tests/english_titles.cpp000066400000000000000000000005501345145022400221360ustar00rootroot00000000000000/* * This file is part of btag. * * © 2012 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include "EnglishTitleLocalizationHandler.h" #include "titles_base.h" int main(int argc, char **argv) { EnglishTitleLocalizationHandler handler; return run_title_capitalization_tests(handler, argc, argv); } fernandotcl-btag-a5d4440/tests/spanish_titles.cpp000066400000000000000000000005501345145022400221520ustar00rootroot00000000000000/* * This file is part of btag. * * © 2012 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include "SpanishTitleLocalizationHandler.h" #include "titles_base.h" int main(int argc, char **argv) { SpanishTitleLocalizationHandler handler; return run_title_capitalization_tests(handler, argc, argv); } fernandotcl-btag-a5d4440/tests/titles_base.cpp000066400000000000000000000054611345145022400214250ustar00rootroot00000000000000/* * This file is part of btag. * * © 2011-2012 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include #include #include #include #include "titles_base.h" int run_title_capitalization_tests(TitleLocalizationHandler &handler, int argc, char **argv) { // Find the path to the test data if (argc != 2) { std::cerr << "You need to supply the path to test data file in the command line" << std::endl; return EXIT_FAILURE; } // Open the test data file std::wifstream f(argv[1]); if (!f.is_open()) { std::cerr << "Failed to open the test data file" << std::endl; return EXIT_FAILURE; } // Force an UTF-8 locale std::ios_base::sync_with_stdio(false); boost::locale::generator gen; std::locale locale(gen("en_US.UTF-8")); f.imbue(locale); std::wcerr.imbue(locale); // Set up the capitalization filter TitleCapitalizationFilter filter; filter.set_localization_handler(&handler); int errors = 0, correct_lines = 0, lineno = 1; std::wstring line; for (;;) { // Get a line std::getline(f, line); if (f.eof()) break; else if (f.fail()) { std::wcerr << L"Error reading line from file" << std::endl; return EXIT_FAILURE; } // Check if we can skip it std::wstring::size_type first = line.find_first_not_of(L" \t\n\r"); if (first == std::wstring::npos || line[first] == L'#') { ++lineno; continue; } // Trim it std::wstring::size_type last = line.find_last_not_of(L" \t\n\r"); std::wstring correctStr(line.substr(first, last - first + 1)); // Get lowercase and uppercase versions of the string std::wstring lowerStr(boost::algorithm::to_lower_copy(correctStr)); std::wstring upperStr(boost::algorithm::to_upper_copy(correctStr)); // Apply the localization handler std::wstring lowerFiltered(filter.filter(lowerStr)); std::wstring upperFiltered(filter.filter(upperStr)); // Make sure they match bool has_error = false; if (lowerFiltered != correctStr) { std::wcerr << lineno << L": Incorrect conversion from lowercase" << std::endl; has_error = true; ++errors; } if (upperFiltered != correctStr) { std::wcerr << lineno << L": Incorrect conversion from uppercase" << std::endl; has_error = true; ++errors; } // Increment the counters ++lineno; if (!has_error) ++correct_lines; } return correct_lines > 10 && errors == 0 ? EXIT_SUCCESS : EXIT_FAILURE; } fernandotcl-btag-a5d4440/tests/titles_base.h000066400000000000000000000005131345145022400210630ustar00rootroot00000000000000/* * This file is part of btag. * * © 2012 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef TITLES_BASE_H #define TITLES_BASE_H #include "TitleCapitalizationFilter.h" int run_title_capitalization_tests(TitleLocalizationHandler &handler, int argc, char **argv); #endif