pax_global_header00006660000000000000000000000064131416710230014510gustar00rootroot0000000000000052 comment=b66a2898c1479b305e7dda8352c81b1efa83fbe4 lltag-lltag-0.14.6/000077500000000000000000000000001314167102300140045ustar00rootroot00000000000000lltag-lltag-0.14.6/COPYING000066400000000000000000000430761314167102300150510ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 675 Mass Ave, Cambridge, MA 02139, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS Appendix: How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 19yy This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) 19yy name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. lltag-lltag-0.14.6/Changes000066400000000000000000000576101314167102300153100ustar00rootroot00000000000000lltag (0.14.6) * The lltag website has moved to http://bgoglin.free.fr/lltag * The repository moved to http://github.com/bgoglin/lltag * The mailing list is now lltag@googlegroups.com -- Brice Goglin Tue, 06 Aug 2017 21:45:00 +0200 lltag (0.14.5) * When renaming, use tags even if their name is not upper-case as usual (reported by Mathieu Roy in Debian bug #666677). -- Brice Goglin Tue, 02 Aug 2016 22:15:00 +0100 lltag (0.14.4) * Fix a typo in usage (reported by Jonas Kölker in Gna! bug #17836). * Fix miscellaneous typos everywhere, reported by Debian's lintian. -- Brice Goglin Tue, 02 Aug 2011 13:32:00 +0100 lltag (0.14.3) * Add -T and --preserve-time, and the preserve_time configuration file option, to preserve file modification time during tagging (requested by Adam Rosi-Kessel in Gna! bug #12367). * Do not try to display binary data tags such as cover front picture (requested by Alexandre Buisse). * Do not try to initialize readline in non-interactive environment, only fail if readline is actually needed. Thanks Miroslaw Zalewski in Debian bug #558831. * Do not apply colors or bold/underline formatting to output messages in non-interactive environment. * Try command-line given user formats (with -F) before those given in the config file (with 'format = ...'). * Add %n) %a - %t to the internal format database. * Add --id3v2, synonym for --mp3v2 (requested by Alexandre Buisse). * Display basename parsers both with and without path parsers when listing internal parsers. * Display the usage when there is nothing to do (requested by Alexandre Buisse). * Fix the case of some options in the manpage. -- Brice Goglin Sun, 21 Mar 2010 00:06:00 +0100 lltag (0.14.2) * Merge the whole concept of default and additional values into "explicit tag values" which may be set with either -a, -A, -t, -n, -d, -g and -c or the generic --tag command line option. The default_* configuration options are removed. Based on complaints from Alexandre Buisse and Olivier Schwander. + Fix the way they are exported in the config file. * Replace slashes with dashes before renaming (reported by Mark McEver in Gna! bug #10127). + Add --rename-slash to change dash into any other string. * Fix track numbers into actual numbers (without track total). -- Brice Goglin Sun, 16 Dec 2007 12:27:00 +0200 lltag (0.14.1) * Make sure the last character of user-provided format is correctly escaped (reported by Benjamin Saunders). * Keep CDDB tags in UTF-8 when using a UTF-8 locale (reported by Tino Keitel in Debian bug #418951). * Set verbose level to 0 by default so that menu usage information is not displayed unless the user explicitly request it, either by passing -v on the command, or by typing 'h' in a menu (requested by Alexandre Buisse). + Fix documentation about verbosity levels in the manpages. * Fix typo in lltag.1 (reported by Georg Neis in Debian bug #438795). * Fix typo in lltag_config.5 (reported by Emmanuel Jeandel). -- Brice Goglin Sat, 22 Sep 2007 11:25:00 +0200 lltag (0.14) * Add an optional ID3v2 tag support for MP3 files using MP3::Tag (requested by lots of people), enabled with --mp3v2 for now. + Add --mp3read option to specify whether ID3v1 and v2 should be read, and in which order (default is 21). * Large rework of the core loop: + Display existing tags with the new ones to be set. + Add O to display existing tags, R to revert to them, Z to reset to no tags at all, and n to skip tagging and jump to renaming. + Also try internal parsers when parsing from the main menu even if -G was not given on the command line. + Support renaming from existing tags without any need to parse and/or reapply tags when --rename and --no-tagging are passed with nothing else (requested by Alexandre Buisse). + Make sure guessing is enabled as default only when there is really nothing else to do. + Make sure we apply additional values to new tags before merging and defaults at the very end, so that --clear and --append are correctly processed. * Tag edition improvements: + Support edition of existing tags and tags with multiple values. + Support edition of all CD tags returned by CDDB. + Add -E/--edit (and edit option in the configuration file) to edit tags immediately. * Make the old MP3 backend more safe: + Check genre and tracknumbers and ignore invalid values to avoid mp3info failures. + Add warnings about multiple values and special tags that are not supported. * Add -q/--quiet to reduce verbosity, useful to disable displaying of menu usage when a menu appears for the first time (requested by Alexandre Buisse). + The verbose option in the config file is now a verbose level, with its default being 1. * Miscellaneous fixes: + Support removing of all tags. + Handle Ctrl-d in interactive menus and make it cancel the current operation without quitting, as opposed to Ctrl-c (requested by Alexandre Buisse). + Uniformize the letter to enter to view current values (changed 'v' into 'V' in the CDDB menus). * Documentation updates and fixes: + Add an EXAMPLES section at the end of the lltag.1 manpage (requested by Stefano Sabatini in Debian bug #406213) and also insert more basic command lines in the howto.html documentation. + Explain the difference between default and additional values in the howto. + Explain how the strategies are used, how new tags are generated and applied, in the DESCRIPTION section of lltag.1. + Have the lltag_config.5 manpage also point to the config template file provided in the documentation directory. * Lots of other minor fixes. -- Brice Goglin Mon, 09 Apr 2007 11:22:00 +0200 lltag (0.13.1) * Multiple fixes regarding parsing: + Try to apply internal parsers immediately after reading the formats file and catch error gracefully instead of dying later horribly. + Catch errors gracefully when actually parsing filenames too. + Report the beginning line (instead of a later one) in the formats file when meeting an invalid parser. + Check that the formats given either with -F or in the formats file have correct number of fields to match, and that we actually get the same number of fields after matching. + Escape []() correctly in internal parsers (reported by David Mohr). + Print the regexp when listing internal parsers in verbose mode (requested by David Mohr). + Improve documentation about the impact of --spaces on internal parsers. + Add 2 new internal formats ("%a/%A [%d]" and "%a/%A (%d)") by David Mohr. * Multiple cleanup in the documentation: + Install the config file in the doc/ directory instead of /etc/lltag/ since it only contains documentation, no actual configuration. + Move configuration option documentation into the new lltag_config.5 manpage, and the documentation of internal formats database file in lltag_formats.5. + Add install-man, uninstall-man, install-doc and uninstall-doc targets in the Makefile to install manpages and documentation on demand. + Some fixes in the manpage. * Hack the parameters that are passed during Perl modules installation to deal with MakeMaker not using PREFIX in the common way. Without this change, setting PREFIX=/usr/local on the make install command line was installing in /usr/local/local/perl (reported by David Mohr). -- Brice Goglin Fri, 01 Dec 2006 18:22:00 +0100 lltag (0.13) * Fix CDDB, was broken since the HTTP search interface of freedb.org is unavailable for now (closes Hamish Moffatt's Debian bug #397626). + CDDB now uses TrackType.org instead of freedb.org. - Reorganize the CDDB code to make it easy to use other online CDDB site, using kind of a backend interface. + Rewrite HTTP requests using libwww-perl. + Drop --cddb-proxy and use the HTTP_PROXY variable in the environment (set it to something like "http://my.proxy.com"). + libwww-perl (LWP) is now recommended since required for CDDB access. * Several important fixes regarding internal tag management: + --clear, --append, additional tags (passed with --tag) + Handling of multiple values for the same tag. - When tags have multiple value, do not keep the same value twice. * Improve manpage and documentation: + Add "files..." to the synopsis in the manpage since lltag only operates on files that are given on the command line (not on the whole directory) and add a message in verbose mode when no files are given (reported by Hamish Moffatt in Debian bug #397627). + Add the configuration file option name near the corresponding command line option in the manpage. + Add a note, near -g, about the fact that the genre string might have to match within a list of specified genres. + Add missing options in the comments in the config file. + Improve requirements in the README. + Main description at the beginning of the manpage. + Various fixes in the manpage. * --nopath (i.e. -p) is now --no-path (and no_path in the config file). * When --tag is passed, disable -G by default. * Change the internal backend API to manipulate hash of values instead of talking to external programs, to help upcoming backends. - Conversion from hash to external programs (command line and output stream) is moved to new helper functions for backends that need them. -- Brice Goglin Thu, 16 Nov 2006 22:53:00 +0100 lltag (0.12.2) * Fix %i processing (was broken since 0.12). * Fix regexp application. * Update the How-to about automatic CDDB and --cddb-query. * Improve error messaging. * Improve messaging in automatic mode. * Cleanup Perl regexps. -- Brice Goglin Mon, 23 Oct 2006 13:38:00 +0200 lltag (0.12.1) * Improve automatization of CDDB: + Add --cddb-query to search in CDDB automatically from the command-line with either keywords or category/id. + Add 'a' and ' a' to switch to automatic CDDB mode while interactively choosing a track in a CD. + Fix automization of CDDB with --yes. * When using 'a' to switch to automatic mode, only change the mode for the local menu (i.e. automatic parsing does not make automatic tagging or CDDB usage). * If CDDB query by keywords finds nothing, go back to keywords query instead of asking the user to choose in the (empty) list. * Add %F, %E and %P to be replaced by original basename, extension and path of the file when renaming. * Add --type to make file type selection more flexible. * Use DELETE or to erase a tag when editing. * Add a easy-to-use backend interface to help adding support for other file types. + Backend information (registrations and failures) are displayed in verbose mode -v). * Fix Perl modularization, no need to use Exporter module since we access modular functions by prefixing with the module name. * Improve some messages. -- Brice Goglin Tue, 03 Oct 2006 16:22:00 +0200 lltag (0.12) * Add CDDB support with -C or --cddb (or C in the main menu): + Search CD ids by keywords, with ability to choose categories and fields to look in. + Lookup a CD by giving its category and CD id directly. + Keep the previous CD contents in memory so that the next track might be tagged immediately. + Edit CDDB common tags before extracting a track from a CD. + Configurable HTTP CDDB server and proxy. * Rework interactive menus to integrate CDDB better: + Add a main menu from where CDDB, parsing and editing is called. + Use 'q' to leave submenus (parsing, CDDB, renaming and editing). + Use 'q' instead of 's' to skip a file from the main menu. + Use 'y' to exit editing after save the changes. + Add 'Q' to exit completely. + Do not match user replies case-insensitively. + Large rework of the manpage to document the new menus. * Some new entries in the How-To. * Do not write guess option in the generated configuration file unless explicitely asked by the user. * Add a doc/ subdirectory: + The How-To is now also included in the tarball. * Perl modularization. * Add the date to the program name when compiling it from SVN. -- Brice Goglin Tue, 19 Sep 2006 21:34:00 -0400 lltag (0.11) * Always read existing tags first, allowing to: + Rename using existing tags when the old filename is useless (requested by Fabien Wernli). In case of multiple occurence of a tag, the first one is used. + Suppport replacing, clearing, and appending well for all types: - Replacing is now the default for all types. - Appending is useless for mp3 since only one occurence (the first one) may be stored in the file. - Remove the CLEARING, REPLACING OR APPENDING manpage section. * Add --tag for additional tags, and warn when not storable in in mp3 files. * Now use %d, -d and DATE instead of %y, -y and YEAR since ogg/flac tags are dates. * Use %i instead of %d to ignore some text when matching. * Fix 'u' confirmation to try the current parser first for next files (was broken since 0.10). * 'basename' is now used instead of 'filename' for 'type' in the format file, 'filename' is still supported for now. * Do not rename if the new name already exists. * Fix reading/tagging the date of ogg/flac files. * Use all-capitals tag names to match ogg/flac. + default_ option is still supported in the config for now. * The indices in the format file may now be full field names instead of letters. * Print the default letter in <> instead of capital when confirming/editing. * Add EDITING TAGS in the manpage. * Add configuration options for the config file in the manpage. * Fix quoting of the command-line in --dry-run. * Large rework of the internal structures. * Major cleanup of the manpage. -- Brice Goglin Sat, 19 Mar 2006 23:56:00 -0500 lltag (0.10) * Use Readline library to make tag editing easier if the installed readline library is smart, with inline edition of existing tags and history of last entered values. * Added -S ans --show-tags to only show file tags instead of tagging (requested by Stephane Gimenez in Gna! bug #4601). * Add --regexp and --rename-regexp for basic regexp-replacing in tags (requested by Jonathan Worth Washington, Gna! bug #4604). * When renaming with a undefined track number, initialize it to 0. * When renaming with a track number, make it at least 2 digits (reported by Emmanuel Jeandel). * Always write renaming configuration options with --gencfg, even if renaming was not enabled. * Cleanup core function names and several messages so that lltag speak about matching instead of tagging. It avoids getting messages about tagging while --no-tagging was passed (requested by Stephane Gimenez). * Use ' instead of # to show spaces between command line argument with --dry-run (requested by Emmanuel Jeandel). * Allow to pass ()[] characters in the matching format without having to escape them. * Allow all %x in the renaming format when x is not a field letter. * Fix where the 'd' field (dummy) is allowed. * Fix the error when no formats file is found. * Fix typos in the manpage (reported by Emmanuel Jeandel). -- Brice Goglin Sat, 19 Nov 2005 02:41:00 -0500 lltag (0.9.1) * Large fix of internal database format files processing: + Fix formats file parsing (last format was omitted). + If $HOME/.lltag/formats exists, /etc/lltag/formats is ignored. + Fix documentation. * Add --config, --gencfg and /etc/lltag/config to configure lltag behavior (requested by Stephane Gimenez). * Do not warn when no file is passed. -- Brice Goglin Mon, 17 Oct 2005 20:51:00 +0200 lltag (0.9) * Add rename support with --rename (requested by Stephane Gimenez): + Add --rename-min to lowcase tags before renaming. + Add --rename-sep to replace spaces before renaming. + Add --rename-ext to assume the extension in provided in the rename format. + Add --no-tagging to rename file without actually tagging them. * When compiling from SVN, add a +svn suffix to the version. * Reorder usage in a comprehensible way. * Do not be case-sensitive when looking at file extensions to guess their type. -- Brice Goglin Sun, 2 Oct 2005 19:06:00 +0200 lltag (0.8) * Add FLAC support (requested by Stephane Gimenez). * Add --maj to upcase first letters in tags (requested by Stephane Gimenez). -- Brice Goglin Tue, 27 Sep 2005 23:03:00 +0200 lltag (0.7.2) * Fix compilation and installation when DESTDIR is overridden. * Fix make uninstall. * Add a VERSION file. * Update all contact addresses and URLs since the project is now hosted by Gna!. -- Brice Goglin Thu, 22 Sep 2005 10:40:00 +0200 lltag (0.7.1) * Add a warning (with reference to README) when system fails with ENOENT or EPERM. * Cleanup system usage. * Update contact address to gna.org and add it to README. -- Brice Goglin Wed, 7 Sep 2005 17:57:18 +0200 lltag (0.7) * Add --clear to force emptying of mp3 tags (default for ogg). * Add --append to force appending of ogg tags (default is overwrite, append is impossible for mp3). * Add a section about clearing, replacing or appending in the manpage. * Add 'comment' tag support (with -c or %c). * Add missing options to the command line in the man page. * Cleanup the way current values are shown, show and . * Accept both CLEAR and when editing fields. * Cleanup system usage when tagging command, get the output and show it in case of error. * Don't add () after sub prototypes. -- Brice Goglin Tue, 16 Aug 2005 00:39:18 +0200 lltag (0.6.2) * Fix Debian tarball generation. * Include COPYING and Changes in the tarball. * Add a README. * Document editing in the manpage (CLEAR and ). -- Brice Goglin Fri, 12 Aug 2005 22:26:56 +0200 lltag (0.6.1) * Guess by default. * Fix wrong processing of return values when internal ou user parsing loops fail. * Add clean target to the Makefile. * A few typos. -- Brice Goglin Sat, 30 Jul 2005 23:24:02 +0200 lltag (0.6) * Allow to edit fields with 'e' during confirmation. * Allow to see what would be done with 'v' during confirmation. * Fix a few missing capitalization. * Print "Nothing to do" when no field has to be tagged. -- Brice Goglin Sat, 30 Jul 2005 21:36:27 +0200 lltag (0.5.5) * Allow to pass multiple |-separated chars or strings to --sep. * When a field appears multiple times in the format, lltag does not print all matched values. It checks that they are identical, prints a warning if not, and keeps the first one. * Do not tag when there's nothing to tag (fix bogus mp3info/vorbiscomment invocation). * Add comments at the beginning of formats. * Fix matching in man page. -- Brice Goglin Sat, 30 Jul 2005 13:26:54 +0200 lltag (0.5.4) * Allow to only set default values (no user formats given, no guess). * Clean acceptable behavior, especially confirmation possibilities in various cases. -- Brice Goglin Mon, 23 May 2005 00:35:26 +0200 lltag (0.5.3) * Fix short and long option processing to set default field values. * Add --list and -L to list internal formats. * Add --version and -V to show version. * Show version in usage. * Automatically set version in the binary during install. -- Brice Goglin Thu, 12 May 2005 21:27:15 +0200 lltag (0.5.2) * "" means "yes" when confirming tagging. * Use lltag.in to replace /etc/ directories during install. * Add format file description to the manpage. * Generate Gentoo tarball in the Makefile (thanks to David Baelde for the ebuild). -- Brice Goglin Sun, 10 Apr 2005 00:41:08 +0200 lltag (0.5.1) * Fix -G option for directory containing spaces. * Add missing '%a - %A' path format. -- Brice Goglin Wed, 30 Mar 2005 23:53:34 +0200 lltag (0.5) * Add /etc/lltag/formats file to store formats. * Move all internal formats to /etc/lltag/formats. * Also read $HOME/.lltag/formats. * Support --spaces option for user-specified formats. * Add confirmation to manpage. * Cleanup a few messages. * Cleanup tag_file return values. * A few other cleanups. -- Brice Goglin Sun, 13 Mar 2005 20:24:22 +0100 lltag (0.4.2) * Add -R|--recursive option. * Check that files are really files. * Fix path matching which could lead to parts of the path matched with the filename. -- Brice Goglin Mon, 7 Mar 2005 00:00:04 +0100 lltag (0.4.1) * Add manpage. * Add --sep option to ask replacement of a character by a space in tags. * Do not accept multiple spaces by default. * Add --spaces to accept multiple spaces. * Accept spaces limiting path subpart. * Add message when no format was found. * Add the missing --format option equivalent to -F. * Use GetOpt to handle options properly. * Cleanup field showing. * Cleanup internal structure names. -- Brice Goglin Sun, 6 Mar 2005 22:15:45 +0100 lltag (0.4) * Support guessing formats with -G. - Internal filename parsers are (with variable whitespaces) "%n - %a - %t", "%n - %t", "%n[.)] %t" "%a - %n - %t", "%a - %t", "%t" - Internal path parsers are (with variable whitespaces and forgotten first directories) "%a/%a - %A", "%a/%A", "%a", "%A" * Add --yes to force tagging without confirmation when guessing. * Add --ask to force confirmation when not guessing. * Add an equivalent long option for each short option. * Support mixing options and files on the command line. * Add verbose message when setting default values. * Show command line in verbose mode even if --dry-run wasn't passed. * Cleanup command line showing. * Cleanup wrong option detection. * Fix format string matching to ensure the whole filename will be used. * Change several ERROR messages into less noisy messages. * Show usage on stdout instead of stderr. * Fix usage. -- Brice Goglin Sun, 6 Mar 2005 15:33:42 +0100 lltag (0.3.1) * Add author and homepage to usage. -- Brice Goglin Tue, 1 Mar 2005 02:14:44 +0100 lltag (0.3) * Format must now be passed with -F. * Multiple formats are allowed, the first that matches will be used. * Extension is no-longer included in format. * Support both ogg and mp3 tagging. * --ogg and --mp3 options to force tagging instead of by-extension detection. * Detect wrong %x code in format. * Detect and warn about problematic regexps. * Cleanup of messages. * Large cleanup of the code. * Split Debian and upstream changelogs. * Add a Makefile. -- Brice Goglin Sun, 27 Feb 2005 11:47:16 +0100 llmp3tag (0.2) * Rewrite parsing of the format to create indexed regexp to fix some issues. * %n now only matches numbers. * Some cleanup in the code. -- Brice Goglin Sun, 27 Feb 2005 02:14:50 +0100 llmp3tag (0.1) * Initial release. -- Brice Goglin Wed, 15 Dec 2004 19:47:00 +0200 lltag-lltag-0.14.6/Makefile000066400000000000000000000060411314167102300154450ustar00rootroot00000000000000NAME = lltag ifeq ($(shell [ -d .git ] && echo 1),1) VERSION = $(shell cat VERSION)+git$(shell date +%Y%m%d).$(shell git show -s --pretty=format:%h) else VERSION = $(shell cat VERSION) endif LIB_SUBDIR = lib DOC_SUBDIR = doc DESTDIR = PREFIX = /usr/local EXEC_PREFIX = $(PREFIX) BINDIR = $(EXEC_PREFIX)/bin DATADIR = $(PREFIX)/share SYSCONFDIR = $(PREFIX)/etc MANDIR = $(PREFIX)/man DOCDIR = $(DATADIR)/doc PERL_INSTALLDIRS = TARBALL = $(NAME)-$(VERSION) DEBIAN_TARBALL = $(NAME)_$(VERSION).orig .PHONY: lltag clean install uninstall tarball lltag:: lltag.in VERSION build-lib sed -e 's!@SYSCONFDIR@!$(DESTDIR)$(SYSCONFDIR)!g' -e 's!@VERSION@!$(DESTDIR)$(VERSION)!g' \ < lltag.in > lltag chmod 755 lltag clean:: clean-lib rm -f lltag install:: install-lib install -d -m 0755 $(DESTDIR)$(BINDIR)/ $(DESTDIR)$(SYSCONFDIR)/lltag/ install -m 0755 lltag $(DESTDIR)$(BINDIR)/lltag install -m 0644 formats $(DESTDIR)$(SYSCONFDIR)/lltag/ uninstall:: uninstall-lib rm $(DESTDIR)$(BINDIR)/lltag rm $(DESTDIR)$(SYSCONFDIR)/lltag/formats rmdir $(DESTDIR)$(SYSCONFDIR)/lltag/ tarball:: mkdir /tmp/$(TARBALL) cp lltag.in /tmp/$(TARBALL) cp formats /tmp/$(TARBALL) cp lltag.1 lltag_config.5 lltag_formats.5 /tmp/$(TARBALL) cp Makefile /tmp/$(TARBALL) cp COPYING README VERSION /tmp/$(TARBALL) cp Changes /tmp/$(TARBALL) cp -a $(DOC_SUBDIR)/ /tmp/$(TARBALL) cp -a $(LIB_SUBDIR) /tmp/$(TARBALL) cd /tmp && tar cfz $(DEBIAN_TARBALL).tar.gz $(TARBALL) cd /tmp && tar cfj $(TARBALL).tar.bz2 $(TARBALL) mv /tmp/$(DEBIAN_TARBALL).tar.gz /tmp/$(TARBALL).tar.bz2 .. rm -rf /tmp/$(TARBALL) # Perl modules .PHONY: build-lib clean-lib install-lib uninstall-lib prepare-lib $(LIB_SUBDIR)/Makefile.PL: $(LIB_SUBDIR)/Makefile.PL.in VERSION sed -e 's!@VERSION@!$(VERSION)!g' < $(LIB_SUBDIR)/Makefile.PL.in > $(LIB_SUBDIR)/Makefile.PL $(LIB_SUBDIR)/Makefile: $(LIB_SUBDIR)/Makefile.PL cd $(LIB_SUBDIR) && perl Makefile.PL INSTALLDIRS=$(PERL_INSTALLDIRS) prepare-lib: $(LIB_SUBDIR)/Makefile build-lib: prepare-lib $(MAKE) -C $(LIB_SUBDIR) install-lib: prepare-lib $(MAKE) -C $(LIB_SUBDIR) install PREFIX= SITEPREFIX=$(PREFIX) PERLPREFIX=$(PREFIX) VENDORPREFIX=$(PREFIX) clean-lib: prepare-lib $(MAKE) -C $(LIB_SUBDIR) distclean rm $(LIB_SUBDIR)/Makefile.PL uninstall-lib: prepare-lib $(MAKE) -C $(LIB_SUBDIR) uninstall # Install the doc, only called on-demand by distrib-specific Makefile .PHONY: install-doc uninstall-doc install-doc: $(MAKE) -C $(DOC_SUBDIR) install DOCDIR=$(DESTDIR)$(DOCDIR) uninstall-doc: $(MAKE) -C $(DOC_SUBDIR) uninstall DOCDIR=$(DESTDIR)$(DOCDIR) # Install the manpages, only called on-demand by distrib-specific Makefile .PHONY: install-man uninstall-man install-man:: install -d -m 0755 $(DESTDIR)$(MANDIR)/man1/ $(DESTDIR)$(MANDIR)/man5/ install -m 0644 lltag.1 $(DESTDIR)$(MANDIR)/man1/ install -m 0644 lltag_config.5 $(DESTDIR)$(MANDIR)/man5/ install -m 0644 lltag_formats.5 $(DESTDIR)$(MANDIR)/man5/ uninstall-man:: rm $(DESTDIR)$(MANDIR)/man1/lltag.1 rm $(DESTDIR)$(MANDIR)/man5/lltag_config.5 rm $(DESTDIR)$(MANDIR)/man5/lltag_formats.5 lltag-lltag-0.14.6/README000066400000000000000000000030751314167102300146710ustar00rootroot00000000000000lltag installation instructions =============================== You don't care about this if you use Debian or Gentoo prepackaged versions that are available on http://bgoglin.free.fr/lltag Run 'make' followed by 'make install' as root. By default, everything is installed in /usr/local/. Most directories might be replaced by overriding their default values on the command line. For instance, a traditional installation might be achieved with make install PREFIX=/usr SYSCONFDIR=/etc MANDIR=/usr/share/man Note that 'make uninstall' (with same options) allows to uninstall. lltag requirements ================== lltag theoretically only requires Perl base to work. However, depending on what type of files you want to work on, you'll need to install either the Perl MP3::Tag module (for MP3 files) or mp3info (for MP3 files, without ID3v2 support), vorbiscomment (for OGG) or metaflac (for FLAC). Additionally, when using CDDB features, LWP (the libwww-perl module) is required. These dependencies are automatically installed when installing Debian or Gentoo prepackaged versions, or at least recommended. lltag may also benefit from a smart readline perl library when the user edits the tags by hand. The recommended library is Term::ReadLine::Gnu (also called GNU Readline Library Wrapper Module) which provides great inline editing and GNU history features. Term::ReadLine::Perl may also be used but it might not be able to save the history between two lltag invocation. If you experience any problem, please see http://bgoglin.free.fr/lltag or report to . lltag-lltag-0.14.6/VERSION000066400000000000000000000000071314167102300150510ustar00rootroot000000000000000.14.6 lltag-lltag-0.14.6/doc/000077500000000000000000000000001314167102300145515ustar00rootroot00000000000000lltag-lltag-0.14.6/doc/Makefile000066400000000000000000000005401314167102300162100ustar00rootroot00000000000000.PHONY: install uninstall install: install -d -m 0755 $(DOCDIR) find . -type d -exec install -d -m 0755 $(DOCDIR)/{} \; find . -type f -not -name Makefile -exec install -m 0644 {} $(DOCDIR)/{} \; uninstall: find . -type f -not -name Makefile -exec rm -f $(DOCDIR)/{} \; find . -depth -type d -exec rmdir $(DOCDIR)/{} \; rmdir $(DOCDIR) || true lltag-lltag-0.14.6/doc/config000066400000000000000000000035351314167102300157470ustar00rootroot00000000000000# lltag configuration file example. # # The following options may be stored in /etc/lltag/config, # a per-user ${HOME}/.lltag/config or in a file passed # on the command-line with --config. # Add a user-defined format. # format = "" # Try to guess if user-defined formats do not match. # guess = 0 # Try CDDB # cddb = 0 # CDDB server # cddb_server_name = "tracktype.org" # cddb_server_port = 80 # Do not use file path when matching filename. # no_path = 0 # Explicit tag. # Might be used multiple times. # tag = "TAG=value" # Allow no or multiple spaces. # spaces = 0 # Upcase first letters of words in tags. # maj = 0 # Replace from with to in all tags with s/from/to/ in tags. # title,number:s/from/to/ replace in title and number tags only. # Might be used multiple times. # regexp = "" # Replace |-separated strings with space in tags. # sep = "" # Force mp3, ogg of flac instead of by-extension detection. # type = # Clear all tags of audio files. # clear_tags = 0 # Append tags only instead of replacing old ones. # append_tags = 0 # Do not actually tag files. # no_tagging = 0 # Rename file according to format. # rename_format = "" # Lowcase tags before renaming. # rename_min = 0 # Replace from with to in all tags with s/from/to/ before renaming. # title,number:s/from/to/ replace in title and number tags only. # Might be used multiple times. # rename_regexp = "" # Replace space with s in tags before renaming. # rename_sep = "" # Assume the rename format provides an extension. # rename_ext = 0 # Do nothing but show what would have been done. # dry_run = 0 # Tag without asking for confirmation when guessing # and rename without asking for confirmation. # yes = 0 # Always ask for confirmation before tagging. # ask = 0 # Recursively traverse all given subdirectories. # recursive = 0 # Messages verbosity level # verbose = 0 lltag-lltag-0.14.6/doc/howto.html000066400000000000000000000220641314167102300166030ustar00rootroot00000000000000 lltag - Automatic command-line mp3/ogg/flac file tagger and renamer - How-To?

lltag How-To?

Basics

How do I use lltag?

  lltag myfile1.mp3 myfile2.ogg
It is also possible to tag all files in subdirectories with:
  lltag -R mymusic/
All mp3, ogg and flac are processed, depending on their extension. If the extension is wrong, it is possible to force the file processing type:
  lltag --ogg myfile.mp3

How do I see current tags in files?

All the tags currently existing in files may be displayed:
  lltag -S *
It is also possible to only display some tags:
  lltag --show-tags artist,album,title,number *

How do I set tags by hand?

Any tag may be changed by hand:
  lltag --artist "myartist" --album "myalbum" --genre "rock" --comment="very cool" *.mp3
File types that support random tag names (ogg and flac currently) may also get random tags:
  lltag --tag foo=nil bar.ogg
It is also possible to clear a tag:
  lltag --tag foo= bar.ogg
And all tags may be removed from all files:
  lltag --clear *.flac

Disabling, Confirming, Forcing

How do I try lltag without actually tagging?

  lltag --dry-run

How do I change what confirmation lltag wants?

It is possible to remove any need for confirmation when no problem occurs:
  lltag --yes myfile.flac
It is also possible to force confirmation when lltag parses the filename with user-provided formats:
  lltag --ask myfile.ogg
Finally, during confirmation, it is possible to use Always to enable --yes at runtime.

Parsing

How do I see how lltag parses filename?

lltag uses internal format that are defined in /etc/lltag/formats/. You might see them with:
  lltag -L

How do I force lltag to use another format?

If internal formats do not fit the way your filename is written, you may pass additional formats with:
  lltag -F "%a-%t" -F "%n)%a-%t" myfile1.mp3 myfile2.flac
It is also possible to try user-provided formats and then try internal formats:
  lltag -F "%a-%t" -F "%n)%a-%t" -G myfile1.mp3 myfile2.flac

What is the preferred parser?

If all your filenames have the same format, i.e. they may be matched with the same parser, you might want to try this one first instead of seeing the other ones fail or having to skip them. When confirming parsing, you may choose to accept the current parser and mark it as preferred so that lltag will try it first for the next files, and not ask you for confirmation as long as it parses filenames fine.
If one filename cannot be matched by the preferred parser, lltag will revert to the common behavior. Any other parser, or the same previous one, may be marked as preferred later again.

Getting tags from the online CDDB

What is CDDB and why does lltag need it?

CDDB is a huge online database of CD describing their artist, album, date, tracks, ... everything that lltag considers as tags. By default, lltag accesses the database on www.freedb.org through the HTTP protocol. The server and its port may be changed on the command-line:
  lltag --cddb --cddb-server mycddb.mydomain.org:443 *
In case of a HTTP proxy being required for HTTP request, lltag may also be configured to use it:
  lltag --cddb --cddb-proxy myhttpproxy.mydomain.org:3128 *

How do get tags from CDDB?

The common way to search a CD on CDDB is to pass keywords, which return a list of matching CDs. Then, you choose a CD in the list, and a track in the CD tracks.
When entering the lltag CDDB menu for the first time, you need to enter keywords to find the CD that match the audio file you are processing. lltag will return a list of CD. At this point, you may either go back to search with other keywords, or enter the index of the CD that you want.
Then, lltag will display of the CD you have chosen and of its tracks. You may then choose a track and use it to tag/rename your file, or go back to choose another CD, or go back more to change the keywords.
When coming back to CDDB the next time, lltag will propose the next track of the previous CD that you used, so that processing an entire album is quick and easy.

How do choose a precise CD in CDDB?

All CD that are registered in CDDB are identified by a category and an hexadecimal identifier. When searching by keywords, lltag will return a list of matching CD with their category/identifier in parentheses.
It is also possible to directly enter this category/identifier instead of keywords to get a precise CD quickly.

Is the interactive interface always required to access CDDB?

The CDDB interface seems to be designed for interactive usage since the user is supposed to choose a CD in the list of CDs returned by the keywords query, and then choose a track in the CD. But, it is also possible to use CDDB automatically if the CD id is known and the files are ordered by track numbers.
The CD id may be given on the command line:
  lltag --cddb-query rock/a0b2c4d7 --yes *
Using --yes enables automatic mode which means lltag will tag the first file as the first track of the CD returned by CDDB, the second file as the second track, ... If lltag ever reaches the end of the CD, it will return to interactive mode so that the user may choose another CD to tag the remaining files.

Managing tags

How do I set a specific value? What are explicit tag values?

Each tag field may receive a specific value even if it is not obtained through parsing the filename or so:
  lltag -a MyArtist -y 1990 myfile.mp3
All regular fields may be defined like the above with -a, -t, -A, -n, -g, -d or -c. These (and any random tag) may also be set through --tag FIELD=value. These values are called explicit values. They are always added to the ones obtained through parsing or so, even if it means that the target tag will have multiple values. Playing with --clear or --append enables configuration of how they are actually added.

How do I cleanup tags?

If your filenames are dirty, it is possible to ask lltag to be flexible with spaces. Any space in the format might be matched with 0 or several spaces with:
  lltag --spaces myfile.mp3
The first letter of each word in each tag might be upcased with:
  lltag --maj myfile.mp3
If the filename contains special characters that have to be translated into spaces for tags:
  lltag --sep _|-|: myfile.mp3

Renaming

How do I rename files after tagging?

Files might be renamed using the tags that were gotten from original filename parsing:
  lltag myfile.mp3 --rename "%a - %t.mp3"
Tags might be lowcased before renaming, and spaces might be replaced with another character:
  lltag myfile.mp3 --rename "%a - %t.mp3" --rename-min --rename-sep _

How do I rename files without tagging?

lltag may also be used as a renaming program without any tagging:
  lltag --no-tagging -F "%A/%n. %a - %t" --rename "%a - %A/%n - %t" */*.ogg
Note that lltag may rename using existing tags even if the old filename is useless. Matching with "%i" to ignore the old filename might be useful in this case.
  lltag --no-tagging --rename "%a - %A/%n - %t" */*.ogg

The lltag team.
$Id: howto.html,v 1.2 2006/05/07 00:57:26 bgoglin Exp $
lltag-lltag-0.14.6/formats000066400000000000000000000037411314167102300154070ustar00rootroot00000000000000# lltag internal format database # each entry is composed of: # [natural format] # format will be shown when matching # type = basename or path # does this format apply to the wall path or only the basename ? # regexp = # internal regexp with %L for a delimiter (empty by default, multiple spaces if --spaces was passed) # %S for a space (or multiple spaces if --spaces was passed) # %N for a numeric string # %A for an alphanumeric string without / # %P for any path (alphanumeric string with /) # %% for % # any other character will remain unchanged # indices = list of comma-separed tag name indicating the corresponding # %N or %A means in the regexp. # the field name may be replaced by the corresponding letter: # a for ARTIST # t for TITLE # A for ALBUM # g for GENRE # n for NUMBER # d for DATE # c for COMMENT # i for IGNORE [%n - %a - %t] type = basename regexp = %L%N%S-%S%A%S-%S%A%L indices = NUMBER,ARTIST,TITLE [%n) %a - %t] type = basename regexp = %L%N)%S%A%S-%S%A%L indices = NUMBER,ARTIST,TITLE [%n - %t] type = basename regexp = %L%N%S-%S%A%L indices = NUMBER,TITLE [%n. %t] type = basename regexp = %L%N.%S%A%L indices = NUMBER,TITLE [%n) %t] type = basename regexp = %L%N)%S%A%L indices = NUMBER,TITLE [%a - %n - %t] type = basename regexp = %L%A%S-%S%N%S-%S%A%L indices = ARTIST,NUMBER,TITLE [%a - %t] type = basename regexp = %L%A%S-%S%A%L indices = ARTIST,TITLE [%t] type = basename regexp = %L%A%L indices = TITLE [%a/%a - %A] type = path regexp = %P%L%A%L/%L%A%S-%S%A%L indices = ARTIST,ARTIST,ALBUM [%a/%A (%d)] type = path regexp = %P%L%A%L/%L%A%L%S(%N)%L indices = ARTIST,ALBUM,DATE [%a/%A [%d]] type = path regexp = %P%L%A%L/%L%A%L%S[%N]%L indices = ARTIST,ALBUM,DATE [%a/%A] type = path regexp = %P%L%A%L/%L%A%L indices = ARTIST,ALBUM [%a - %A] type = path regexp = %P%L%A%S-%S%A%L indices = ARTIST,ALBUM [%a] type = path regexp = %P%L%A%L indices = ARTIST [%A] type = path regexp = %P%L%A%L indices = ALBUM lltag-lltag-0.14.6/lib/000077500000000000000000000000001314167102300145525ustar00rootroot00000000000000lltag-lltag-0.14.6/lib/Lltag/000077500000000000000000000000001314167102300156155ustar00rootroot00000000000000lltag-lltag-0.14.6/lib/Lltag/CDDB.pm000066400000000000000000000510641314167102300166550ustar00rootroot00000000000000package Lltag::CDDB ; use strict ; use Lltag::Misc ; use I18N::Langinfo qw(langinfo CODESET) ; # return values that are passed to lltag use constant CDDB_SUCCESS => 0 ; use constant CDDB_ABORT => -1 ; # local return values use constant CDDB_ABORT_TO_KEYWORDS => -10 ; use constant CDDB_ABORT_TO_CDIDS => -11 ; # keep track of where we were during the previous CDDB access my $previous_cdids = undef ; my $previous_cd = undef ; my $previous_track = undef ; # confirmation behavior my $current_cddb_yes_opt = undef ; # HTTP browser my $browser ; ######################################### # init my $cddb_supported = 1 ; my $cddb_track_usage_forced ; my $cddb_cd_usage_forced ; my $cddb_keywords_usage_forced ; sub init_cddb { my $self = shift ; if (not eval { require LWP ; } ) { print "LWP (libwww-perl module) does not seem to be available, disabling CDDB.\n" if $self->{verbose_opt} ; $cddb_supported = 0 ; return ; } # default confirmation behavior $current_cddb_yes_opt = $self->{yes_opt} ; # HTTP browser $browser = LWP::UserAgent->new; # use HTTP_PROXY environment variable $browser->env_proxy ; # need to show menu usage once ? $cddb_track_usage_forced = $self->{menu_usage_once_opt} ; $cddb_cd_usage_forced = $self->{menu_usage_once_opt} ; $cddb_keywords_usage_forced = $self->{menu_usage_once_opt} ; } ######################################### # freedb.org specific code # NOT USED ANYMORE since Magix acquired freedb.org # and closed the online search module for now ######################################### sub freedborg_cddb_response { my $self = shift ; my $path = shift ; print " Sending CDDB request...\n" ; print " '$path'\n" if $self->{verbose_opt} ; my $response = $browser->get( "http://" . $self->{cddb_server_name} . ($self->{cddb_server_port} != 80 ? $self->{cddb_server_port} : "") . $path . "\n" ) ; if (!$response->is_success) { Lltag::Misc::print_error (" ", "HTTP request to CDDB server (" . $self->{cddb_server_name} .":". $self->{cddb_server_port} . ") failed.") ; return undef ; } if ($response->content_type ne 'text/html') { Lltag::Misc::print_error (" ", "Weird CDDB response (type ".$response->content_type.") from server " . $self->{cddb_server_name} .":". $self->{cddb_server_port} . ".") ; return undef ; } # TODO: grep for something to be sure it worked return $response->content ; } sub freedborg_cddb_query_cd_by_keywords { my $self = shift ; my $keywords = shift ; # extract fields and cat from the keywords my @fields = () ; my @cats = () ; my @keywords_list = () ; foreach my $word (split / +/, $keywords) { if ($word =~ m/^fields=(.+)$/) { push @fields, (split /\++/, $1) ; } elsif ($word =~ m/^cats=(.+)$/) { push @cats, (split /\++/, $1) ; } else { push @keywords_list, $word ; } } # assemble remaining keywords with "+" $keywords = join "+", @keywords_list ; # by default, search in all cats, within artist and title only @cats = ( "all" ) unless @cats ; @fields = ( "artist", "title" ) unless @fields ; my $query_fields = (grep { $_ eq "all" } @fields) ? "allfields=YES" : "allfields=NO".(join ("", map { "&fields=$_" } @fields)) ; my $query_cats = (grep { $_ eq "all" } @cats) ? "allcats=YES" : "allcats=NO".(join ("", map { "&cats=$_" } @cats)) ; my $response = freedborg_cddb_response $self, "/freedb_search.php?words=${keywords}&${query_fields}&${query_cats}&grouping=none&x=0&y=0" ; return (CDDB_ABORT, undef) unless defined $response ; my @cdids = () ; my $samename = undef ; my $same = 0 ; foreach my $line (split /\n/, $response) { next if $line !~ //) { $same = 0 ; $samename = undef ; } else { $same = 1; } my @links = split (/(.*)@) { my %cdid = ( CAT => $1, ID => $2, NAME => $same ? $samename : $3 ) ; push @cdids, \%cdid ; $samename = $cdid{NAME} unless $same ; $same = 1; } } } return (CDDB_SUCCESS, \@cdids) ; } sub freedborg_cddb_query_tracks_by_id { my $self = shift ; my $cat = shift ; my $id = shift ; my $name = shift ; my $response = freedborg_cddb_response $self, "/freedb_search_fmt.php?cat=${cat}&id=${id}" ; return (CDDB_ABORT, undef) unless defined $response ; my $cd ; $cd->{CAT} = $cat ; $cd->{ID} = $id ; foreach my $line (split /\n/, $response) { if ($line =~ m/tracks: (\d+)/i) { $cd->{TRACKS} = $1 ; } elsif ($line =~ m/total time: ([\d:]+)/i) { $cd->{"TOTAL TIME"} = $1 ; } elsif ($line =~ m/genre: (\w+)/i) { $cd->{GENRE} = $1 ; } elsif ($line =~ m/id3g: (\d+)/i) { $cd->{ID3G} = $1 ; } elsif ($line =~ m/year: (\d+)/i) { $cd->{DATE} = $1 ; } elsif ($line =~ m@ *(\d+)\. *(-?[\d:]+)(.*)@) { # '-?' because there are some buggy entries... my %track = ( TITLE => $3, TIME => $2 ) ; $cd->{$1} = \%track ; } elsif ($line =~ m@

(.+ / .+)

@) { if (defined $name) { if ($name ne $1) { Lltag::Misc::print_warning (" ", "Found CD name '$1' instead of '$name', this entry might be corrupted") ; } } else { $name = $1 ; } } } return (CDDB_SUCCESS, undef) unless defined $name ; # FIXME: are we sure no artist or album may contain " / " ? $name =~ m@^(.+) / (.+)$@ ; $cd->{ARTIST} = $1 ; $cd->{ALBUM} = $2 ; # FIXME: check number and indexes of tracks ? return (CDDB_SUCCESS, $cd) ; } sub freedborg_cddb_query_cd_by_keywords_usage { my $indent = shift ; print $indent." => CDDB query for CD matching the keywords\n" ; print $indent." Search in all CD categories within fields 'artist' and 'title' by default\n" ; print $indent." cats=foo+bar => Search in CD categories 'foo' and 'bar' only\n" ; print $indent." fields=all => Search keywords in all fields\n" ; print $indent." fields=foo+bar => Search keywords in fields 'foo' and 'bar'\n" ; print $indent."/ => CDDB query for CD matching category and id\n" ; } my $freedborg_cddb_backend = { cddb_query_cd_by_keywords => \&freedborg_cddb_query_cd_by_keywords, cddb_query_tracks_by_id => \&freedborg_cddb_query_tracks_by_id, cddb_query_cd_by_keywords_usage => \&freedborg_cddb_query_cd_by_keywords_usage, } ; ######################################### # tracktype.org specific code # USED since november 2006 ######################################### sub tracktypeorg_cddb_response { my $self = shift ; my $path = shift ; my $postdata = shift ; my $response ; print " Sending CDDB request...\n" ; if (defined $postdata) { print " 'POST $path'\n" if $self->{verbose_opt} ; $response = $browser->post( "http://" . $self->{cddb_server_name} . ($self->{cddb_server_port} != 80 ? $self->{cddb_server_port} : "") . $path, $postdata ) ; } else { print " 'GET $path'\n" if $self->{verbose_opt} ; $response = $browser->get( "http://" . $self->{cddb_server_name} . ($self->{cddb_server_port} != 80 ? $self->{cddb_server_port} : "") . $path ) ; } if (!$response->is_success) { Lltag::Misc::print_error (" ", "HTTP request to CDDB server (" . $self->{cddb_server_name} .":". $self->{cddb_server_port} . ") failed.") ; return undef ; } if ($response->content_type ne 'text/plain') { Lltag::Misc::print_error (" ", "Weird CDDB response (type ".$response->content_type.") from server " . $self->{cddb_server_name} .":". $self->{cddb_server_port} . ".") ; return undef ; } my $content = $response->content ; # deal with windows line-break $content =~ s/\r\n/\n/g ; # convert from utf8 if not using a utf8 locale utf8::decode($content) unless $self->{utf8} ; return $content ; } sub tracktypeorg_cddb_query_cd_by_keywords { my $self = shift ; my $keywords = shift ; my %postdata = ( "hello" => "lltag", "proto" => 4, "cmd" => "cddb album $keywords", ) ; my $response = tracktypeorg_cddb_response $self, "/~cddb/cddb.cgi", \%postdata ; return (CDDB_ABORT, undef) unless defined $response ; my @lines = (split /\n/, $response) ; # check status in header my $header = shift @lines ; # TODO: check status my @cdids = () ; foreach my $line (@lines) { if ($line =~ m@^([^ ]+) ([^ ]+) (.+ / .+)$@) { my %cdid = ( CAT => $1, ID => $2, NAME => $3 ) ; push @cdids, \%cdid ; } } return (CDDB_SUCCESS, \@cdids) ; } sub tracktypeorg_cddb_query_tracks_by_id { my $self = shift ; my $cat = shift ; my $id = shift ; my $name = shift ; my $response = tracktypeorg_cddb_response $self, "/freedb/${cat}/${id}" ; return (CDDB_ABORT, undef) unless defined $response ; # TODO: grep for something to be sure it worked my $cd ; $cd->{CAT} = $cat ; $cd->{ID} = $id ; $cd->{TRACKS} = 0 ; foreach my $line (split /\n/, $response) { next if $line =~ /^#/ ; if ($line =~ m/^DISCID=(.+)/) { if ($id ne $1) { Lltag::Misc::print_warning (" ", "Found CD id '$1' instead of '$id', this entry might be corrupted") ; } } elsif ($line =~ m@^DTITLE=(.*)@) { if (defined $name) { if ($name ne $1) { Lltag::Misc::print_warning (" ", "Found CD name '$1' instead of '$name', this entry might be corrupted") ; } } else { $name = $1 ; } } elsif ($line =~ m/^DYEAR=(.*)/) { $cd->{DATE} = $1 ; } elsif ($line =~ m/^DGENRE=(.*)/) { $cd->{GENRE} = $1 ; } elsif ($line =~ m/^TTITLE(\d+)=(.*)/) { my $num = $1 + 1; if ($num != $cd->{TRACKS} + 1) { Lltag::Misc::print_warning (" ", "Found CD track '$num' instead of '".($cd->{TRACKS}+1)."', this entry might be corrupted") ; } my %track = ( TITLE => $2 ) ; $cd->{$num} = \%track ; $cd->{TRACKS} = $num ; } } return (CDDB_SUCCESS, undef) unless defined $name ; # FIXME: are we sure no artist or album may contain " / " ? $name =~ m@^(.+) / (.+)$@ ; $cd->{ARTIST} = $1 ; $cd->{ALBUM} = $2 ; return (CDDB_SUCCESS, $cd) ; } sub tracktypeorg_cddb_query_cd_by_keywords_usage { my $indent = shift ; print $indent." => CDDB query for CD matching the keywords\n" ; print $indent." Search in all CD categories within fields 'artist' OR 'album'\n" ; print $indent."/ => CDDB query for CD matching category and id\n" ; } my $tracktypeorg_cddb_backend = { cddb_query_cd_by_keywords => \&tracktypeorg_cddb_query_cd_by_keywords, cddb_query_tracks_by_id => \&tracktypeorg_cddb_query_tracks_by_id, cddb_query_cd_by_keywords_usage => \&tracktypeorg_cddb_query_cd_by_keywords_usage, } ; my $cddb_backend = $tracktypeorg_cddb_backend ; ###################################################### # interactive menu to browse CDDB, tracks in a CD sub cddb_track_usage { Lltag::Misc::print_usage_header (" ", "Choose Track in CDDB CD") ; print " => Choose a track of the current CD (current default is Track $previous_track)\n" ; print " a => Choose a track and do not ask for confirmation anymore\n" ; print " a => Use default track and do not ask for confirmation anymore\n" ; print " E => Edit current CD common tags\n" ; print " V => View the list of CD matching the keywords\n" ; print " c => Change the CD chosen in keywords query results list\n" ; print " k => Start again CDDB query with different keywords\n" ; print " q => Quit CDDB query\n" ; print " h => Show this help\n" ; $cddb_track_usage_forced = 0 ; } sub print_cd { my $cd = shift ; map { print " $_: $cd->{$_}\n" ; } grep { $_ !~ /^\d+$/ } (keys %{$cd}) ; my $track_format = " Track %0".(length $cd->{TRACKS})."d: %s%s\n" ; for(my $i=1; $i <= $cd->{TRACKS}; $i++) { my $track = $cd->{$i} ; my $title = "" ; $title = $track->{TITLE} if exists $track->{TITLE} and defined $track->{TITLE} ; my $time = "" ; $time = " ($track->{TIME})" if exists $track->{TIME} and defined $track->{TIME} ; printf ($track_format, $i, $title, $time) ; } } sub get_cddb_tags_from_tracks { my $self = shift ; my $cd = shift ; my $tracknumber = undef ; # update previous_track to 1 or ++ $previous_track = 0 unless defined $previous_track ; $previous_track++ ; # if automatic mode and still in the CD, let's go if ($current_cddb_yes_opt and $previous_track <= $cd->{TRACKS}) { $tracknumber = $previous_track ; Lltag::Misc::print_notice (" ", "Automatically choosing next CDDB track, #$tracknumber...") ; goto FOUND ; } # either in non-automatic or reached the end of the CD, dump the contents print_cd $cd ; # reached the end of CD, reset to the beginning if ($previous_track == $cd->{TRACKS} + 1) { $previous_track = 1; if ($current_cddb_yes_opt) { Lltag::Misc::print_notice (" ", "Reached the end of the CD, returning to interactive mode") ; # return to previous confirmation behavior $current_cddb_yes_opt = $self->{yes_opt} ; } } cddb_track_usage if $cddb_track_usage_forced ; while (1) { my $reply = Lltag::Misc::readline (" ", "Enter track index [aEVckq]". " (default is Track $previous_track, h for help)", "", -1) ; # if ctrl-d, abort cddb $reply = 'q' unless defined $reply ; $reply = $previous_track if $reply eq '' ; return (CDDB_ABORT, undef) if $reply =~ m/^q/ ; return (CDDB_ABORT_TO_KEYWORDS, undef) if $reply =~ m/^k/ ; return (CDDB_ABORT_TO_CDIDS, undef) if $reply =~ m/^c/ ; if ($reply =~ m/^E/) { # move editable values into a temporary hash my $values_to_edit = {} ; foreach my $key (keys %{$cd}) { next if $key eq 'TRACKS' or $key =~ /^\d+$/ ; $values_to_edit->{$key} = $cd->{$key} ; delete $cd->{$key} ; } # clone them so that we can restore them if canceled my $values_edited = Lltag::Tags::clone_tag_values ($values_to_edit) ; # edit them my $res = Lltag::Tags::edit_values ($self, $values_edited) ; # replace the edited values with the originals if canceled $values_edited = $values_to_edit if $res == Lltag::Tags->EDIT_CANCEL ; # move them back foreach my $key (keys %{$values_edited}) { $cd->{$key} = $values_edited->{$key} ; } next ; } if ($reply =~ m/^V/) { print_cd $cd ; next ; } ; if ($reply =~ m/^a/) { $reply = $previous_track ; $current_cddb_yes_opt = 1 ; } if ($reply =~ m/^(\d+) *a/) { $current_cddb_yes_opt = 1 ; $reply = $1 ; } if ($reply =~ m/^\d+$/ and $reply >= 1 and $reply <= $cd->{TRACKS}) { $tracknumber = $reply ; last ; } cddb_track_usage () ; } FOUND: my $track = $cd->{$tracknumber} ; # get the track tags my %values ; foreach my $key (keys %{$cd}) { next if $key eq 'TRACKS' or $key =~ /^\d+$/ ; $values{$key} = $cd->{$key} ; } $values{TITLE} = $track->{TITLE} if exists $track->{TITLE} ; $values{NUMBER} = $tracknumber ; # save the previous track number $previous_track = $tracknumber ; return (CDDB_SUCCESS, \%values) ; } ########################################################## # interactive menu to browse CDDB, CDs in a query results sub cddb_cd_usage { Lltag::Misc::print_usage_header (" ", "Choose CD in CDDB Query Results") ; print " => Choose a CD in the current keywords query results list\n" ; print " V => View the list of CD matching the keywords\n" ; print " k => Start again CDDB query with different keywords\n" ; print " q => Quit CDDB query\n" ; print " h => Show this help\n" ; $cddb_cd_usage_forced = 0 ; } sub print_cdids { my $cdids = shift ; my $cdid_format = " %0".(length (scalar @{$cdids}))."d: %s (cat=%s, id=%s)\n" ; for(my $i=0; $i < @{$cdids}; $i++) { my $cdid = $cdids->[$i] ; printf ($cdid_format, $i+1, $cdid->{NAME}, $cdid->{CAT}, $cdid->{ID}) ; } } # returns (SUCCESS, undef) if CDDB returned an bad/empty CD sub get_cddb_tags_from_cdid { my $self = shift ; my $cdid = shift ; my $cddb_query_tracks_by_id_func = $cddb_backend->{cddb_query_tracks_by_id} ; my ($res, $cd) = &{$cddb_query_tracks_by_id_func} ($self, $cdid->{CAT}, $cdid->{ID}, $cdid->{NAME}) ; return (CDDB_ABORT, undef) if $res == CDDB_ABORT ; if (!$cd or !$cd->{TRACKS}) { print " There is no tracks in this CD.\n" ; return (CDDB_SUCCESS, undef) ; } $previous_cd = $cd ; undef $previous_track ; return get_cddb_tags_from_tracks $self, $cd ; } sub get_cddb_tags_from_cdids { my $self = shift ; my $cdids = shift ; AGAIN: print_cdids $cdids ; cddb_cd_usage if $cddb_cd_usage_forced ; while (1) { my $reply = Lltag::Misc::readline (" ", "Enter CD index [Vkq] (no default, h for help)", "", -1) ; # if ctrl-d, abort cddb $reply = 'q' unless defined $reply ; next if $reply eq '' ; return (CDDB_ABORT, undef) if $reply =~ m/^q/ ; return (CDDB_ABORT_TO_KEYWORDS, undef) if $reply =~ m/^k/ ; goto AGAIN if $reply =~ m/^V/ ; if ($reply =~ m/^\d+$/ and $reply >= 1 and $reply <= @{$cdids}) { # do the actual query for CD contents my ($res, $values) = get_cddb_tags_from_cdid $self, $cdids->[$reply-1] ; goto AGAIN if $res == CDDB_ABORT_TO_CDIDS or ($res == CDDB_SUCCESS and not defined $values) ; return ($res, $values) ; } cddb_cd_usage () ; } } ########################################################## # interactive menu to browse CDDB, keywords query sub cddb_keywords_usage { Lltag::Misc::print_usage_header (" ", "CDDB Query by Keywords") ; my $cddb_query_cd_by_keywords_usage_func = $cddb_backend->{cddb_query_cd_by_keywords_usage} ; &{$cddb_query_cd_by_keywords_usage_func} (" ") ; print " q => Quit CDDB query\n" ; print " h => Show this help\n" ; $cddb_keywords_usage_forced = 0 ; } sub get_cddb_tags { my $self = shift ; my ($res, $values) ; if (!$cddb_supported) { print " Cannot use CDDB without LWP (libwww-perl module).\n" ; goto ABORT ; } if (defined $previous_cd) { bless $previous_cd ; print " Going back to previous CD cat=$previous_cd->{CAT} id=$previous_cd->{ID}\n" ; ($res, $values) = get_cddb_tags_from_tracks $self, $previous_cd ; if ($res == CDDB_ABORT_TO_CDIDS) { bless $previous_cdids ; ($res, $values) = get_cddb_tags_from_cdids $self, $previous_cdids ; } goto OUT if $res == CDDB_SUCCESS ; goto ABORT if $res == CDDB_ABORT ; } cddb_keywords_usage if $cddb_keywords_usage_forced ; while (1) { my $keywords ; if (defined $self->{requested_cddb_query}) { $keywords = $self->{requested_cddb_query} ; print " Using command-line given keywords '$self->{requested_cddb_query}'...\n" ; undef $self->{requested_cddb_query} ; # FIXME: either put it in the history, or preput it next time } else { $keywords = Lltag::Misc::readline (" ", "Enter CDDB query [q] (no default, h for help)", "", -1) ; # if ctrl-d, abort cddb $keywords = 'q' unless defined $keywords ; } next if $keywords eq '' ; # be careful to match the whole reply, not only the first char # since multiple chars are valid keyword queries goto ABORT if $keywords eq 'q' ; if ($keywords eq 'h') { cddb_keywords_usage () ; next ; } # it this a category/id ? if ($keywords =~ m@^\s*(\w+)/([\da-f]+)\s*$@) { my $cdid ; $cdid->{CAT} = $1 ; $cdid->{ID} = $2 ; # FIXME: do not show 'c' for goto to CD list in there ($res, $values) = get_cddb_tags_from_cdid $self, $cdid ; goto OUT if $res == CDDB_SUCCESS and defined $values ; goto ABORT if $res == CDDB_ABORT ; next ; } # do the actual query for CD id with keywords my $cdids ; my $cddb_query_cd_by_keywords_func = $cddb_backend->{cddb_query_cd_by_keywords} ; ($res, $cdids) = &{$cddb_query_cd_by_keywords_func} ($self, $keywords) ; goto ABORT if $res == CDDB_ABORT ; if (!@{$cdids}) { print " No CD found.\n" ; next ; } $previous_cdids = $cdids ; $previous_cd = undef ; ($res, $values) = get_cddb_tags_from_cdids $self, $cdids ; next if $res == CDDB_ABORT_TO_KEYWORDS ; goto OUT ; } OUT: goto ABORT if $res == CDDB_ABORT ; return ($res, $values) ; ABORT: $previous_cdids = undef ; $previous_cd = undef ; $previous_track = undef ; return (CDDB_ABORT, undef); } 1 ; lltag-lltag-0.14.6/lib/Lltag/FLAC.pm000066400000000000000000000041151314167102300166610ustar00rootroot00000000000000package Lltag::FLAC ; use strict ; require Lltag::Tags ; require Lltag::Misc ; sub test_metaflac { my $self = shift ; # cannot test with "metaflac -h" since it returns 1 my ($status, @output) = Lltag::Misc::system_with_output ("metaflac", "/dev/null") ; print "metaflac does not seem to work, disabling 'Flac' backend.\n" if $status and $self->{verbose_opt} ; return $status ; } sub read_tags { my $self = shift ; my $file = shift ; my ($status, @output) = Lltag::Misc::system_with_output ("metaflac", "--list", "--block-type=VORBIS_COMMENT", $file) ; return undef if $status ; @output = map { my $line = $_ ; $line =~ s/^\s*comment\[\d+\]\s*:\s*(.*)/$1/ ; $line =~ s/^TRACKNUMBER=/NUMBER=/ ; $line } ( grep { /comment\[\d+\]/ } @output ) ; return Lltag::Tags::convert_tag_stream_to_values ($self, @output) ; } sub set_tags { my $self = shift ; my $file = shift ; my $values = shift ; my %field_name_flac_translations = ( 'NUMBER' => 'TRACKNUMBER', ) ; my @flac_tagging_cmd = ( 'metaflac' ) ; my @flac_tagging_clear_option = ( '--remove-all-tags' ) ; my @system_args = ( @flac_tagging_cmd , # clear all tags @flac_tagging_clear_option , # apply new tags ( map { my $flacname = $_ ; $flacname = $field_name_flac_translations{$_} if defined $field_name_flac_translations{$_} ; my @tags = Lltag::Tags::get_tag_value_array ($self, $values, $_) ; map { ( "--set-tag", $flacname."=".$_ ) } @tags } @{$self->{field_names}} ), # apply non-regular tags ( map { my $flacname = $_ ; my @tags = Lltag::Tags::get_tag_value_array ($self, $values, $_) ; map { ( "--set-tag", $flacname."=".$_ ) } @tags } Lltag::Tags::get_values_non_regular_keys ($self, $values) ), $file ) ; Lltag::Tags::set_tags_with_external_prog ($self, @system_args) ; } sub new { my $self = shift ; return undef if test_metaflac $self ; return { name => "Flac (using metaflac)", type => "flac", extension => "flac", read_tags => \&read_tags, set_tags => \&set_tags, } ; } 1 ; lltag-lltag-0.14.6/lib/Lltag/MP3.pm000066400000000000000000000136621314167102300165620ustar00rootroot00000000000000package Lltag::MP3 ; use strict ; require Lltag::Tags ; require Lltag::Misc ; sub test_mp3info { my $self = shift ; my ($status, @output) = Lltag::Misc::system_with_output ("mp3info", "-h") ; print "mp3info does not seem to work, disabling 'MP3' backend.\n" if $status and $self->{verbose_opt} ; return $status ; } ####################################################### # valid ID3v1 genres in mp3info my @mp3info_genres = ("", # 0 "Blues", "Classic Rock", "Country", "Dance", "Disco", # 5 "Funk", "Grunge", "Hip-Hop", "Jazz", "Metal", # 10 "New Age", "Oldies", "Other", "Pop", "R&B", # 15 "Rap", "Reggae", "Rock", "Techno", "Industrial", # 20 "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack", # 25 "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", # 30 "Fusion", "Trance", "Classical", "Instrumental", "Acid", # 35 "House", "Game", "Sound Clip", "Gospel", "Noise", # 40 "AlternRock", "Bass", "Soul", "Punk", "Space", # 45 "Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", # 50 "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", # 55 "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta Rap", # 60 "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", # 65 "Cabaret", "New Wave", "Psychedelic", "Rave", "Showtunes", # 70 "Trailer", "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", # 75 "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock", # 80 "Folk", "Folk/Rock", "National Folk", "Swing", "Fast-Fusion", # 85 "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", # 90 "Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", # 95 "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", # 100 "Humour", "Speech", "Chanson", "Opera", "Chamber Music", # 105 "Sonata", "Symphony", "Booty Bass", "Primus", "Porn Groove", # 110 "Satire", "Slow Jam", "Club", "Tango", "Samba", # 115 "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", # 120 "Duet", "Punk Rock", "Drum Solo", "A Cappella", "Euro-House", # 125 "Dance Hall", "Goa", "Drum & Bass", "Club-House", "Hardcore", # 130 "Terror", "Indie", "BritPop", "Negerpunk", "Polsk Punk", # 135 "Beat", "Christian Gangsta Rap", "Heavy Metal", "Black Metal", "Crossover", # 140 "Contemporary Christian", "Christian Rock", "Merengue", "Salsa", "Thrash Metal", # 145 "Anime", "JPop", "Synthpop", ) ; sub check_mp3info_genre { my $genre = shift ; return scalar ( grep { lc($genre) eq lc($_) } @mp3info_genres ) ; } sub check_id3v1_tracknumber { my $number = shift ; return ( $number =~ m/^\d+$/ and $number <= 255 ) ; } sub fix_values_for_mp3info { my $self = shift ; my $values = shift ; # only regular fields are supported foreach my $field (Lltag::Tags::get_values_non_regular_keys ($self, $values)) { Lltag::Misc::print_warning (" ", "Cannot set $field in MP3 ID3v1 tags") ; delete $values->{$field} ; } # remove unsupported genres and keep a single value my @supported_genres = () ; foreach my $genre (Lltag::Tags::get_tag_value_array ($self, $values, 'GENRE')) { if (check_mp3info_genre $genre) { push @supported_genres, $genre ; } else { Lltag::Misc::print_warning (" ", "Genre $genre is not supported in ID3v1 MP3 tags") ; } } delete $values->{GENRE} ; if (@supported_genres > 1) { @{$values->{GENRE}}= @supported_genres ; } elsif (@supported_genres == 1) { $values->{GENRE} = $supported_genres[0] ; } # remove unsupported tracknumbers and keep a single value my @supported_numbers = () ; foreach my $number (Lltag::Tags::get_tag_value_array ($self, $values, 'NUMBER')) { if (check_id3v1_tracknumber $number) { push @supported_numbers, $number ; } else { Lltag::Misc::print_warning (" ", "Track number $number is not supported in ID3v1 MP3 tags") ; } } delete $values->{NUMBER} ; if (@supported_numbers > 1) { @{$values->{NUMBER}} = @supported_numbers ; } elsif (@supported_numbers == 1) { $values->{NUMBER} = $supported_numbers[0] ; } # keep a single value foreach my $field (keys %{$values}) { if (ref($values->{$field}) eq 'ARRAY') { my $val = Lltag::Tags::get_tag_unique_value ($self, $values, $field) ; delete $values->{$field} ; $values->{$field} = $val ; Lltag::Misc::print_warning (" ", "Multiple $field values not supported in ID3v1 MP3 tags, keeping only $val.") ; } } } ####################################################### sub read_tags { my $self = shift ; my $file = shift ; my ($status, @output) = Lltag::Misc::system_with_output ("mp3info", "-p", "ARTIST=%a\nALBUM=%l\nTITLE=%t\nNUMBER=%n\nGENRE=%g\nDATE=%y\nCOMMENT=%c\n", $file) ; return undef if $status ; return Lltag::Tags::convert_tag_stream_to_values ($self, @output) ; } sub set_tags { my $self = shift ; my $file = shift ; my $values = shift ; my %field_name_mp3info_option = ( 'ARTIST' => 'a', 'TITLE' => 't', 'ALBUM' => 'l', 'NUMBER' => 'n', 'GENRE' => 'g', 'DATE' => 'y', 'COMMENT' => 'c' ) ; my @mp3_tagging_cmd = ( 'mp3info' ) ; my @mp3_tagging_clear_option = map { ( "-$_" , "" ) } (values %field_name_mp3info_option) ; fix_values_for_mp3info $self, $values ; my @system_args = ( @mp3_tagging_cmd , # clear all tags @mp3_tagging_clear_option , # apply new tags ( map { ( "-".$field_name_mp3info_option{$_} , $values->{$_} ) } (keys %{$values}) ), $file ) ; Lltag::Tags::set_tags_with_external_prog ($self, @system_args) ; } sub new { my $self = shift ; return undef if test_mp3info $self ; return { name => "MP3 (using mp3info)", type => "mp3", extension => "mp3", read_tags => \&read_tags, set_tags => \&set_tags, } ; } 1 ; lltag-lltag-0.14.6/lib/Lltag/MP3v2.pm000066400000000000000000000161541314167102300170310ustar00rootroot00000000000000package Lltag::MP3v2 ; use strict ; require Lltag::Tags ; require Lltag::Misc ; use constant MP3V2_READ_V1 => 1 ; use constant MP3V2_READ_V2 => 2 ; use constant MP3V2_READ_V1_V2 => 12 ; use constant MP3V2_READ_V2_V1 => 21 ; sub test_MP3Tag { my $self = shift ; if (not eval { require MP3::Tag ; } ) { print "MP3::Tag does not seem to be available, disabling 'MP3v2' backend.\n" if $self->{verbose_opt} ; return -1 ; } return 0 ; } ################################################# # Convert v1 tag to lltag tag name, keep a unique non-null one sub read_v1_tag { my $self = shift ; my $values = shift ; my $v1_field = shift ; my $value = shift ; # not needed return if $v1_field eq 'parent' or $v1_field eq 'mp3' ; # ignore for now, use when we have the list of genres return if $v1_field eq 'genreID' ; # translate into common field names my $field = uc($v1_field) ; $field =~ s/YEAR/DATE/ ; $field =~ s/TRACK/NUMBER/ ; if (grep { $_ =~ $field } @{$self->{field_names}}) { if (exists $values->{$field}) { Lltag::Misc::print_warning (" ", "Duplicated MP3v1 tag '$field', overwriting.") ; } $values->{$field} = $value if $value ; } else { Lltag::Misc::print_warning (" ", "Unrecognized MP3v1 tag '$v1_field', ignoring.") ; } } ################################################# # Convert v2 tag to lltag tag name, append all non-null ones sub read_v2_tag { my $self = shift ; my $values = shift ; my $v2_field = shift ; my $value = shift ; # TODO: restore them too ? return if $v2_field eq "Comments -> Description" or $v2_field eq "Comments -> encoding" or $v2_field eq "Comments -> Language" ; my %v2_field_name_translations = ( "Lead performer(s)/Soloist(s)" => "ARTIST", "Title/songname/content description" => "TITLE", "Album/Movie/Show title" => "ALBUM", "Track number/Position in set" => "NUMBER", "Content type" => "GENRE", "Year" => "DATE", "Comments -> Text" => "COMMENT", ) ; # translate into common field names my $field ; if (exists $v2_field_name_translations{$v2_field}) { $field = $v2_field_name_translations{$v2_field} ; } else { $field = uc($v2_field) ; } # remove the track total from the track number to avoid renaming problems with slashes or so if ($field eq "NUMBER") { if ($value =~ /^(.\d+)/) { $value = $1 ; } else { return ; } } Lltag::Tags::append_tag_multiple_value ($self, $values, $field, $value) ; } ################################################# # Merge v1 and v2 tags, and deal with conflicts sub merge_v1_v2_tags { my $self = shift ; my $v1_values = shift ; my $v2_values = shift ; if ($self->{mp3v2_read_opt} eq MP3V2_READ_V1_V2) { print " Merging MP3 v1 and v2 tags...\n" if $self->{verbose_opt} ; # we should append v2 to v1 below # switch v1 and v2, so that we can append v1 to v2 below my $tmp = $v1_values ; $v1_values = $v2_values ; $v2_values = $tmp ; } else { print " Merging MP3 v2 and v1 tags...\n" if $self->{verbose_opt} ; } # append v1 to v2 foreach my $field (keys %{$v1_values}) { Lltag::Tags::append_tag_multiple_value ($self, $v2_values, $field, $v1_values->{$field}) ; } return $v2_values ; } ################################################# # Read both v1 and v2 if they exist and return their merge sub read_tags { my $self = shift ; my $file = shift ; my $mp3 = MP3::Tag->new ($file) ; $mp3->get_tags(); # Extract ID3v2 first, if it exists my $v2_values = undef ; if ($self->{mp3v2_read_opt} ne MP3V2_READ_V1 and exists $mp3->{ID3v2}) { $v2_values = {} ; print " Found a MP3 v2 tag, reading it...\n" if $self->{verbose_opt} ; my $id3v2 = $mp3->{ID3v2} ; my $frameIDs_hash = $id3v2->get_frame_ids('truename'); foreach my $frame (keys %$frameIDs_hash) { # drop private frames next if $frame eq "PRIV" ; my ($info, $name, @infos) = $id3v2->get_frame($frame); unshift @infos, $info ; foreach $info (@infos) { if (ref $info) { while(my ($key, $value) = each %$info) { read_v2_tag $self, $v2_values, "$name -> $key", $value ; } } else { read_v2_tag $self, $v2_values, $name, $info ; } } } } # Extract ID3v1 last, if it exists, since v2 is generally preferred my $v1_values = undef ; if ($self->{mp3v2_read_opt} ne MP3V2_READ_V2 and exists $mp3->{ID3v1}) { $v1_values = {} ; print " Found a MP3 v1 tag, reading it...\n" if $self->{verbose_opt} ; my $id3v1 = $mp3->{ID3v1} ; map { read_v1_tag $self, $v1_values, $_, $id3v1->{$_} } (keys %{$id3v1}) ; } return $v2_values unless defined $v1_values ; return $v1_values unless defined $v2_values ; return merge_v1_v2_tags $self, $v1_values, $v2_values ; } ################################################# # Set tags sub set_one_v2_tag { my $id3v2 = shift ; my $value = shift ; my @frame_args = @_ ; if (ref($value) eq 'ARRAY') { foreach my $val (@{$value}) { $id3v2->add_frame(@_, $val) ; } } else { $id3v2->add_frame(@_, $value) ; } } sub set_tags { my $self = shift ; my $file = shift ; my $values = shift ; # TODO: dry-run # TODO: disable v1 or v2 ? my $mp3 = MP3::Tag->new ($file) ; $mp3->get_tags(); # clear existing tags if (exists $mp3->{ID3v1}) { $mp3->{ID3v1}->remove_tag ; } if (exists $mp3->{ID3v2}) { $mp3->{ID3v2}->remove_tag ; } # add a new v1 tag my $id3v1 = $mp3->new_tag("ID3v1"); map { # warning about unknown v1 tag in verbose mode only, since v2 tag will be ok Lltag::Misc::print_warning (" ", "Cannot set $_ in mp3v1 tags") if $self->{verbose_opt} ; } (Lltag::Tags::get_values_non_regular_keys ($self, $values)) ; map { # only one tag is allowed in v1, use the first one my $value = Lltag::Tags::get_tag_unique_value ($self, $values, $_) ; # convert to MP3v1 tag name my $field = lc($_) ; $field =~ s/date/year/ ; $field =~ s/number/track/ ; # set tag $id3v1->$field ($value) ; } ( grep { defined $values->{$_} } @{$self->{field_names}} ) ; # commit changes $id3v1->write_tag () ; # add a new v2 tag my $id3v2 = $mp3->new_tag("ID3v2"); my %v2_frame_name_translations = ( "ARTIST" => "TPE1", "TITLE" => "TIT2", "ALBUM" => "TALB", "NUMBER" => "TRCK", "GENRE" => "TCON", "DATE" => "TYER", ) ; map { my $field = $_ ; my $frame ; if (exists $v2_frame_name_translations{$field}) { set_one_v2_tag $id3v2, $values->{$field}, $v2_frame_name_translations{$field} ; } elsif ($field eq "COMMENT") { set_one_v2_tag $id3v2, $values->{$field}, "COMM", "", "" ; } else { # FIXME: set other fields as comments ? print "Cannot set $field in MP3 ID3v2 tags\n" ; } } (keys %{$values}) ; # commit changes $id3v2->write_tag () ; } ################################################# # Initialization sub new { my $self = shift ; return undef if test_MP3Tag $self ; return { name => "MP3v2 (using MP3::Tag)", type => "mp3", extension => "mp3", read_tags => \&read_tags, set_tags => \&set_tags, } ; } 1 ; lltag-lltag-0.14.6/lib/Lltag/Misc.pm000066400000000000000000000134661314167102300170600ustar00rootroot00000000000000package Lltag::Misc ; use strict ; use Term::ReadLine; use Term::ANSIColor ; # are we running in a normal terminal ? if not, disable readline, colors and bold/underline formatting my $stdio_is_a_tty = 0 ; ################################################################### # rewrite of system which returns a descriptor of a stream # containing both stdout and stderr sub system_with_output { pipe (my $pipe_out, my $pipe_in) ; my $pid = fork() ; if ($pid < 0) { # in the father, when fork failed close $pipe_in ; close $pipe_out ; return (-1, "Failed to fork to execute command line: ". join (" ", @_) ."\n") ; } elsif ($pid > 0) { # in the father, when fork done close $pipe_in ; waitpid($pid, 0); my $status = $? >> 8 ; $status = -1 if $status == 255 ; my @lines = <$pipe_out> ; close $pipe_out ; return ( $status , @lines ) ; } else { # in the child close $pipe_out ; open STDERR, ">&", $pipe_in ; open STDOUT, ">&", $pipe_in ; { exec @_ } ; print $pipe_in "Failed to execute command line: ". join (" ", @_) ."\n" ; print $pipe_in "Please install $_[0] properly (see README).\n" if $!{ENOENT} or $!{EPERM} ; close $pipe_in ; exit -1 ; } } ################################################################### # configure readline depending on the features provided by the installation my $term ; my $attribs ; my $readline_firsttime ; my $myreadline ; my $history_dir ; my $history_file ; # dumb readline replacement sub dummy_readline { my $indent = shift ; my $prompt = shift ; my $preput = shift ; my $clear_allowed = shift ; # 0 = no clearing, 1 = clearing allowed and documented, -1 = clearing allowed (normal behavior) $preput = "" if not defined $preput ; if ($readline_firsttime) { print $indent."You might want to install an advanced Perl readline module such as 'Term::ReadLine::GNU'.\n" ; print $indent."The current value is given in parenthesis, to keep it" . ($clear_allowed>0 ? ", to clear it" : "") . ".\n" ; $readline_firsttime = 0 ; } ASK: my $val = $term->readline ("$indent$prompt".($preput ? " ($preput)" : "")." ? ") ; return $preput if !$val ; $val = "" if $val eq "CLEAR" or $val eq "" ; if (!$val and !$clear_allowed) { print "$indent Clearing is not allowed here.\n" ; goto ASK ; } print "\n" unless defined $val ; return $val ; } # true readline wrapper sub real_readline { my $indent = shift ; my $prompt = shift ; my $preput = shift ; my $clear_allowed = shift ; $preput = "" if not defined $preput ; ASK: my $val = $term->readline ("$indent$prompt ? ", $preput) ; if (!$val and !$clear_allowed) { print "$indent Clearing is not allowed here.\n" ; goto ASK ; } print "\n" unless defined $val ; return $val ; } # the actual wrapper sub readline { die "ERROR: Interactive mode not available in this environment.\n" unless $stdio_is_a_tty ; return &$myreadline (@_) ; } # initialization sub init_readline { my $self = shift ; $history_dir = $self->{user_lltag_dir} ; $history_file = $self->{lltag_edit_history_filename} ; # detect whether readline works eval { my ($IN,$OUT) = Term::ReadLine->findConsole(); open IN, "<$IN" || die "Cannot open $IN for read\n"; close IN ; open OUT, ">$OUT" || die "Cannot open $OUT for write\n"; close OUT ; } or return ; $stdio_is_a_tty = 1 ; $term = Term::ReadLine->new('lltag editor') ; $attribs = $term->Attribs ; $term->ornaments('md,me,,') ; $readline_firsttime = 1 ; # read the history file eval { if (-f $history_dir."/".$history_file) { $term->ReadHistory ($history_dir."/".$history_file) or warn "Failed to open history file $history_dir/$history_file: $!\n" ; } } unless $term->Features->{ReadHistory} ; if ($term->Features->{preput}) { $myreadline = \&real_readline ; $term->MinLine(3) ; } else { $myreadline = \&dummy_readline ; } } # exit, saves readline history if supported by the installation sub exit_readline { return unless $stdio_is_a_tty ; # only keep the last 100 entries eval { $term->StifleHistory (100); } unless $term->Features->{StifleHistory} ; # save the history file eval { if (!-d $history_dir."/") { mkdir $history_dir or warn "Failed to create $history_dir directory to store the history file: $!.\n" ; } $term->WriteHistory ($history_dir."/".$history_file) or warn "Failed to write history file $history_dir/$history_file: $!.\n" ; } unless $term->Features->{WriteHistory} ; } ################################################################### # Print a usage header in underlined sub print_usage_header { print shift ; print color 'underline' if $stdio_is_a_tty; print shift ; print " - Usage:" ; print color 'reset' if $stdio_is_a_tty ; print "\n" ; } ################################################################### # Print a notice or a warning in underlined sub print_notice { print shift ; print color 'underline' if $stdio_is_a_tty ; print "NOTICE:" ; print color 'reset' if $stdio_is_a_tty ; print " ".(shift)."\n" ; } sub print_warning { print shift ; print color 'underline' if $stdio_is_a_tty ; print "WARNING:" ; print color 'reset' if $stdio_is_a_tty ; print " ".(shift)."\n" ; } ################################################################### # Print an error in underlined and bold sub format_error { if ($stdio_is_a_tty) { return (color 'bold').(color 'underline')."ERROR:".(color 'reset')." " .(color 'bold').(shift).(color 'reset') ; } else { return "ERROR: ".shift unless $stdio_is_a_tty ; } } sub print_error { print shift ; print ((format_error(shift))."\n") ; } sub die_error { print ((format_error(shift))."\n") ; exit_readline () ; exit -1 ; } 1 ; lltag-lltag-0.14.6/lib/Lltag/OGG.pm000066400000000000000000000043441314167102300165740ustar00rootroot00000000000000package Lltag::OGG ; use strict ; require Lltag::Tags ; require Lltag::Misc ; sub test_vorbiscomment { my $self = shift ; my ($status, @output) = Lltag::Misc::system_with_output ("vorbiscomment", "-h") ; print "vorbiscomment does not seem to work, disabling 'OGG' backend.\n" if $status and $self->{verbose_opt} ; return $status ; } sub read_tags { my $self = shift ; my $file = shift ; my ($status, @output) = Lltag::Misc::system_with_output ("vorbiscomment", "-l", $file) ; return undef if $status ; @output = map { my $line = $_ ; $line =~ s/^TRACKNUMBER=/NUMBER=/ ; $line } @output ; return Lltag::Tags::convert_tag_stream_to_values ($self, @output) ; } sub set_tags { my $self = shift ; my $file = shift ; my $values = shift ; my %field_name_ogg_translations = ( 'NUMBER' => 'TRACKNUMBER', ) ; my @ogg_tagging_cmd = ( 'vorbiscomment', '-q' ) ; my @ogg_tagging_clear_option = ( '-w' ) ; # apply regular tags my @regular_tags_args = ( map { my $oggname = $_ ; $oggname = $field_name_ogg_translations{$_} if defined $field_name_ogg_translations{$_} ; my @tags = (Lltag::Tags::get_tag_value_array $self, $values, $_) ; map { ( "-t" , $oggname."=".$_ ) } @tags } @{$self->{field_names}} ) ; # apply non-regular tags my @non_regular_tags_args = ( map { my $oggname = $_ ; my @tags = (Lltag::Tags::get_tag_value_array $self, $values, $_) ; map { ( "-t" , $oggname."=".$_ ) } @tags } Lltag::Tags::get_values_non_regular_keys ($self, $values) ) ; # work-around vorbiscomment which does not like when tags is passed my @workaround_args = (scalar @regular_tags_args + @non_regular_tags_args) ? () : ("-c", "/dev/null") ; my @system_args = ( @ogg_tagging_cmd , # clear all tags @ogg_tagging_clear_option , @regular_tags_args , @non_regular_tags_args , @workaround_args , $file ) ; Lltag::Tags::set_tags_with_external_prog ($self, @system_args) ; } sub new { my $self = shift ; return undef if test_vorbiscomment $self ; return { name => "OGG (using vorbiscomment)", type => "ogg", extension => "ogg", read_tags => \&read_tags, set_tags => \&set_tags, } ; } 1 ; lltag-lltag-0.14.6/lib/Lltag/Parse.pm000066400000000000000000000511001314167102300172220ustar00rootroot00000000000000package Lltag::Parse ; use strict ; no strict "refs" ; # for ${$i} # ignoring fields during parsing use constant IGNORE_LETTER => 'i' ; use constant IGNORE_NAME => 'IGNORE' ; # subregexp my $match_path = '(?:[^/]*\/)*' ; my $match_any = '((?:[^ /]+ +)*[^ /]+)' ; my $match_num = '([0-9]+)' ; my $match_space = ' '; my $match_spaces = ' *' ; my $match_limit = '' ; # the parser that the user wants to always use my $preferred_parser = undef ; # confirmation behavior my $current_parse_ask_opt ; my $current_parse_yes_opt ; ####################################################### # Parsing return values use constant PARSE_SUCCESS_PREFERRED => 1 ; use constant PARSE_SUCCESS => 0 ; use constant PARSE_ABORT => -1 ; use constant PARSE_SKIP_PARSER => -2 ; use constant PARSE_SKIP_PATH_PARSER => -3 ; use constant PARSE_NO_MATCH => -4 ; # Parsing acceptable behavior use constant PARSE_MAY_SKIP_PARSER => 1 ; use constant PARSE_MAY_SKIP_PATH_PARSER => 2 ; use constant PARSE_MAY_PREFER => 4 ; ####################################################### # initialization my $confirm_parser_usage_forced ; sub init_parsing { my $self = shift ; # default confirmation behavior $current_parse_ask_opt = $self->{ask_opt} ; $current_parse_yes_opt = $self->{yes_opt} ; # spaces_opt changes matching regexps $match_limit = $match_space = $match_spaces if $self->{spaces_opt} ; # need to show menu usage once ? $confirm_parser_usage_forced = $self->{menu_usage_once_opt} ; } ####################################################### # parsing format specific usage sub parsing_format_usage { my $self = shift ; print " %".IGNORE_LETTER." means that the text has to be ignored\n" ; print " %% means %\n" ; } ####################################################### # parsing confirmation sub confirm_parser_letters { my $behaviors = shift ; my $string = "[y" ; $string .= "u" if $behaviors & PARSE_MAY_PREFER ; $string .= "a" ; $string .= "n" if $behaviors & PARSE_MAY_SKIP_PARSER ; $string .= "p" if $behaviors & PARSE_MAY_SKIP_PATH_PARSER ; $string .= "q]" ; return $string ; } sub confirm_parser_usage { my $behaviors = shift ; Lltag::Misc::print_usage_header (" ", "Parsing filenames") ; print " y => Yes, use this matching (default)\n" ; print " u => Use this format for all files until one does not match\n" if $behaviors & PARSE_MAY_PREFER ; print " a => Always yes, stop asking for a confirmation\n" ; print " n => No, try the next matching format\n" if $behaviors & PARSE_MAY_SKIP_PARSER ; print " p => No, try the next path matching format\n" if $behaviors & PARSE_MAY_SKIP_PATH_PARSER ; print " q => Quit parsing, stop trying to parse this filename\n" ; print " h => Show this help\n" ; $confirm_parser_usage_forced = 0 ; } sub confirm_parser { my $self = shift ; my $file = shift ; my $confirm = shift ; my $behaviors = shift ; my $values = shift ; # prefer this type of tagging ? my $preferred = 0 ; # confirm if required if ($current_parse_ask_opt or ($confirm and !$current_parse_yes_opt)) { confirm_parser_usage $behaviors if $confirm_parser_usage_forced ; while (1) { my $reply = Lltag::Misc::readline (" ", "Use this matching ".(confirm_parser_letters ($behaviors))." (default is yes, h for help)", "", -1) ; # if ctrl-d, stop trying to parse $reply = 'q' unless defined $reply ; if ($reply eq "" or $reply =~ m/^y/) { last ; } elsif ($reply =~ m/^a/) { $current_parse_ask_opt = 0 ; $current_parse_yes_opt = 1 ; last ; } elsif ($behaviors & PARSE_MAY_PREFER and $reply =~ m/^u/) { $preferred = 1 ; $current_parse_ask_opt = 0 ; $current_parse_yes_opt = 1 ; last ; } elsif ($behaviors & PARSE_MAY_SKIP_PARSER and $reply =~ m/^n/) { return (PARSE_SKIP_PARSER, undef) ; } elsif ($behaviors & PARSE_MAY_SKIP_PATH_PARSER and $reply =~ m/^p/) { return (PARSE_SKIP_PATH_PARSER, undef) ; } elsif ($reply =~ m/^q/) { return (PARSE_ABORT, undef) ; } else { confirm_parser_usage $behaviors ; } } } if ($preferred) { return (PARSE_SUCCESS_PREFERRED, $values) ; } else { return (PARSE_SUCCESS, $values) ; } } ####################################################### # actual parsing sub apply_parser { my $self = shift ; my $file = shift ; my $parsename = shift ; my $parser = shift ; my $confirm = shift ; my $behaviors = shift ; my @matches ; # protect against bad regexp, just in case (we should have found problems during initialization) eval { @matches = ($parsename =~ m/^$parser->{regexp}$/) ; 1 ; # be sure to return success when the regexp does not match } or Lltag::Misc::die_error ("Failed to apply parser '$parser->{title}', regexp '$parser->{regexp}' is invalid?") ; # we ensure earlier that there is at least one field to match, so an error will return () return (PARSE_SKIP_PARSER, undef) unless @matches ; print " '$parser->{title}' matches this file...\n" ; my @field_table = @{$parser->{field_table}} ; # check the number of matches Lltag::Misc::die_error ("Matched ".(scalar @matches)." fields instead of ".(scalar @field_table).", parser invalid?") unless @matches == @field_table ; my $values = {} ; # traverse matches for(my $i=0; $i<@field_table; $i++) { my $field = $field_table[$i] ; if ($field ne IGNORE_NAME) { my $val = $matches[$i] ; # apply maj, sep and regexp to the value $val =~ s/\b(.)/uc $1/eg if $self->{maj_opt} ; $val =~ s/($self->{sep_opt})/ /g if defined $self->{sep_opt} ; map { $val = Lltag::Tags::apply_regexp_to_tag ($val, $_, $field) } @{$self->{regexp_opts}} ; # check whether it's already defined. # TODO: append ? if (defined $values->{$field}) { Lltag::Misc::print_warning (" ", ucfirst($field)." already set to '".$values->{$field} ."', skipping new value '$val'") if $values->{$field} ne $val ; next ; } # ok $values->{$field} = $val ; if ($self->{verbose_opt} or $confirm or $current_parse_ask_opt) { print " ". ucfirst($field) .$self->{field_name_trailing_spaces}{$field} .": ". $val ."\n" ; } } } return confirm_parser ($self, $file, $confirm, $behaviors, $values) ; } ####################################################### # internal parsers my @internal_basename_parsers = () ; my @internal_path_parsers = () ; sub add_internal_parser { my $self = shift ; my $file = shift ; my $startline = shift ; my $type = shift ; my $title = shift ; my $regexp = shift ; my $regexp_size = shift ; my $field_table = shift ; if ($type and $title and $regexp and @{$field_table}) { my $parser ; $parser->{title} = $title ; $parser->{regexp} = $regexp ; @{$parser->{field_table}} = @{$field_table} ; # check whether there are the same number of fields in the regexp and in the field_table Lltag::Misc::die_error (" Parser '$title' at line $startline in file '$file' needs same number of matching fields in regexp ($regexp_size) and indices (".(scalar@{$field_table} ).").") unless $regexp_size == scalar @{$field_table} ; # check whether the regexp is applicable eval { my $dummy = ("dummy" =~ m@^$regexp/[^/]+$@) ; # be sure to return success even if not matched 1 ; } or # print the parser and its formats file (not the line since we may be way later already Lltag::Misc::die_error (" Parser '$title' regexp '$regexp' looks invalid at line $startline in file '$file'.") ; # add the parser if ($type eq "basename" or $type eq "filename") { # TODO: drop filename support on september 20 2006 print " Got basename format '$title' (regexp '$regexp')\n" if $self->{verbose_opt} ; push (@internal_basename_parsers, $parser) ; } elsif ($type eq "path") { print " Got path format '$title' (regexp '$regexp')\n" if $self->{verbose_opt} ; push (@internal_path_parsers, $parser) ; } } elsif ($type or $title or $regexp or @{$field_table}) { Lltag::Misc::die_error ("Incomplete format at line $startline in file '$file'.") ; } } sub read_internal_parsers { my $self = shift ; # get parsers from configuration files my $file ; if (open FORMAT, "$self->{user_lltag_dir}/$self->{lltag_format_filename}") { $file = "$self->{user_lltag_dir}/$self->{lltag_format_filename}" ; } elsif (open FORMAT, "$self->{common_lltag_dir}/$self->{lltag_format_filename}") { $file = "$self->{common_lltag_dir}/$self->{lltag_format_filename}" ; } else { print "Did not find any format file.\n" ; goto NO_FORMATS_FILE_FOUND; } print "Reading format file '$file'...\n" if $self->{verbose_opt} ; my $startline = undef ; my $type = undef ; my $title = undef ; my $regexp = undef ; my $regexp_size = undef ; my @field_table = () ; while () { chomp $_ ; next if /^#/ ; next if /^$/ ; if (/^\[(.*)\]$/) { add_internal_parser $self, $file, $startline, $type, $title, $regexp, $regexp_size, \@field_table ; $startline = $. ; $type = undef ; $regexp = undef ; $regexp_size = undef ; @field_table = () ; $title = $1 ; # stocker la ligne ? } elsif (/^type = (.*)$/) { Lltag::Misc::die_error ("Unsupported format type '$1' at line $. in file '$file'.") if $1 ne "basename" and $1 ne "filename" and $1 ne "path" ; # TODO: drop filename support on september 20 2006 $type = $1 ; } elsif (/^regexp = (.*)$/) { $regexp = $1 ; # escape special characters # FIXME: add *+$^ ? $regexp =~ s/\./\\./g ; $regexp =~ s/\(/\\\(/g ; $regexp =~ s/\)/\\\)/g ; $regexp =~ s/\[/\\\[/g ; $regexp =~ s/\]/\\\]/g ; $regexp =~ s@/@\\/@g ; $regexp_size = 0 ; # do the replacement progressively so that %% and %x and not mixed while ($regexp =~ m/(%(?:P|L|S|N|A|%))/) { if ($1 eq '%P') { $regexp =~ s/%P/$match_path/ ; } elsif ($1 eq '%L') { $regexp =~ s/%L/$match_limit/ ; } elsif ($1 eq '%S') { $regexp =~ s/%S/$match_space/ ; } elsif ($1 eq '%N') { $regexp =~ s/%N/$match_num/ ; $regexp_size++; } elsif ($1 eq '%A') { $regexp =~ s/%A/$match_any/ ; $regexp_size++; } elsif ($1 eq '%%') { $regexp =~ s/%%/%/ ; } } Lltag::Misc::die_error ("Parser '$title' at line $startline in file '$file' needs at least one matching %A or %N in its regexp.") unless $regexp_size ; } elsif (/^indices = (.*)$/) { my @name_table = split (/,/, $1) ; Lltag::Misc::die_error ("Parser '$title' at line $startline in file '$file' needs at least one indice.") unless @name_table ; @field_table = map { my $field ; if (defined $self->{field_name_letter}{$_} or $_ eq IGNORE_NAME) { # full field name, keep as it is $field = $_ } elsif (defined $self->{field_letter_name}{$_}) { # field letter $field = $self->{field_letter_name}{$_} ; } elsif ($_ eq IGNORE_LETTER) { # ignore letter $field = IGNORE_NAME ; } else { Lltag::Misc::die_error ("Unrecognized field '$_' on line $. in file '$file'.") ; } $field } @name_table ; } else { Lltag::Misc::die_error ("Unrecognized line $. in file '$file': '$_'.") ; } } close FORMAT ; # save the last format add_internal_parser $self, $file, $startline, $type, $title, $regexp, $regexp_size, \@field_table ; NO_FORMATS_FILE_FOUND: } sub list_internal_parsers { # path+basename foreach my $path_parser (@internal_path_parsers) { foreach my $basename_parser (@internal_basename_parsers) { print " $path_parser->{title}/$basename_parser->{title}\n" ; } } # basename only foreach my $basename_parser (@internal_basename_parsers) { print " $basename_parser->{title}\n" ; } } sub merge_internal_parsers { my $path_parser = shift ; my $basename_parser = shift ; my $parser ; $parser->{title} = "$path_parser->{title}/$basename_parser->{title}" ; $parser->{regexp} = "$path_parser->{regexp}/$basename_parser->{regexp}" ; @{$parser->{field_table}} = (@{$path_parser->{field_table}}, @{$basename_parser->{field_table}}) ; return $parser ; } sub apply_internal_basename_parsers { my $self = shift ; my $file = shift ; my $parsename = shift ; # no path, only try each basename parser foreach my $basename_parser (@internal_basename_parsers) { # try to tag, with confirmation my ($res, $values) = apply_parser $self, $file, $parsename, $basename_parser, 1, PARSE_MAY_PREFER|PARSE_MAY_SKIP_PARSER ; if ($res == PARSE_SUCCESS || $res == PARSE_SUCCESS_PREFERRED || $res == PARSE_ABORT) { if ($res == PARSE_SUCCESS_PREFERRED) { $preferred_parser = $basename_parser ; } return ($res, $values) ; } # try next parser die "Unknown tag return value: $res.\n" # this is a bug if $res != PARSE_SKIP_PARSER ; } return (PARSE_NO_MATCH, undef) ; } sub apply_internal_path_basename_parsers { my $self = shift ; my $file = shift ; my $parsename = shift ; # try each path parser and each basename parser foreach my $path_parser (@internal_path_parsers) { # match the path only first, to reduce number of (path,basename) parsers to try, # and to check that there are no '/' afterwards # protect against bad regexp, just in case (we should have found problems during initialization) my $res ; eval { $res = ($parsename =~ m@^$path_parser->{regexp}/[^/]+$@) ; 1 ; # be sure to return success when the regexp does not match } or Lltag::Misc::die_error ("Failed to apply parser '$path_parser->{title}', regexp '$path_parser->{regexp}' is invalid?") ; if ($res) { foreach my $basename_parser (@internal_basename_parsers) { my $whole_parser = merge_internal_parsers ($path_parser, $basename_parser) ; # try to tag, with confirmation my ($res, $values) = apply_parser $self, $file, $parsename, $whole_parser, 1, PARSE_MAY_PREFER|PARSE_MAY_SKIP_PARSER|PARSE_MAY_SKIP_PATH_PARSER ; if ($res == PARSE_SUCCESS || $res == PARSE_SUCCESS_PREFERRED || $res == PARSE_ABORT) { if ($res == PARSE_SUCCESS_PREFERRED) { $preferred_parser = $whole_parser ; } return ($res, $values) ; } # try next path parser if asked goto NEXT_PATH_PARSER if $res == PARSE_SKIP_PATH_PARSER ; # try next parser die "Unknown tag return value: $res.\n" # this is a bug if $res != PARSE_SKIP_PARSER ; } } NEXT_PATH_PARSER: } return (PARSE_NO_MATCH, undef) ; } ####################################################### # user parsers # list of user-provided parsers my @user_parsers ; # change a format strings into usable infos sub generate_user_parser { my $self = shift ; my $format_string = shift ; print "Generating parser for format '". $format_string ."'...\n" ; my $parser ; $parser->{title} = $format_string ; # merge spaces if --spaces was passed if ($self->{spaces_opt}) { $format_string =~ s/ +/ /g ; } # create the regexp and store indice fields my @array = split(//, $format_string) ; my @field_table = () ; for(my $i = 0; $i < @array; $i++) { my $char = $array[$i] ; # normal characters if ($char ne "%") { if ($char eq " ") { # replace spaces with general space matching regexp $array[$i] = $match_space ; } elsif ($char eq "/") { # replace / with space flexible matching regexp $array[$i] = $match_limit."/".$match_limit ; } elsif (index ("()[]", $char) != -1) { # escape regexp control characters $array[$i] = "\\".$char ; } # keep this character next ; } if ($i == @array - 1) { Lltag::Misc::die_error ("Format '". $format_string ."' ends with '%' without operator letter.") ; } # remove % and check next char splice (@array, $i, 1) ; # replace the char with the matching $char = $array[$i] ; next if $char eq "%" ; if ($char eq "n") { $array[$i] = $match_num ; } elsif ($char =~ m/$self->{field_letters_union}/) { $array[$i] = $match_any ; } elsif ($char eq IGNORE_LETTER) { # looks like constants do not work in regexp $array[$i] = $match_any ; } else { Lltag::Misc::die_error ("Format '". $format_string ."' contains unrecognized operator '%". $array[$i] ."'.") ; } # store the indice if ($char eq IGNORE_LETTER) { push @field_table, IGNORE_NAME ; } else { push @field_table, $self->{field_letter_name}{$char} ; } } @{$parser->{field_table}} = @field_table ; Lltag::Misc::die_error ("Format '$format_string' does not contain any matching field.") unless @field_table ; # done if ($self->{spaces_opt}) { $parser->{regexp} = $match_limit. join("", @array) .$match_limit ; } else { $parser->{regexp} = join("", @array) ; } # check insolvable regexp for(my $i = 0; $i < @array - 1; $i++) { my $char = $array[$i] ; my $nextchar = $array[$i+1] ; if ( $char eq $match_any and ( $nextchar eq $match_any or $nextchar eq $match_num ) ) { Lltag::Misc::print_warning (" ", "Format '". $format_string ."' leads to problematic subregexp '". $char.$nextchar ."' that won't probably match as desired") ; } } if ($self->{verbose_opt}) { print " Format string will parse with: ". $parser->{regexp} ."\n" ; print " Fields are: ". (join ',', @field_table) ."\n" ; } return $parser ; } sub generate_user_parsers { my $self = shift ; @user_parsers = map ( generate_user_parser ($self, $_), @{$self->{user_format_strings}} ) ; } sub apply_user_parsers { my $self = shift ; my $file = shift ; my $parsename = shift ; # try each format until one works foreach my $parser (@user_parsers) { # try to tag, without confirmation my ($res, $values) = apply_parser $self, $file, $parsename, $parser, 0, PARSE_MAY_PREFER|PARSE_MAY_SKIP_PARSER ; if ($res == PARSE_SUCCESS || $res == PARSE_SUCCESS_PREFERRED || $res == PARSE_ABORT) { if ($res == PARSE_SUCCESS_PREFERRED) { $preferred_parser = $parser ; } return ($res, $values) ; } print " '". $parser->{title} ."' does not match.\n" ; # try next parser die "Unknown tag return value: $res.\n" # this is a bug if $res != PARSE_SKIP_PARSER ; } return (PARSE_NO_MATCH, undef) ; } ####################################################### # high-level parsing routines sub try_to_parse_with_preferred { my $self = shift ; my $file = shift ; my $parsename = shift ; my $values = undef ; my $res ; # try the preferred parser first return (PARSE_NO_MATCH, undef) unless defined $preferred_parser ; print " Trying to parse filename with the previous matching parser...\n" ; # there can't be any confirmation here, SKIP is not possible ($res, $values) = apply_parser $self, $file, $parsename, $preferred_parser, 0, 0 ; if ($res != PARSE_SKIP_PARSER) { # only SUCCESS if possible die "Unknown tag return value: $res.\n" # this is a bug if $res != PARSE_SUCCESS ; return ($res, $values) ; } else { Lltag::Misc::print_notice (" ", "'$preferred_parser->{title}' does not match anymore, returning to original mode") ; $current_parse_ask_opt = $self->{ask_opt} ; $current_parse_yes_opt = $self->{yes_opt} ; $preferred_parser = undef ; return (PARSE_NO_MATCH, undef) ; } } my $user_parsers_initialized = 0 ; my $internal_parsers_initialized = 0 ; sub try_to_parse { my $self = shift ; my $file = shift ; my $parsename = shift ; my $try_internals = shift ; my $values = undef ; my $res ; # initialize user parsers once if (!$user_parsers_initialized) { generate_user_parsers ($self) ; $user_parsers_initialized = 1 ; } # try user provided parsers first if (@user_parsers) { print " Trying to parse filename with user-provided formats...\n" ; ($res, $values) = apply_user_parsers $self, $file, $parsename ; return ($res, $values) if $res == PARSE_SUCCESS or $res == PARSE_SUCCESS_PREFERRED or $res == PARSE_ABORT ; } # try to guess my internal format database then if ($try_internals) { print " Trying to parse filename with internal formats...\n" ; # initialize internal parsers once if (!$internal_parsers_initialized) { read_internal_parsers ($self) ; $internal_parsers_initialized = 1 ; } if ($self->{no_path_opt} or $parsename !~ m@/@) { ($res, $values) = apply_internal_basename_parsers $self, $file, $parsename ; } else { ($res, $values) = apply_internal_path_basename_parsers $self, $file, $parsename ; } return ($res, $values) if $res == PARSE_SUCCESS or $res == PARSE_SUCCESS_PREFERRED or $res == PARSE_ABORT ; } if ($try_internals or @user_parsers) { print " Didn't find any parser!\n" ; } return (PARSE_NO_MATCH, undef) ; } 1 ; lltag-lltag-0.14.6/lib/Lltag/Rename.pm000066400000000000000000000143111314167102300173620ustar00rootroot00000000000000package Lltag::Rename ; use strict ; use Lltag::Misc ; # constants for rename format specific letters use constant DIRNAME_LETTER => "P" ; use constant BASENAME_LETTER => "F" ; use constant EXTENSION_LETTER => "E" ; # confirmation behavior my $current_rename_yes_opt ; ####################################################### # rename specific usage sub rename_usage { my $self = shift ; print " Renaming options:\n" ; print " --rename Rename file according to format\n" ; print " --rename-min Lowcase tags before renaming\n" ; print " --rename-sep Replace spaces with s in tags before renaming\n" ; print " --rename-slash Replace slashes with s in tags before renaming\n" ; print " --rename-regexp Apply a replace regexp to tags before renaming\n" ; print " --rename-ext Assume the rename format provides an extension\n" ; } ####################################################### # rename format specific usage sub rename_format_usage { my $self = shift ; print " %".BASENAME_LETTER." means the original basename of the file\n" ; print " %".EXTENSION_LETTER." means the original extension of the file\n" ; print " %".DIRNAME_LETTER." means the original path of the file\n" ; } ####################################################### # init my $rename_confirm_usage_forced ; sub init_renaming { my $self = shift ; # default confirmation behavior $current_rename_yes_opt = $self->{yes_opt} ; # need to show menu usage once ? $rename_confirm_usage_forced = $self->{menu_usage_once_opt} ; } ####################################################### # rename confirmation sub rename_confirm_usage { Lltag::Misc::print_usage_header (" ", "Renaming files") ; print " y => Yes, rename this file (default)\n" ; print " a => Always rename without asking\n" ; print " e => Edit the filename before tagging\n" ; print " n/q => No, don't rename this file\n" ; print " h => Show this help\n" ; $rename_confirm_usage_forced = 0 ; } ####################################################### # main rename routine sub rename_with_values { my $self = shift ; my $file = shift ; my $extension = shift ; my $values = shift ; my $rename_values = {} ; my $undefined = 0 ; print " Renaming with format '$self->{rename_opt}'...\n" ; # make sure we find tags through their upcase names my $ucvalues = Lltag::Tags::clone_tag_values_uc ($self, $values) ; foreach my $field (keys %{$ucvalues}) { # use the first tag for renaming my $val = Lltag::Tags::get_tag_unique_value ($self, $ucvalues, $field) ; $val = lc ($val) if $self->{rename_min_opt} ; $val =~ s/ /$self->{rename_sep_opt}/g if $self->{rename_sep_opt} ; $val =~ s/\//$self->{rename_slash_opt}/g ; map { $val = Lltag::Tags::apply_regexp_to_tag ($val, $_, $field) } @{$self->{rename_regexp_opts}} ; $rename_values->{$field} = $val ; } my $format_string = $self->{rename_opt} ; my @array = split(//, $format_string) ; for(my $i = 0; $i < @array - 1; $i++) { # normal characters next if $array[$i] ne "%" ; # remove % and check next char splice (@array, $i, 1) ; # replace the char with the matching my $char = $array[$i] ; next if $char eq "%" ; if ($char =~ m/$self->{field_letters_union}/) { my $field = $self->{field_letter_name}{$char} ; my $val = $rename_values->{$field} ; # rename does not contain an array anymore if (not defined $val) { $undefined++ ; Lltag::Misc::print_warning (" ", "Undefined field '".$field."'") ; $val = "" ; } if ($char eq 'n') { # initialize track number to 0 if empty $val = "0" if !$val ; # make it at least 2 digits $val = '0'.$val if $val < 10 and length $val < 2 ; } $array[$i] = $val ; } elsif ($char eq BASENAME_LETTER) { my $basename ; if ($file =~ m@([^/]+)\.[^./]+$@) { $basename = $1 ; } elsif ($file =~ m@([^/]+)$@) { $basename = $1 ; } else { $basename = $file ; } $array[$i] = $basename ; } elsif ($char eq EXTENSION_LETTER) { my $extension ; if ($file =~ m@\.([^./]+)$@) { $extension = $1 ; } else { $extension = "" ; } $array[$i] = $extension ; } elsif ($char eq DIRNAME_LETTER) { my $path ; if ($file =~ m@^(.*/)[^/]+@) { $path = $1 ; } else { $path = "" ; } $array[$i] = $1 ; } else { $array[$i] = "%".$char ; } } my $new_name = join ("", @array) ; $new_name .= ".". $extension unless $self->{rename_ext_opt} ; print " New filename is '$new_name'\n" ; # confirm if required or if any field undefined if ($undefined or !$current_rename_yes_opt) { rename_confirm_usage if $rename_confirm_usage_forced ; ASK_CONFIRM: my $reply = Lltag::Misc::readline (" ", "Really rename the file [yaeq] (default is yes, h for help)", "", -1) ; # if ctrl-d, do not rename $reply = 'q' unless defined $reply ; if ($reply eq "" or $reply =~ m/^y/i) { goto RENAME_IT ; } elsif ($reply =~ m/^a/) { $current_rename_yes_opt = 1 ; goto RENAME_IT ; } elsif ($reply =~ m/^n/ or $reply =~ m/^q/) { return ; } elsif ($reply =~ m/^e/) { my $newnew_name = Lltag::Misc::readline (" ", "New filename", $new_name, 0) ; # if ctrl-d, keep same filename $new_name = $newnew_name if defined $newnew_name ; goto ASK_CONFIRM ; } else { rename_confirm_usage ; goto ASK_CONFIRM ; } } RENAME_IT: if ($new_name eq $file) { print " Filename would not change, not renaming\n" ; return ; } if (-e $new_name) { print " File $new_name already exists, not renaming\n" ; return ; } return if $self->{dry_run_opt} ; my $remain = $new_name ; my $path = '' ; while ($remain =~ m@^([^/]*/+)(.*)$@) { $path .= $1 ; $remain = $2 ; if (!-d $path) { print " Creating directory '$path'\n" ; if (!mkdir $path) { Lltag::Misc::print_error (" ", "Failed to create directory ($!).") ; return ; } } } print " Renaming.\n" ; rename $file, $new_name or Lltag::Misc::print_error (" ", "Failed to rename ($!).") ; } 1 ; lltag-lltag-0.14.6/lib/Lltag/Tags.pm000066400000000000000000000245531314167102300170620ustar00rootroot00000000000000package Lltag::Tags ; use strict ; no strict "refs" ; ####################################################### # init my $edit_values_usage_forced ; sub init_tagging { my $self = shift ; # need to show menu usage once ? $edit_values_usage_forced = $self->{menu_usage_once_opt} ; } ####################################################### # display tag values sub display_one_tag_value { my $self = shift ; my $values = shift ; my $field = shift ; my $prefix = shift ; if ($field =~ / -> _/) { print $prefix.ucfirst($field).": \n" } elsif (ref($values->{$field}) ne 'ARRAY') { print $prefix.ucfirst($field).": " . ($values->{$field} eq "" ? "" : $values->{$field}) ."\n" } else { my @vals = @{$values->{$field}} ; for(my $i = 0; $i < @vals; $i++) { print $prefix.ucfirst($field)." #".($i+1).": ".$vals[$i]."\n" } } } sub display_tag_values { my $self = shift ; my $values = shift ; my $prefix = shift ; # display regular tags first foreach my $field (@{$self->{field_names}}) { next unless defined $values->{$field} ; display_one_tag_value $self, $values, $field, $prefix ; } # display misc tags later foreach my $field (keys %{$values}) { next if grep { $field eq $_ } @{$self->{field_names}} ; display_one_tag_value $self, $values, $field, $prefix ; } } ####################################################### # various tag management routines # clone tag values (to be able to modify without changing the original) sub clone_tag_values { my $old_values = shift ; return undef unless defined $old_values ; # clone the hash my %new_values = %{$old_values} ; for my $field (keys %new_values) { if (ref($new_values{$field}) eq 'ARRAY') { # clone the array pointed by the ref in the hash @{$new_values{$field}} = @{$new_values{$field}} ; } } return \%new_values ; } # add a value to a field, creating an array if required sub append_tag_value { my $self = shift ; my $values = shift ; my $field = shift ; my $value = shift ; if (not defined $values->{$field}) { $values->{$field} = $value ; } elsif (ref($values->{$field}) ne 'ARRAY') { # create an array (except if we already have this value) my $tmp = $values->{$field} ; if ($tmp ne $value) { # need to delete the hash ref before changing its type delete $values->{$field} ; @{$values->{$field}} = ($tmp, $value) ; } } else { # append to the array (except if we already have this value) push @{$values->{$field}}, $value unless grep { $value eq $_ } @{$values->{$field}} ; } } # add a value or an array of values to a tag sub append_tag_multiple_value { my $self = shift ; my $values = shift ; my $field = shift ; my $multiple_value = shift ; if (ref($multiple_value) ne 'ARRAY') { append_tag_value $self, $values, $field, $multiple_value ; } else { map { append_tag_value $self, $values, $field, $_ ; } @{$multiple_value} ; } } # append a hash of values (either unique or arrays) into another hash sub append_tag_values { my $self = shift ; my $old_values = shift ; my $new_values = shift ; foreach my $field (keys %{$new_values}) { append_tag_multiple_value $self, $old_values, $field, $new_values->{$field} ; } } # add a set of new values into an old hash, depending of clear/append options sub merge_new_tag_values { my $self = shift ; my $old_values = shift ; my $new_values = shift ; if ($self->{clear_opt}) { $old_values = {} ; } foreach my $field (keys %{$new_values}) { delete $old_values->{$field} if defined $old_values->{$field} and !$self->{append_opt} ; append_tag_multiple_value $self, $old_values, $field, $new_values->{$field} ; } return $old_values ; } # return values for a field as an array sub get_tag_value_array { my $self = shift ; my $values = shift ; my $field = shift ; if (not defined $values->{$field}) { return () ; } elsif (ref ($values->{$field}) ne 'ARRAY') { return ($values->{$field}) ; } else { return @{$values->{$field}} ; } } # return a unique value for a field sub get_tag_unique_value { my $self = shift ; my $values = shift ; my $field = shift ; my @array = get_tag_value_array $self, $values, $field ; die "Trying to return a unique tag value on an empty array.\n" if ! @array ; return $array[0] ; } # return non-regular keys whose value is defined sub get_values_non_regular_keys { my $self = shift ; my $values = shift ; return grep { my $key = $_ ; !(grep { $_ eq $key } @{$self->{field_names}}) } (keys %{$values}) ; } # handle explicit tag values sub process_explicit_tag_value { my $self = shift ; my $string = shift ; if ($string =~ m/^([^=]+)=(.*)$/) { append_tag_value $self, $self->{explicit_values}, $1, $2 ; } else { die "Explicit tags must be given as 'TAG=value'.\n" ; } } ####################################################### # extract tags from the stream # helper to be used by backends who get the tags as the stream output of another program sub convert_tag_stream_to_values { my $self = shift ; my $values = {} ; while (my $line = shift @_) { chomp $line ; my ($field, $value) = ($line =~ m/^(.*)=(.*)$/) ; next if !$value ; # remove the track total from the track number to avoid renaming problems with slashes or so if ($field eq "NUMBER") { if ($value =~ /^(\d+)/) { $value = $1 ; } else { return ; } } Lltag::Tags::append_tag_value ($self, $values, $field, $value) ; } return $values ; } ####################################################### # get tagging command line, display it if required, execute it if required # output the errors, ... # helper to be used by backends who set the tags with another program sub set_tags_with_external_prog { my $self = shift ; # show command line and really tag if asked if ($self->{dry_run_opt} or $self->{verbose_opt}) { print " '". +(join "' '", @_) ."'\n" ; } if (!$self->{dry_run_opt}) { print " Tagging.\n" ; my ($status, @output) = Lltag::Misc::system_with_output (@_) ; if ($status) { print " Tagging failed, command line was: '". join ("' '", @_) ."'.\n" ; while (my $line = shift @output) { print "# $line" ; } } } } ####################################################### # edit current tags use constant EDIT_SUCCESS => 0 ; use constant EDIT_CANCEL => -1; sub edit_values_usage { my $self = shift ; my $values = shift ; Lltag::Misc::print_usage_header (" ", "Editing") ; # print all fields, including the undefined ones foreach my $field (@{$self->{field_names}}) { print " ".$self->{field_name_letter}{$field} ." => Edit ".ucfirst($field).$self->{field_name_trailing_spaces}{$field} ."\n" ; } print " tag FOO => Edit tag FOO\n" ; print " V => View current fields\n" ; print " y/E => End edition\n" ; print " q/C => Cancel edition\n" ; print " During edition, enter to drop a value.\n" ; $edit_values_usage_forced = 0 ; } sub edit_one_value { my $self = shift ; my $values = shift ; my $field = shift ; if (ref($values->{$field}) eq 'ARRAY') { my @oldvals = @{$values->{$field}} ; my @newvals = () ; for(my $i=0; $i<@oldvals; $i++) { my $value = Lltag::Misc::readline (" ", ucfirst($field)." field #".($i+1), $oldvals[$i], 1) ; if (defined $value) { push @newvals, $value unless $value eq "" ; } else { # if ctrl-d, reset to same value, without removing it if empty push @newvals, $oldvals[$i] ; } } delete $values->{$field} ; if (@newvals == 1) { $values->{$field} = $newvals[0] ; } elsif (@newvals) { @{$values->{$field}} = @newvals ; } else { $values->{$field} = "" ; } } else { my $value = Lltag::Misc::readline (" ", ucfirst($field)." field", $values->{$field}, 1) ; # if ctrl-d, change nothing if (defined $value) { if ($value eq "DELETE" or $value eq "") { delete $values->{$field} ; } else { $values->{$field} = $value ; } } } } # edit values in place sub edit_values { my $self = shift ; my $values = shift ; edit_values_usage $self, $values if $edit_values_usage_forced ; print " Current tag values are:\n" ; display_tag_values $self, $values, " " ; while (1) { my $edit_reply = Lltag::Misc::readline (" ", "Edit a field [". $self->{field_letters_string} ."Vyq] (no default, h for help)", "", -1) ; # if ctrl-d, cancel editing $edit_reply = 'q' unless defined $edit_reply ; if ($edit_reply =~ m/^tag (.+)/) { edit_one_value $self, $values, $1 ; } elsif ($edit_reply =~ m/^($self->{field_letters_union})/) { edit_one_value $self, $values, $self->{field_letter_name}{$1} ; } elsif ($edit_reply =~ m/^y/ or $edit_reply =~ m/^E/) { return EDIT_SUCCESS ; } elsif ($edit_reply =~ m/^q/ or $edit_reply =~ m/^C/) { return EDIT_CANCEL ; } elsif ($edit_reply =~ m/^V/) { print " Current tag values are:\n" ; display_tag_values $self, $values, " " ; } else { edit_values_usage $self, $values ; } } } ####################################################### # apply user-given regexp sub apply_regexp_to_tag { my $val = shift ; my $regexp = shift ; my $tag = shift ; # parse the regexp if ($regexp =~ m@(?:([^:]+):)?s/([^/]+)/([^/]*)/$@) { my @tags = () ; @tags = split (/,/, $1) if $1; my $from = $2 ; my $to = $3 ; $val =~ s/$from/$to/g if !@tags or grep { $tag eq $_ } @tags ; } else { die "Unrecognized user regexp '$regexp'.\n" ; } return $val ; } ####################################################### # clone tag values (to be able to modify without changing the original) and merge tags case-insensitively sub clone_tag_values_uc { my $self = shift ; my $old_values = shift ; return undef unless defined $old_values ; # clone the hash my %new_values; # use upcase values first for my $field (keys %{$old_values}) { if ($field eq uc($field)) { append_tag_multiple_value $self, \%new_values, uc($field), $old_values->{$field} ; } } # other values then for my $field (keys %{$old_values}) { if ($field ne uc($field)) { append_tag_multiple_value $self, \%new_values, uc($field), $old_values->{$field} ; } } return \%new_values ; } 1 ; lltag-lltag-0.14.6/lib/MANIFEST000066400000000000000000000002251314167102300157020ustar00rootroot00000000000000MANIFEST Makefile.PL Lltag/Tags.pm Lltag/Misc.pm Lltag/MP3.pm Lltag/MP3v2.pm Lltag/OGG.pm Lltag/FLAC.pm Lltag/CDDB.pm Lltag/Parse.pm Lltag/Rename.pm lltag-lltag-0.14.6/lib/Makefile.PL.in000066400000000000000000000001301314167102300171230ustar00rootroot00000000000000use ExtUtils::MakeMaker; WriteMakefile ( NAME => 'Lltag', VERSION => "@VERSION@", ); lltag-lltag-0.14.6/lltag.1000066400000000000000000000531201314167102300151720ustar00rootroot00000000000000.\" Process this file with .\" groff -man -Tascii foo.1 .\" .TH LLTAG 1 "NOVEMBER 2006" .SH NAME lltag - tag and rename mp3/ogg/flac music files automagically .SH SYNOPSIS .B lltag .RB [ -C ] .RB [ -E ] .RB [ "-F " ] .RB [ -G ] .RB [ -p ] .RB [ "-a " ] .RB [ "-t " ] .RB [ "-A <album>" ] .RB [ "-n <number>" ] .RB [ "-g <genre>" ] .RB [ "-d <date>" ] .RB [ "-c <comment>" ] .RB [ "--tag <TAG=value>" ] .RB [ --spaces ] .RB [ --maj ] .RB [ "--sep\ <s1|s2|...>" ] .RB [ "--regexp <regexp>" ] .RB [ --mp3/--ogg/--flac ] .RB [ "--type <type>" ] .RB [ --clear ] .RB [ --append ] .RB [ --no-tagging ] .RB [ --preserve-time ] .RB [ "--rename <format>" ] .RB [ --rename-ext ] .RB [ --rename-min ] .RB [ "--rename-sep <sep>" ] .RB [ "--rename-slash <char>" ] .RB [ "--rename-regexp <regexp>" ] .RB [ --dry-run ] .RB [ --yes ] .RB [ --ask ] .RB [ "--cddb-query <query>" ] .RB [ "--cddb-server <server[:port]>" ] .RB [ -R ] .RB [ -v ] .RB [ -q ] .RB [ "--config <file>" ] .RB [ "--gencfg <file>" ] .RB [ -S ] .RB [ "--show-tags <tags>" ] .RB [ -L ] .RB [ -V ] .RB [ -h ] .RB files... .\" .SH DESCRIPTION .B lltag is a command-line tool to automagically set tags of MP3, OGG or FLAC files. There are several ways to obtains the tags that will be set: .TP .B Parsing the filename .B lltag may either parse the filename using its own internal database of commonly-used formats (default behavior, or when .B -G is passed), or some user-provided formats (when .B -F is passed). .TP .B Requesting from CDDB .B lltag may access an online CDDB database to extract tags from a track of a CD (when .B -C is passed). .TP .B Explicitly setting values .B lltag provides a set of command-line option to manually set various tags. .TP .B Manually editing values .B lltag provides an interactive interface to edit existing values or any value provided by the above strategies. .P Each time, a new audio file is processed, .B lltag starts by trying to obtain new tags depending on the behavior options given by the user. First, if a preferred parser has been selected before, it is used to try to parse the new filename. Then, if editing is enabled .RB ( -E ), the user will be able to modify existing tag values. Then, if CDDB is enabled .RB ( -C ), the user will be asked to request tags from the online CDDB database. Then, if the user provided any parsing format .RB ( -F ), or if guessing is enabled .RB ( -G ), .B lltag will attempt to parse the filename. Note that if no behavior is chosen at all on command-line, including no renaming option, then parsing with the internal format database will be used by default (as if .B -G had been passed). As soon as one of the above strategies succeeds, .B lltag jumps to the main menu where the user may either accept new tags or select another behavior (see .B MAIN MENU in .B INTERACTIVE MENUS below for details). If .B --yes has been passed, or if automatic mode has been previously enabled in the menu, it will proceed with tagging (and renaming if requested) and go on with the next file. The new tags that the selected strategy returns will be appended with the explicit values given with .BR " -a ", " -t ", " -A ", " -g ", " -n ", " -d ", " -c " or " --tag . They will then either replace (default), clear and replace .RB ( --clear ) or append to .RB ( --append ) the existing tags in the target file. Once the tags are known, a backend program or library is used to apply them to the audio file (unless .B --no-tagging is passed). .RB "The " MP3::Tag " Perl module or " mp3info is used to tag .B MP3 files while .B vorbiscomment is used for .B OGG files, and .B metaflac is used for .B FLAC files. In the end, when called with .BR --rename , the target file will also be renamed according to a user-provided format filled with the tag values. .SH OPTIONS .TP .BI "-A, --ALBUM" " <album>" Add a value for the \fIALBUM\fR tag. .TP .BI "-a, --ARTIST" " <artist>" Add a value for the \fIARTIST\fR tag. .TP .BI "--append" Force appending of ogg/flac tags (instead of replacing existing tags). The corresponding configuration file option is .IR append_tags . Since mp3 files may only get one tag of each type, appending does nothing, the first occurrence only is stored. .TP .B --ask Always ask confirmation to the user before using a user-specified parser. By default, all actions require confirmation, except when a matching user-specified format is found. The corresponding configuration file option is .IR ask . See .B PARSING MENU in .B INTERACTIVE MENUS below for details. .TP .B "-C, --cddb" Try to find tags in the CDDB online database before trying to parse filenames. The queries are sent using the HTTP interface, which means a HTTP proxy might be used when required. The corresponding configuration file option is .IR cddb . .TP .BI "--cddb-query" " <keywords>" .TP .BI "--cddb-query" " <cat>/<id>" Automatically search for CD matching <keywords> or matching category <cat> and id <id> as if the user passed .B --cddb and entered the query interactively in the module. .TP .BI "--cddb-server" " <server[:port]>" Change the CDDB server, and eventually its port. The default is .BR www.freedb.org:80 . The corresponding configuration file options are .IR cddb_server_name " and " cddb_server_port . If a HTTP proxy is required to access the internet, the environment variable .B HTTP_PROXY may be used (set to something like "http://my.proxy.com"). .TP .BI "-c, --COMMENT" " <comment>" Add a value for the \fICOMMENT\fR tag. .TP .B --clear Force clearing of all tags before tagging (instead of replacing existing tags). The corresponding configuration file option is .IR clear_tags . .TP .BI --config " <file>" Parse additional configuration file. See .B CONFIGURATION FILES below for details. .TP .BI "-d, --DATE" " <date>" Add a value for the \fIDATE\fR tag. Note that the ID3 date tag may only store 4 characters (for a year). .TP .B --dry-run Do not really tag files, just show what would have been done. The corresponding configuration file option is .IR dry_run . .TP .B -E, --edit Edit tags immediately. .TP .BI "-F, --format" " <format>" Add the specified format string to the list of user-supplied formats. The corresponding configuration file option is .IR format . Might be used several times to try different formats. See .B FORMAT below for details. .TP .B --flac Tag all files as FLAC files, using the FLAC backend (based on \fBmetaflac\fR). The corresponding configuration file option is .IR type . .TP .B "-G, --guess" Guess format using the internal database if no user-specified format worked (default behavior). The corresponding configuration file option is .IR guess . .TP .BI "-g, --GENRE" " <genre>" Add a value for the \fIGENRE\fR tag. While some file types accept any string as a genre, some others (especially ID3v1 tags in MP3 files) require the string to match within a list of specified genres. .TP .BI --gencfg " <file>" Generate configuration file. See .B CONFIGURATION FILES below for details. .TP .B "-h, --help" Print a usage message and exit. .TP .B "-L, --list" List internal formats. .TP .B --maj Upcase the first letter of each word in tags. The corresponding configuration file option is .IR maj . .TP .B --mp3 Tag all files as MP3 files, using the MP3 backend (based on either \fBmp3info\fR or \fBMP3::Tag\fR). The corresponding configuration file option is .IR type . .TP .B --mp3v2, --id3v2 Enable the experimental MP3 ID3v2-aware backend (based on \fRMP3::Tag\fR) instead of the old ID3v1-only backend. .TP .B --mp3read=[1][2] Configure how the MP3v2 backend reads and merges ID3v1 and v2 tags. By default, v1 are appended to v2 (\fB21\fR). If set to \fB1\fR, only v1 are read. If set to \fB2\fR, only v2 are read. If set to \fB12\fR, v2 are appended to v1. Note that merging/appending takes care of removing duplicates. .TP .BI "-n, --NUMBER" " <number>" Add a value for the \fINUMBER\fR tag. .TP .B --no-tagging Do not actually tag files. This might be used to rename files without tagging. The corresponding configuration file option is .IR no_tagging . .TP .B -T, --preserve-time Preserve file modification time during tagging. The corresponding configuration file option is .IR preserve_time . .TP .B --ogg Tag all files as OGG files, using the OGG backend (based on \fBvorbiscomment\fR). The corresponding configuration file option is .IR type . .TP .B "-p, --no-path" Do not consider the path of files when matching. The corresponding configuration file option is .IR no_path . .TP .B "-q, --quiet" Decrease message verbosity. The corresponding configuration file option is .I verbose which indicates the verbose level. See .BR -v for details about the existing verbosity levels. .TP .B "-R, --recursive" Recursively search for files in subdirectories that are given on the command line. The corresponding configuration file option is .IR recursive . .TP .BI --regexp " <[tag,tag:]s/from/to/>" Replace \fIfrom\fR with \fIto\fR in tags before tagging. The corresponding configuration file option is .IR regexp . If several tags (comma-separated) prefix the regexp, replacement is only applied to the corresponding fields. This option might be used multiple times to specify multiple replacing. .TP .BI --rename " <format>" After tagging, rename the file according to the format. The corresponding configuration file option is .IR rename_format . The format is filled using the first occurrence of each tag that was used to tag the file right before. It means that an old existing tag may be used if no new one replaced it and .B --clear was not passed. By default, confirmation is asked before tagging. See .B RENAMING MENU in .B INTERACTIVE MENUS below for details. .TP .B --rename-ext Assume that the file extension is provided by the rename format instead of automatically adding the extension corresponding to the file type. The corresponding configuration file option is .IR rename_ext . .TP .B --rename-min Lowcase all tags before renaming. The corresponding configuration file option is .IR rename_min . .TP .BI --rename-regexp " <[tag,tag:]s/from/to/>" Replace \fIfrom\fR with \fIto\fR in tags before renaming. If several tags (comma-separated) prefix the regexp, replacement is only applied to the corresponding fields. This option might be used multiple times to specify multiple replacing. The corresponding configuration file option is .IR rename_regexp . .TP .BI --rename-sep " <sep>" Replace spaces with sep when renaming. The corresponding configuration file option is .IR rename_sep . See .B --rename-regexp for a more general replace feature. .TP .BI --rename-slash " <char>" Replace slashes with char when renaming. The corresponding configuration file option is .IR rename_slash . See .B --rename-regexp for a more general replace feature. .TP .B -S Instead of tagging, lltag shows the tags that are currently set in files. See .B --show-tags to show only some tags. .TP .BI --sep " <string|string>" Replace the specified characters or strings with space in tags. The corresponding configuration file option is .IR sep . They have to be |-separated. See .B --regexp for a more general replace feature. .TP .BI --show-tags " <tag1,tag2,...>" Instead of tagging, lltag shows tags that are currently set in files. The argument is a comma separated list of tag types .RI ( artist ", " title ", " album ", " number ", " .IR genre ", " date ", " comment " or " all ). See also .B -S to show all tags. .TP .B --spaces Allow multiple or no space instead of only one when matching. Also allow spaces limiting path elements. The corresponding configuration file option is .IR spaces . See also .B INTERNAL FORMATS to get the detailled impact of this option. .TP .BI "-t, --TITLE" " <title>" Add a value for the \fITITLE\fR tag. .TP .BI "--tag" " <TAG=value>" Add an explicit tag value. The corresponding configuration file option is .IR tag . Might be used several times, even for the same tag. When setting a common tag, it is similar to using .BR -a ", " -A ", " -t ", " -n ", " -g ", " -d " or " -c . Note that mp3 tags do not support whatever .IR TAG . .TP .BI "--type" " <type>" Tag all files as .B <type> files. The corresponding configuration file option is .IR type . .TP .B "-v, --verbose" Increase message verbosity. The corresponding configuration file option is .I verbose which indicates the verbose level. The default verbosity level is 0 to show only important messages. Other possible values are 1 to show usage information when a menu is displayed for the first time, and 2 to always show usage information before a menu appears. See also .BR -q . .TP .B "-V, --version" Show the version. .TP .B --yes Always accept tagging without asking the user. The corresponding configuration file option is .IR yes . By default user-specified format matching is accepted while guess format matching is asked for confirmation. Also always accept renaming without asking the user. .SH INTERACTIVE MENUS When not running with .BI --yes , the user has to tells lltag what to do. Files are processed one after the other, with the following steps: .TP .B * If the .B preferred parser exists, try to apply it. .TP .B * If failed, if .B --cddb was passed, trying a CDDB query. .TP .B * If failed, try the user-provided formats, if any. .TP .B * If failed, if no user-format were passed, or if .B -G was passed, try the internal formats. .TP .B * Then we have a list of tags to apply, we may apply them, edit them, or go back to a CDDB query or trying to parse the filename again. .TP .B * Then, if .B --rename was passed, the file is renamed. When hitting .B Ctrl-d at the beginning of an empty line .RB ( EOF ), the general behavior is to cancel the current operation without leaving. We now describe all interactive menus in detail. .SS MAIN MENU Once some tags have been obtained by either CDDB, parsing or the explicit values given on the command line, the main menu opens to either change the tags or apply them: .TP .B y Yes, use these tags (default) .TP .B a Always yes, stop asking for a confirmation .TP .B P Try to parse the file, see .B PARSING MENU .TP .B C Query CDDB, see .B CDDB MENUS .TP .B E Edit values, see .B EDITING MENU .TP .B D Only use explicit values, forget about CDDB or parsed tags .TP .B Z Reset to no tag values at all .TP .B R Revert to existing tag values from the current file .TP .B O Display existing tag values in the current file .TP .B n Do not tag this file, jump to renaming (or to the next file if renaming is disabled) .TP .BR q " (or " EOF ) Skip this file .TP .B Q Quit without tagging anything anymore .SS CDDB MENUS When the CDDB opens for the first time, the user must enter a query to choose a CD in the online database. .TP .B <space-separated keywords> CDDB query for CD matching the keywords. Search in all CD categories within fields artist OR album. .\" freedb.org specific manual, not used anymore .\"Search in all CD categories within fields artist and title by default. .\"If .\".B cats=foo+bar .\"is added, search in CD categories foo and bar only. .\"If .\".B fields=all .\"is added, search keywords in all fields. .\"If .\".B fields=foo+bar .\"is added, search keywords in fields foo and bar. .\".TP .\".B <category>/<hexadecinal id> .\"CDDB query for CD matching category and id .TP .BR q " (or " EOF ) Quit CDDB query, see .B MAIN MENU .P Once keywords have been passed as a query to CDDB, a list of matching CD titles will be displayed. The user then needs to choose one: .TP .B <index> Choose a CD in the current keywords query results list .TP .B V View the list of CD matching the keywords .TP .B k Start again CDDB query with different keywords .TP .BR q " (or " EOF ) Quit CDDB query, see .B MAIN MENU .P Once a CD have been chosen, the user needs to choose a track .TP .B <index> Choose a track of the current CD .TP .B <index> a Choose a track and do not ask for confirmation anymore .TP .B a Use default track and do not ask for confirmation anymore .TP .B E Edit current CD common tags, see .B EDITING MENU .TP .B v View the list of CD matching the keywords .TP .B c Change the CD chosen in keywords query results list .TP .B k Start again CDDB query with different keywords .TP .BR q " (or " EOF ) Quit CDDB query, see .B MAIN MENU .P Note that entering the CDDB menus again will go back to the previous CD instead of asking the user to query again, so that an entire CD may be tagged easily. .SS PARSING MENU When .B --ask is passed or when guessing, each matching will lead to a confirmation message before tagging. Available behaviors are: .TP .B y Tag current file with current format. This is the default. .TP .B u Tag current file with current format. Then use current format for all remaining matching files. When a non-matching file is reached, stop using this preferred format. .TP .B a Tag current file with current format. Then, never asking for a confirmation anymore. .TP .B n Don't tag current file with this format. Try the next matching format on the current file. .TP .B p When matching is done through combination of a path parser and a basename parser, keep the basename parser and try the next path parser on the current file. .TP .BR q " (or " EOF ) Stop trying to parse this file. .SS EDITING MENU It is possible to edit tags, either before tagging or file, or before choosing a track in a CD obtained by CDDB. The current value of all regular fields is shown and may be modified by entering another value, deleted by entering .BR <DELETE> , or cleared. The behavior depends on the installed readline library. If it is smart, the current value may be edited inline and an history is available. If not, pressing .I <enter> will keep the current value while .I CLEAR will empty it. .I EOF while cancel the editing of this single value. Each field may be selected for edition by pressing its corresponding letter in the format (see \fBFORMAT\fR). Since there might be some non-standard tag names, it is also possible to enter \fItag FOO\fR to modify tag \fIFOO\fR. Editing ends by tagging (if \fIE\fR is pressed) or canceling and return to confirmation menu (if \fIC\fR is pressed). The other options are: .TP .B V View the current values of tags .TP .B y End edition, save changes, and return to previous menu .TP .BR q " (or " EOF ) Cancel edition, forget about changes, and return to previous menu .SS RENAMING MENU By default, before renaming, a confirmation is asked to the user. You may bypass it by passing .B --yes on the command line. If the rename format uses a field that is not defined, a warning is shown and confirmation is always asked. Available behaviors when renaming are: .TP .B y Rename current file with current new filename. This is the default. .TP .B a Rename current file with current new filename. Then, never asking for a renaming confirmation anymore. .TP .B e Edit current new filename before renaming. The behavior depends on the installed readline library. If it is smart, the current value may be edited inline and an history is available. .TP .BR q " (or " EOF ) Don't rename current file. .TP .B h Show help about confirmation. .SH FORMAT User-specified formats must be a string composed of any characters and the following special codes: .RS .I "%a" to match the author. .I "%A" to match the album. .I "%g" to match the genre. .I "%n" to match the track number. .I "%t" to match the title. .I "%d" to match the date. .I "%c" to match the comment. .I "%i" to match anything and ignore it. .I "%%" to match %. .RE Additionally, while renaming, the following codes are available: .RS .I "%F" is replaced by the original basename of the file. .I "%E" is replaced by the original extension of the file. .I "%P" is replaced by the original path of the file. .SH INTERNAL FORMATS The internal format database is usually stored in .IR /etc/lltag/formats . The user may override this file by defining a .IR $HOME/.lltag/formats . If this file exists, the system-wide one is ignored. See the manpage of .I lltag_formats or .I /etc/lltag/formats for details. .SH CONFIGURATION FILES lltag reads some configuration files before parsing command line options. The system-wide configuration file is defined in .I /etc/lltag/config if it exists. It also reads .I $HOME/.lltag/config if it exists. The user may also add another configurable file with .BR --config . lltag may also generate a configuration with .BR --gencfg . See the manpage of .I lltag_config or the example of .I config file provided in the documentation for details. .SH FILES .RE .I /etc/lltag/formats .RS System-wide internal format database. See .B INTERNAL FORMATS for details. .RE .I $HOME/.lltag/formats .RS User internal format database. If it exists, the system-wide one is ignored. .RE .I $HOME/.lltag/edit_history .RS History of last entered values in the edition mode if the .B Readline library supports this feature. .RE .I /etc/lltag/config .RS System-wide configuration file, if it exists. See .B CONFIGURATION FILES for details. .RE .I $HOME/.lltag/config .RS User configuration file. .SH EXAMPLES .RE Show all tags for each OGG files in the current directory: .RS lltag \-S *.ogg .RE Show only a selected list of tags for all files in all subdirectories: .RS lltag \-\-show-tags artist,album,title,number \-R . .RE Set an arbitrary tag in a file (only works with OGG vorbis or FLAC files): .RS lltag \-\-tag foo=nil foo.ogg .RE Delete the foo tag from a file: .RS lltag \-\-tag foo= bar.ogg .RE Set the ALBUM, ARTIST and GENRE tag values of the MP3 files in the current directory: .RS lltag \-\-ARTIST "Queen" \-\-ALBUM "Innunendo" \-\-GENRE "rock" \-\-COMMENT="very cool" *.mp3 .RE Rename a file by assembling its current NUMBER, ARTIST and TITLE tag values: .RS lltag \-\-no\-tagging \-\-rename "%n - %a - %t" foobar.ogg .RE Clear all tags in all FLAC files: .RS lltag \-\-clear *.flac .SH SEE ALSO .PP .BR lltag_config "(5), " lltag_formats (5) The .I howto.html file provided within the documentation. .SH AUTHOR Brice Goglin ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lltag-lltag-0.14.6/lltag.in�������������������������������������������������������������������������0000775�0000000�0000000�00000076510�13141671023�0015453�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/perl -w use strict ; no strict "refs" ; # for $backend{$file_type} require Lltag::Tags ; require Lltag::Misc ; require Lltag::MP3 ; require Lltag::MP3v2 ; require Lltag::OGG ; require Lltag::FLAC ; require Lltag::CDDB ; require Lltag::Parse ; require Lltag::Rename ; use Getopt::Long ; Getopt::Long::Configure('noignorecase', 'noautoabbrev', 'bundling') ; ####################################################### # main hash for globals and config my $self = {} ; ####################################################### # UTF-8 locale ? use I18N::Langinfo qw(langinfo CODESET) ; $self->{utf8} = ( langinfo (CODESET ()) eq "UTF-8" ) ; ####################################################### # configuration file location $self->{common_lltag_dir} = "@SYSCONFDIR@/lltag" ; $self->{user_lltag_dir} = "$ENV{HOME}/.lltag" ; $self->{lltag_format_filename} = "formats" ; $self->{lltag_config_filename} = "config" ; $self->{lltag_edit_history_filename} = "edit_history" ; ####################################################### Lltag::Misc::init_readline ($self) ; ####################################################### # format parameters @{$self->{field_names}} = ('ARTIST', 'TITLE', 'ALBUM', 'NUMBER', 'GENRE', 'DATE', 'COMMENT') ; @{$self->{field_letters}} = ('a', 't', 'A', 'n', 'g', 'd', 'c') ; %{$self->{field_name_letter}} = ( 'ARTIST' => 'a', 'TITLE' => 't', 'ALBUM' => 'A', 'NUMBER' => 'n', 'GENRE' => 'g', 'DATE' => 'd', 'COMMENT' => 'c', ) ; %{$self->{field_name_trailing_spaces}} = ( 'ARTIST' => ' ', 'TITLE' => ' ', 'ALBUM' => ' ', 'NUMBER' => ' ', 'GENRE' => ' ', 'DATE' => ' ', 'COMMENT' => ' ', ) ; %{$self->{field_letter_name}} = ( 'a' => 'ARTIST', 't' => 'TITLE', 'A' => 'ALBUM', 'n' => 'NUMBER', 'g' => 'GENRE', 'd' => 'DATE', 'c' => 'COMMENT', ) ; # change format letters into a parsing string $self->{field_letters_union} = (join '|', @{$self->{field_letters}}) ; # cache the list of letters for later $self->{field_letters_string} = (join '', @{$self->{field_letters}}) ; ####################################################### # version my $version = "@VERSION@" ; sub version { print "This is lltag version $version.\n" ; exit 0 ; } ####################################################### # usage sub usage { print $0." $version is a frontend to tag and rename MP3/OGG/FLAC files automagically.\n" ; print "Usage: ".$0." [options] files...\n" ; print " Tagging options:\n" ; print " -F, --format <format> Try format (multiple instances allowed)\n" ; print " -G, --guess Guess format (default)\n" ; print " -C, --cddb Query tags in CDDB\n" ; print " -E, --edit Edit tags\n" ; print map { " -". $self->{field_name_letter}{$_} .", --". $_.$self->{field_name_trailing_spaces}{$_} ."<val> Add explicit value <val> for ". ucfirst($_) ."\n" } @{$self->{field_names}} ; print " --tag <TAG=val> Add explicit value <val> to tag <TAG>\n" ; print " -p, --no-path Remove the path from filenames when matching\n" ; print " --spaces Allow no or multiple spaces\n" ; print " --maj Upcase first letters of words in tags\n" ; print " --sep <s1|s2|...> Replace |-separated strings with space in tags\n" ; print " --regexp <regexp> Apply a replace regexp to tags before tagging\n" ; print " --mp3/--ogg/--flac Force mp3, ogg or flac instead of by-extension detection\n" ; print " --type <type> Force <type> instead of by-extension detection\n" ; print " --mp3v2, --id3v2 Enable experimental ID3v2 backend for MP3 files\n" ; print " --mp3read=[12] Read ID3v1 and v2 MP3 tags (default is 21)\n" ; print " --clear Clear all tags of audio files when possible\n" ; print " --append Append tags only instead of replacing old ones when possible\n" ; print " --no-tagging Do not actually tag files\n" ; print " -T, --preserve-time Preserve file modification during tagging\n" ; Lltag::Rename::rename_usage ($self) ; print " General options:\n" ; print " --dry-run Do nothing but show what would have been done\n" ; print " --yes Tag without asking for confirmation when guessing\n" ; print " Rename without asking for confirmation\n" ; print " --ask Always ask for confirmation before tagging\n" ; print " -R, --recursive Recursively search all files in subdirectories\n" ; print " -v, --verbose More verbose messages\n" ; print " -q, --quiet Less verbose messages\n" ; print " --config <file> Read additional configuration file\n" ; print " --gencfg <file> Generate additional configuration file\n" ; print " CDDB options:\n" ; print " --cddb-query <query> Start with CDDB query <query> by keywords or id\n" ; print " --cddb-server <server> Change the CDDB server and port\n" ; print " Behavior options:\n" ; print " -S Show all tags from files\n" ; print " --show-tags <tag,..> Show several tags from files\n" ; print " -L, --list List internal formats\n" ; print " -V, --version Show lltag version\n" ; print " -h, --help Show this help\n" ; print " Format is composed of anything you want with special fields:\n" ; print map { " %". $self->{field_name_letter}{$_} ." means ". ucfirst($_) ."\n" } @{$self->{field_names}} ; Lltag::Parse::parsing_format_usage ($self) ; Lltag::Rename::rename_format_usage ($self) ; print "\n" ; print "Author: Brice Goglin\n" ; print "Homepage: http://bgoglin.free.fr/lltag\n" ; print "Report bugs to: <lltag AT googlegroups.com>\n" ; exit 1; } ####################################################### # options my $verbose_level = 0 ; # do not display menu usage information $self->{dry_run_opt} = 0 ; $self->{recursive_opt} = 0 ; $self->{list_formats_opt} = 0 ; $self->{show_tags_opt} = "" ; $self->{no_tagging_opt} = 0 ; $self->{preserve_time} = 0 ; %{$self->{explicit_values}} = () ; $self->{ask_opt} = 0 ; $self->{guess_opt} = 0 ; $self->{cddb_opt} = 0 ; $self->{edit_opt} = 0 ; $self->{no_path_opt} = 0 ; $self->{maj_opt} = 0 ; $self->{sep_opt} = undef ; $self->{spaces_opt} = 0 ; $self->{type_opt} = undef ; $self->{mp3v2_opt} = 0 ; $self->{mp3v2_read_opt} = Lltag::MP3v2->MP3V2_READ_V2_V1 ; $self->{yes_opt} = 0 ; $self->{clear_opt} = 0 ; $self->{append_opt} = 0 ; @{$self->{regexp_opts}} = () ; $self->{rename_opt} = undef ; $self->{rename_min_opt} = 0 ; $self->{rename_sep_opt} = " " ; $self->{rename_ext_opt} = 0 ; @{$self->{rename_regexp_opts}} = () ; $self->{rename_slash_opt} = "-" ; $self->{gencfg_file} = undef ; @{$self->{user_format_strings}} = () ; $self->{cddb_server_name} = "tracktype.org" ; $self->{cddb_server_port} = 80 ; # command line given cddb query, undefined afterwards $self->{requested_cddb_query} = undef ; ####################################################### # parse config files first my @additional_config_files = () ; # process these options but kept other options in @ARGV for later Getopt::Long::Configure('passthrough') ; GetOptions( 'config=s' => \@additional_config_files, ) ; # restore default behavior: process all options and warn on error Getopt::Long::Configure('nopassthrough') ; sub process_option { $_ = shift ; chomp $_ ; if (/^format\s*=\s*"(.+)"$/) { push (@{$self->{user_format_strings}}, $1) ; } elsif (/^guess\s*=\s*(.+)$/) { $self->{guess_opt} = $1 ; } elsif (/^no_path\s*=\s*(.+)$/) { $self->{no_path_opt} = $1 ; } elsif (/^tag\s*=\s*"(.+)"$/) { Lltag::Tags::process_explicit_tag_value ($self, $1) ; } elsif (/^spaces\s*=\s*(.+)$/) { $self->{spaces_opt} = $1 ; } elsif (/^maj\s*=\s*(.+)$/) { $self->{maj_opt} = $1 ; } elsif (/^sep\s*=\s*"(.*)"$/) { $self->{sep_opt} = $1 ; } elsif (/^type\s*=\s*(.+)$/) { if ($1 eq "none") { $self->{type_opt} = undef ; } else { $self->{type_opt} = $1 ; } } elsif (/^regexp\s*=\s*"(.+)"$/) { push (@{$self->{regexp_opts}}, $1) ; } elsif (/^clear_tags\s*=\s*(.+)$/) { $self->{clear_opt} = $1 ; } elsif (/^append_tags\s*=\s*(.+)$/) { $self->{append_opt} = $1 ; } elsif (/^no_tagging\s*=\s*(.+)$/) { $self->{no_tagging_opt} = $1 ; } elsif (/^preserve_time\s*=\s*(.+)$/) { $self->{preserve_time} = $1 ; } elsif (/^rename_format\s*=\s*"(.*)"$/) { if ($1 eq "") { $self->{rename_opt} = undef ; } else { $self->{rename_opt} = $1 ; } } elsif (/^rename_min\s*=\s*(.+)$/) { $self->{rename_min_opt} = $1 ; } elsif (/^rename_sep\s*=\s*"(.*)"$/) { $self->{rename_sep_opt} = $1 ; } elsif (/^rename_slash\s*=\s*"(.*)"$/) { $self->{rename_slash_opt} = $1 ; } elsif (/^rename_regexp\s*=\s*"(.+)"$/) { push (@{$self->{rename_regexp_opts}}, $1) ; } elsif (/^rename_ext\s*=\s*(.+)$/) { $self->{rename_ext_opt} = $1 ; } elsif (/^dry_run\s*=\s*(.+)$/) { $self->{dry_run_opt} = $1 ; } elsif (/^yes\s*=\s*(.+)$/) { $self->{yes_opt} = $1 ; $self->{ask_opt} = 0 if $1 ; } elsif (/^ask\s*=\s*(.+)$/) { $self->{ask_opt} = $1 ; $self->{yes_opt} = 0 if $1 ; } elsif (/^recursive\s*=\s*(.+)$/) { $self->{recursive_opt} = $1 ; } elsif (/^verbose\s*=\s*(.+)$/) { $verbose_level = $1 ; } elsif (/^cddb\s*=\s*(.+)$/) { $self->{cddb_opt} = $1 ; } elsif (/^cddb_server_name\s*=\s*"(.+)"$/) { $self->{cddb_server_name} = $1 ; } elsif (/^cddb_server_port\s*=\s*(\d+)$/) { $self->{cddb_server_port} = $1 ; } elsif (/^edit = \s*(.+)$/) { $self->{edit_opt} = $1 ; # Error } elsif (/^[^#]/ && !/^(\s*)$/) { die "Unrecognized option line #$.: \"$_\"\n" ; } } sub parse_generic_config_file { my $file = shift ; open CONF, $file or return ; while (<CONF>) { process_option $_ ; } close CONF ; } parse_generic_config_file "$self->{common_lltag_dir}/$self->{lltag_config_filename}" ; parse_generic_config_file "$self->{user_lltag_dir}/$self->{lltag_config_filename}" ; sub parse_additional_config_file { my $file = shift ; open CONF, $file or die "Failed to open additional configuration file '$file' ($!).\n" ; while (<CONF>) { process_option $_ ; } close CONF ; } foreach my $file (@additional_config_files) { parse_additional_config_file $file ; } ####################################################### # parse cmdline options # user format list given on the command-line, # to be added to front of config file user_format_strings my @cmdline_user_format_strings = () ; # parse options GetOptions( 'F|format=s' => \@cmdline_user_format_strings, 'G|guess' => \$self->{guess_opt}, 'C|cddb' => \$self->{cddb_opt}, 'E|edit' => \$self->{edit_opt}, 'p|no-path' => \$self->{no_path_opt}, 'spaces' => \$self->{spaces_opt}, 'maj' => \$self->{maj_opt}, 'sep=s' => \$self->{sep_opt}, 'regexp=s' => \@{$self->{regexp_opts}}, # we do not use backends here since it would require to load them # before parsing the command-line, and is useless anyway 'mp3' => sub { $self->{type_opt} = "mp3" ; }, 'ogg' => sub { $self->{type_opt} = "ogg" ; }, 'flac' => sub { $self->{type_opt} = "flac" ; }, 'type=s' => sub { shift ; $self->{type_opt} = shift ; }, 'mp3v2|id3v2' => \$self->{mp3v2_opt}, 'mp3read=s' => \$self->{mp3v2_read_opt}, 'clear' => \$self->{clear_opt}, 'append' => \$self->{append_opt}, 'no-tagging' => \$self->{no_tagging_opt}, 'T|preserve-time' => \$self->{preserve_time}, 'rename=s' => \$self->{rename_opt}, 'rename-min' => \$self->{rename_min_opt}, 'rename-sep=s' => \$self->{rename_sep_opt}, 'rename-slash=s' => \$self->{rename_slash_opt}, 'rename-regexp=s' => \@{$self->{rename_regexp_opts}}, 'rename-ext' => \$self->{rename_ext_opt}, 'dry-run' => \$self->{dry_run_opt}, 'R|recursive' => \$self->{recursive_opt}, 'yes' => sub { $self->{yes_opt} = 1 ; $self->{ask_opt} = 0 ; }, 'ask' => sub { $self->{ask_opt} = 1 ; $self->{yes_opt} = 0 ; }, 'cddb-server=s' => sub { shift ; my $name = shift ; if ($name =~ m/(.*):(.*)/) { $self->{cddb_server_name} = $1 ; $self->{cddb_server_port} = $2 ; } else { $self->{cddb_server_name} = $name ; $self->{cddb_server_port} = 80 ; } }, 'cddb-query=s' => \$self->{requested_cddb_query}, 'v|verbose' => sub { shift ; $verbose_level++ ; }, 'q|quiet' => sub { shift ; $verbose_level-- ; }, 'gencfg=s' => \$self->{gencfg_file}, 'h|help' => sub { usage () ; }, 'V|version' => sub { version () ; }, 'L|list' => \$self->{list_formats_opt}, 'show-tags=s' => \$self->{show_tags_opt}, 'S' => sub { $self->{show_tags_opt} = "all" ; }, 'tag=s' => sub { shift ; Lltag::Tags::process_explicit_tag_value ($self, shift) ; }, map { my $field = $_ ; $self->{field_name_letter}{$field} .'|'. $field .'=s' => sub { shift ; my $value = shift ; Lltag::Tags::process_explicit_tag_value ($self, "$field=$value") }, } @{$self->{field_names}} , ) or usage () ; # add the command-line user format list to the front of the config file user_format_strings unshift @{$self->{user_format_strings}},@cmdline_user_format_strings ; # check that MP3v2 ID3v1 and v2 read order is corect die "Invalid MP3 ID3v1 and v2 order given to --mp3read: $self->{mp3v2_read_opt}\n" if $self->{mp3v2_read_opt} ne Lltag::MP3v2->MP3V2_READ_V1 and $self->{mp3v2_read_opt} ne Lltag::MP3v2->MP3V2_READ_V2 and $self->{mp3v2_read_opt} ne Lltag::MP3v2->MP3V2_READ_V1_V2 and $self->{mp3v2_read_opt} ne Lltag::MP3v2->MP3V2_READ_V2_V1 ; # set verbosity options depending of the level $self->{verbose_opt} = ($verbose_level > 1) ; $self->{menu_usage_once_opt} = ($verbose_level > 0) ; # if --cddb-query is passed, enable cddb too $self->{cddb_opt} = 1 if defined $self->{requested_cddb_query} ; # yes/ask option status may vary with user confirmation replies my $current_main_yes_opt = $self->{yes_opt} ; Lltag::CDDB::init_cddb ($self) ; Lltag::Parse::init_parsing ($self) ; Lltag::Tags::init_tagging ($self) ; Lltag::Rename::init_renaming ($self) ; ####################################################### # backends # hash backends by type my %backends = () ; # hash types by file extention my %file_extension_to_backend_type = () ; # return 0 on success, -1 on error, 1 on busy sub register_backend { my $backend = shift ; return -1 if not defined $backend ; my $backend_extension = $backend->{extension} ; my $backend_type = $backend->{type} ; my $backend_name = $backend->{name} ; if (defined $backends{$backend_type}) { print "Failed to register backend '$backend_name' since file type '$backend_type' already exists.\n" if $self->{verbose_opt} ; return 1 ; } if (defined $file_extension_to_backend_type{$backend_extension}) { print "Failed to register backend '$backend_name' since file extenstion '$backend_extension' already exists.\n" if $self->{verbose_opt} ; return 1 ; } $backends{$backend_type} = $backend ; $file_extension_to_backend_type{$backend_extension} = $backend_type ; print "Registered backend '$backend_name' for type '$backend_type' extension '$backend_extension'.\n" if $self->{verbose_opt} ; return 0 ; } # register MP3v2 backend if enabled, and fallback to MP3 if failed if (!$self->{mp3v2_opt} or register_backend (Lltag::MP3v2::new ($self)) < 0) { Lltag::Misc::print_warning ("", "Failed to register MP3v2 backend, falling back to MP3.") if $self->{mp3v2_opt} ; register_backend (Lltag::MP3::new ($self)) ; } # register OGG backend register_backend (Lltag::OGG::new ($self)) ; # register FLAC backend register_backend (Lltag::FLAC::new ($self)) ; ####################################################### # check that the file type is correct die "Unrecognized file type '$self->{type_opt}'.\n" if defined $self->{type_opt} and not defined $backends{$self->{type_opt}} ; ####################################################### # extract backend and parsename sub extract_parsename_and_backend { my $file = shift ; # split into parsename and extension, and remove the path if asked my @parts = split (/\./, $file) ; my $extension = pop @parts ; my $parsename = join (".", @parts) ; if ($self->{no_path_opt}) { my @parts = split (/\//, $parsename) ; $parsename = pop @parts ; } my $file_type ; if (defined $self->{type_opt}) { $file_type = $self->{type_opt} ; } else { # if not forced, get the type from the extension $file_type = $file_extension_to_backend_type{lc($extension)} ; } return ($parsename, undef) unless defined $file_type ; return ($parsename, $backends{$file_type}) ; } ####################################################### # show existing tags if --show-tags was passed sub read_tags { my $file = shift ; my $backend = shift ; # extract tags as a stream my $read_tags_func = $backend->{read_tags} ; my $values = &$read_tags_func ($self, $file) ; Lltag::Misc::print_warning (" ", "Failed to get tags from file.") unless defined $values ; return $values ; } ####################################################### # real tagging sub tag_with_values { my $file = shift ; my $backend = shift ; my $values = shift ; # tagging command line my $set_tags_func = $backend->{set_tags} ; &{$set_tags_func}($self, $file, $values) ; } ####################################################### # add explicit_values sub add_explicit_values { my $self = shift ; my $values = shift ; # append explicit values Lltag::Tags::append_tag_values ($self, $values, $self->{explicit_values}) ; return $values ; } ####################################################### # wrappers for all tag-obtention routines sub try_parse_with_preferred { my $self = shift ; my $file = shift ; my $parsename = shift ; my $old_values = shift ; # save old values before merging $old_values = Lltag::Tags::clone_tag_values ($old_values) ; my ($res, $new_values) = Lltag::Parse::try_to_parse_with_preferred ($self, $file, $parsename) ; if ($res == Lltag::Parse->PARSE_SUCCESS) { return Lltag::Tags::merge_new_tag_values ($self, $old_values, (add_explicit_values ($self, $new_values))) ; } # sanity check die "Unknown tag return value: $res.\n" unless $res == Lltag::Parse->PARSE_NO_MATCH ; return undef ; } sub try_parse { my $self = shift ; my $file = shift ; my $parsename = shift ; my $try_internals = shift ; my $old_values = shift ; # save old values before merging $old_values = Lltag::Tags::clone_tag_values ($old_values) ; my ($res, $new_values) = Lltag::Parse::try_to_parse ($self, $file, $parsename, $try_internals) ; if ($res == Lltag::Parse->PARSE_SUCCESS or $res == Lltag::Parse->PARSE_SUCCESS_PREFERRED) { return Lltag::Tags::merge_new_tag_values ($self, $old_values, (add_explicit_values ($self, $new_values))) ; } # sanity check die "Unknown tag return value: $res.\n" unless $res == Lltag::Parse->PARSE_NO_MATCH or $res == Lltag::Parse->PARSE_ABORT ; return undef ; } sub try_edit { my $self = shift ; my $current_values = shift ; # save old values before editing $current_values = Lltag::Tags::clone_tag_values ($current_values) ; # do not add explicit here since this function might # be called on current values without need to add them my $res = Lltag::Tags::edit_values ($self, $current_values) ; if ($res == Lltag::Tags->EDIT_SUCCESS) { return $current_values ; } # sanity check die "Unknown edit return value: $res.\n" unless $res == Lltag::Tags->EDIT_CANCEL ; return undef ; } sub try_cddb { my $self = shift ; my $old_values = shift ; # save old values before merging $old_values = Lltag::Tags::clone_tag_values ($old_values) ; my ($res, $new_values) = Lltag::CDDB::get_cddb_tags ($self) ; if ($res == Lltag::CDDB->CDDB_SUCCESS) { return Lltag::Tags::merge_new_tag_values ($self, $old_values, (add_explicit_values ($self, $new_values))) ; } # sanity check die "Unknown CDDB return value: $res.\n" unless $res == Lltag::CDDB->CDDB_ABORT ; return undef ; } sub try_explicits { my $self = shift ; my $old_values = shift ; my $empty_values = {} ; # save old values before merging $old_values = Lltag::Tags::clone_tag_values ($old_values) ; return Lltag::Tags::merge_new_tag_values ($self, $old_values, (add_explicit_values ($self, $empty_values))) ; } ####################################################### # main confirmation loop my $main_confirm_menu_usage_forced = $self->{menu_usage_once_opt} ; sub main_confirm_menu_usage { Lltag::Misc::print_usage_header (" ", "Main menu") ; print " y => Yes, use these tags (default)\n" ; print " a => Always yes, stop asking for a confirmation\n" ; print " P => Try to parse the file\n" ; print " C => Query CDDB\n" ; print " E => Edit values\n" ; print " D => Only use explicit values\n" ; print " Z => Reset to no values\n" ; print " R => Revert to old values\n" ; print " O => Display old values\n" ; print " n => Skip tagging and jump to rename\n" ; print " s/q => Skip this file\n" ; print " Q => Quit without tagging anything anymore\n" ; print " h => Show this help\n" ; $main_confirm_menu_usage_forced = 0 ; # TODO update } use constant CONFIRM_TAG => 1 ; use constant CONFIRM_DONT_TAG => 2 ; use constant CONFIRM_SKIP_FILE => -1 ; use constant CONFIRM_EXIT => -2 ; sub main_confirm_menu { my $self = shift ; my $file = shift ; my $parsename = shift ; my $current_values = shift ; my $old_values = shift ; while (1) { # display current values if (keys %{$current_values}) { print " Current tag values are:\n" ; Lltag::Tags::display_tag_values ($self, $current_values, " ") ; } else { print " There are no current tag values.\n" ; } # display the menu once main_confirm_menu_usage if $main_confirm_menu_usage_forced ; # ask the user for confirmation my $confirm_reply = Lltag::Misc::readline (" ", "Use these tag values [yaPCEDZROnqQ] (default is yes, h for help)", "", -1) ; # if ctrl-d, skip this file $confirm_reply = 'q' unless defined $confirm_reply ; if ($confirm_reply =~ m/^y/ or $confirm_reply eq "") { return (CONFIRM_TAG, $current_values) } elsif ($confirm_reply =~ m/^a/) { $current_main_yes_opt = 1 ; return (CONFIRM_TAG, $current_values) } elsif ($confirm_reply =~ m/^n/) { print " Skipping tagging for this file...\n" ; return (CONFIRM_DONT_TAG, $current_values) } elsif ($confirm_reply =~ m/^s/ or $confirm_reply =~ m/^q/) { return (CONFIRM_SKIP_FILE, undef) ; } elsif ($confirm_reply =~ m/^Q/) { return (CONFIRM_EXIT, undef) ; } elsif ($confirm_reply =~ m/^P/) { my $new_values = try_parse ($self, $file, $parsename, 1, $old_values) ; $current_values = $new_values if defined $new_values ; } elsif ($confirm_reply =~ m/^C/) { my $new_values = try_cddb ($self, $old_values) ; $current_values = $new_values if defined $new_values ; } elsif ($confirm_reply =~ m/^E/) { my $new_values = try_edit ($self, $current_values) ; $current_values = $new_values if defined $new_values ; } elsif ($confirm_reply =~ m/^D/) { $current_values = try_explicits ($self, $old_values) ; } elsif ($confirm_reply =~ m/^Z/) { $current_values = {} ; } elsif ($confirm_reply =~ m/^R/) { $current_values = Lltag::Tags::clone_tag_values ($old_values) ; } elsif ($confirm_reply =~ m/^O/) { # display old values if (keys %{$old_values}) { print " Existing tag values were:\n" ; Lltag::Tags::display_tag_values ($self, $old_values, " ") ; } else { print " There were no tag values.\n" ; } } else { main_confirm_menu_usage ; } } } ####################################################### # main process # if -L was passed, show formats and exit if ($self->{list_formats_opt}) { # read internal parsers Lltag::Parse::read_internal_parsers ($self) ; # list them print "Listing internal parsers:\n" ; Lltag::Parse::list_internal_parsers () ; exit 0 ; } # process remaining command-line arguments as files my @files = () ; while ( @ARGV ) { if ($self->{recursive_opt}) { my $dir = shift @ARGV ; open FIND, "find \"$dir\" |" ; my @dirfiles = <FIND> ; close FIND ; foreach my $file (@dirfiles) { chomp $file ; if (-f $file) { push (@files, $file) ; } } } else { my $file = shift @ARGV ; if (-f $file) { push (@files, $file) ; } else { print "Skipping the non-file '$file'\n" ; } } } if (!@files) { print "No files to process.\n" if $self->{verbose_opt} ; # nothing to do (except maybe a config file to generate), so print the help usage () unless $self->{gencfg_file} ; } # display tags if ($self->{show_tags_opt}) { my @fields_to_show = split (/,/, $self->{show_tags_opt}) ; foreach my $file (@files) { print "$file:\n" if @files ; # do not print filename if there is only one file my ($parsename, $backend) = extract_parsename_and_backend $file ; if (not defined $backend) { print " Skipping this unknown-type file.\n" ; next ; } my $values = read_tags $file, $backend ; foreach my $field (@{$self->{field_names}}, (Lltag::Tags::get_values_non_regular_keys ($self, $values))) { next if not defined $values->{$field} ; next unless grep { /^all$/ } @fields_to_show or grep { /^$field$/i } @fields_to_show ; map { print " $field=".($field =~ / -> _/ ? "<binary data>" : $_)."\n" } (Lltag::Tags::get_tag_value_array ($self, $values, $field)) ; } } exit 0 ; } # main reading/parsing/tagging/renaming loop while ( @files ) { my $file = shift @files ; my $res ; print "\n" ; print "Processing file \"".$file."\"...\n" ; my ($parsename, $backend) = extract_parsename_and_backend $file ; if (not defined $backend) { print " Skipping this file '$file' with unknown type.\n" ; next ; } # read old tags my $old_values = read_tags $file, $backend ; # FIXME: read the tracknumber and pass it to cddb ? # FIXME: parse first, to get the track number? my $current_values = undef ; # try preferred parser first { my $new_values = try_parse_with_preferred $self, $file, $parsename, $old_values ; if (defined $new_values) { $current_values = $new_values ; goto CONFIRM ; } } # edit, if enabled if ($self->{edit_opt}) { my $new_values = try_edit ($self, try_explicits ($self, $old_values)) ; if (defined $new_values) { $current_values = $new_values ; goto CONFIRM ; } } # try CDDB, if enabled if ($self->{cddb_opt}) { my $new_values = try_cddb $self, $old_values ; if (defined $new_values) { $current_values = $new_values ; goto CONFIRM ; } } # try to parse with all parsers, either user-provided or internals # if guess or any user-parser or NOTHING else enabled if ($self->{guess_opt} or @{$self->{user_format_strings}} or ( !(keys %{$self->{explicit_values}}) and !$self->{cddb_opt} and !$self->{edit_opt} and !$self->{rename_opt} ) ) { # force guess if NOTHING enabled my $try_internals = ($self->{guess_opt} or ( !@{$self->{user_format_strings}} and !(keys %{$self->{explicit_values}}) and !$self->{cddb_opt} and !$self->{edit_opt} and !$self->{rename_opt} ) ) ; my $new_values = try_parse $self, $file, $parsename, $try_internals, $old_values ; if (defined $new_values) { $current_values = $new_values ; goto CONFIRM ; } } $current_values = try_explicits ($self, $old_values) ; CONFIRM: if ($current_main_yes_opt) { $res = CONFIRM_TAG ; } else { ($res, $current_values) = main_confirm_menu $self, $file, $parsename, $current_values, $old_values ; } if ($res == CONFIRM_EXIT) { print " Exiting...\n" ; last ; } elsif ($res == CONFIRM_SKIP_FILE) { print " Skipping this file...\n" ; next ; } elsif ($res == CONFIRM_TAG) { if (!$self->{no_tagging_opt}) { # save access and modification times my ($old_atime, $old_mtime) = (stat ($file)) [8, 9] ; # actually tagging tag_with_values $file, $backend, $current_values ; # restore old times if needed utime $old_atime, $old_mtime, $file if $self->{preserve_time} ; } } # sanity check die "Unknown confirm menu return value: $res.\n" unless $res == CONFIRM_TAG or $res == CONFIRM_DONT_TAG ; # renaming if (defined $self->{rename_opt}) { my $extension = $backend->{extension} ; Lltag::Rename::rename_with_values ($self, $file, $extension, $current_values) ; } } ############################################# # generate configuration file sub generate_config { my $file = shift ; die "Cannot generate $file which already exists." if -e "$file" ; open NEWCFG, ">$file" or die "Cannot open $file ($!).\n" ; print NEWCFG "# This is a lltag configuration file.\n" ; print NEWCFG "# It was automatically generated.\n" ; print NEWCFG "# You may modify and reuse it as you want.\n" ; print NEWCFG "\n" ; map { print NEWCFG "format = \"$_\"\n" ; } @{$self->{user_format_strings}} ; print NEWCFG "guess = $self->{guess_opt}\n" ; print NEWCFG "edit = $self->{edit_opt}\n" ; print NEWCFG "no_path = $self->{no_path_opt}\n" ; map { my $key = $_ ; my @array = Lltag::Tags::get_tag_value_array($self, $self->{explicit_values}, $_) ; map { print NEWCFG "tag = \"$key=$_\"\n" } @array ; } (keys %{$self->{explicit_values}}) ; print NEWCFG "spaces = $self->{spaces_opt}\n" ; print NEWCFG "maj = $self->{maj_opt}\n" ; print NEWCFG "sep = \"$self->{sep_opt}\"\n" if defined $self->{sep_opt} ; map { print NEWCFG "regexp = \"$_\"\n" ; } @{$self->{regexp_opts}} ; print NEWCFG "type = ". (defined $self->{type_opt} ? $self->{type_opt} : "none") ."\n" ; print NEWCFG "clear_tags = $self->{clear_opt}\n" ; print NEWCFG "append_tags = $self->{append_opt}\n" ; print NEWCFG "no_tagging = $self->{no_tagging_opt}\n" ; print NEWCFG "preserve_time = $self->{preserve_time}\n" ; print NEWCFG "rename_format = \"" .($self->{rename_opt} ? $self->{rename_opt} : ""). "\"\n" ; print NEWCFG "rename_min = $self->{rename_min_opt}\n" ; print NEWCFG "rename_sep = \"$self->{rename_sep_opt}\"\n" ; print NEWCFG "rename_slash = \"$self->{rename_slash_opt}\"\n" ; map { print NEWCFG "rename_regexp = \"$_\"\n" ; } @{$self->{rename_regexp_opts}} ; print NEWCFG "rename_ext = $self->{rename_ext_opt}\n" ; print NEWCFG "cddb = $self->{cddb_opt}\n" ; print NEWCFG "cddb_server_name = \"$self->{cddb_server_name}\"\n" ; print NEWCFG "cddb_server_port = $self->{cddb_server_port}\n" ; print NEWCFG "dry_run = $self->{dry_run_opt}\n" ; print NEWCFG "yes = $self->{yes_opt}\n" ; print NEWCFG "ask = $self->{ask_opt}\n" ; print NEWCFG "recursive = $self->{recursive_opt}\n" ; print NEWCFG "verbose = $verbose_level\n" ; close NEWCFG ; } generate_config $self->{gencfg_file} if defined $self->{gencfg_file} ; Lltag::Misc::exit_readline () ; ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������lltag-lltag-0.14.6/lltag_config.5�������������������������������������������������������������������0000664�0000000�0000000�00000010763�13141671023�0016531�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������.\" Process this file with .\" groff -man -Tascii foo.1 .\" .TH lltag_config 5 "NOVEMBER 2006" .SH NAME config \- Configuration file for lltag .SH DESCRIPTION The following options may be stored in .I /etc/lltag/config or the user's .IR $HOME/.lltag/config , or in any file passed with .IR --config . Such a configuration file may also be generated with .IR --gencfg . .SH Obtaining tags .I format = \fI"string" .RS Add a user-defined format .RB [ -R ]. Might be used multiple times. Default is to guess if no user-defined formats and no default field values are given. .RE .I guess = <0/1> .RS Try to guess if user-defined formats do not match .RB [ -G ]. Default is .BR 0 " (" disabled ") when no user-defined formats and no explicit values are given." .RE .I tag = <TAG=value> .RS Add an explicit tag .RB [ --tag ]. Might be used multiple times. .RE .SH Tweaking filename parsing .I no_path = <0/1> .RS Do not use file path when matching filename .RB [ -p ]. Default is .BR 0 " (" disabled ")." .RE .I spaces = <0/1> .RS Allow no or multiple spaces .RB [ --spaces ]. Default is .BR 0 " (" disabled ")." .RE .SH Cleaning obtained tags .I edit = <0/1> .RS Edit tags immediately .RB [ --edit ]. Default is .BR 0 " (" disabled ")." .RE .I maj = <0/1> .RS Upcase first letters of words in tags .RB [ --maj ]. Default is .BR 0 " (" disabled ")." .RE .I regexp = \fI"s/from/to/" .RS Replace \fIfrom\fR with \fIto\fR in all tags. .I title,number:s/from/to/ replaces in title and number tags only. Might be used multiple times .RB [ --regexp ]. Default is to not apply any regexp. .RE .I sep = \fI"string" .RS Replace |-separated strings with space in tags. Default is to not replace any separator. .RE .SH Configuration of tag application .I type = <mp3|ogg|flac|none> .RS Force mp3, ogg of flac instead of by-extension detection .RB [ --mp3 ", " --ogg " and " --flac ]. Default is .BR none . .RE .I clear_tags = <0/1> .RS Clear all tags of audio files. .BR [ --clear ]. Default is .BR 0 " (" disabled ")." .RE .I append_tags = <0/1> .RS Append tags only instead of replacing old ones. .RB [ --append ]. Default is .BR 0 " (" disabled ")." .RE .I no_tagging = <0/1> .RS Do not actually tag files .RB [ --no-tagging ]. Default is .BR 0 " (" disabled ")." .RE .I preserve_time = <0/1> .RS Preserve file modification time during tagging .RB [ --preserve-time ]. Default is .BR 0 " (" disabled ")." .RE .SH Renaming .I rename_format = \fI"string" .RS Rename file according to format .RB [ --rename ]. Default is to not rename. .RE .I rename_min = <0/1> .RS Lowcase tags before renaming .RB [ --rename-min ]. Default is .BR 0 " (" disabled ")." .RE .I rename_regexp = \fI"s/from/to/" .RS Replace \fIfrom\fR with \fIto\fR in all tags before renaming. .I title,number:s/from/to/ replaces in title and number tags only. Might be used multiple times .RB [ --rename-regexp ]. Default is to not apply any regexp. .RE .I rename_sep = \fI"string" .RS Replace spaces with a string in tags before renaming .RB [ --rename-sep ]. Default is to not replace any separator. .RE .I rename_slash = \fI"string" .RS Replace slashes with a string in tags before renaming .RB [ --rename-slash ]. Default is to replace with a dash. .RE .I rename_ext = <0/1> .RS Assume the rename format provides an extension .RB [ --rename-ext ]. Default is .BR 0 " (" disabled ")." .RE .SH Miscellaneous .I dry_run = <0/1> .RS Do nothing but show what would have been done .RB [ --dry-run ]. Default is .BR 0 " (" disabled ")." .RE .I yes = <0/1> .RS Tag without asking for confirmation when guessing and rename without asking for confirmation .RB [ --yes ]. Default is .BR 0 " (" disabled ")." .RE .I ask = <0/1> .RS Always ask for confirmation before tagging .RB [ --ask ]. Default is .BR 0 " (" disabled ")." .RE .I recursive = <0/1> .RS Recursively traverse all given subdirectories .RB [ -R ]. Default is .BR 0 " (" disabled ")." .RE .I verbose = <integer> .RS Message verbosity level .RB [ -v " and " -q ]. Default is .BR 0 " (" "only important messages" ")." Other possible values are .BR 1 " (" "show usage information when a menu is displayed for the first time" ")" and .BR 2 " (" "always show usage information before a menu appears" ")." .RE .SH CDDB configuration .I cddb_server_name = "hostname" .RS Change the CDDB server name. Default is .BR www.freedb.org . .RE .I cddb_server_port = <port> .RS Change the CDDB server port. Default is .BR 80 " (" HTTP ")." .RE .SH SEE ALSO .PP .BR lltag (1) The .I config template file provided within the documentation directory. .SH AUTHOR Brice Goglin �������������lltag-lltag-0.14.6/lltag_formats.5������������������������������������������������������������������0000664�0000000�0000000�00000003502�13141671023�0016730�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������.\" Process this file with .\" groff -man -Tascii foo.1 .\" .TH lltag_formats 5 "NOVEMBER 2006" .SH NAME formats \- Internal formats database file for lltag .SH DESCRIPTION The internal format database is usually stored in .IR /etc/lltag/formats . The user may override this file by defining a .IR $HOME/.lltag/formats . If this file exists, the system-wide one is ignored. These files contain entries starting with a line such as: .I [%n - %a - %t] .RS A title between bracket that will be displayed at runtime. .RE Then, the following 3 lines must be given to explain how the format is actually used to parse filenames: .I type = basename .RS The type is either .I basename (to parse the last part of the path to a file) or .I path (to parse the directory part of the path to a file). When actually parsing the path to target files, .I basename and .I path parsers will be assembled. All possible combination will be tried. .RE .I regexp = %L%N%S-%S%A%S-%S%A%L .RS A string composed of any characters, with the following special fields: .I %L for delimiter (empty by default, multiple spaces if .I --spaces was given) .I %S for a space (or multiple spaces if .I --spaces was given) .I %N for numbers .I %A for an alphanumeric string without / .I %P for any path (alphanumeric string with /) .I %% for % .RE .I indices = NUMBER,ARTIST,TITLE .RS A list of fields to match (either given by their full name or associated letter) corresponding to each %N or %A field in the previous format. It may be .BR AUTHOR " (or " a "), " .BR ALBUM " (" A "), " .BR GENRE " (" g "), " .BR NUMBER " (" n "), " .BR TITLE " (" t "), " .BR DATE " (" d "), " .BR COMMENT " (" c ") or " .BR IGNORE " (" i ")." See also .B FORMAT in the manpage of .I lltag for details about these fields. .SH SEE ALSO .PP .BR lltag (1) .SH AUTHOR Brice Goglin ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������