├── VERSION ├── lib ├── Makefile.PL.in ├── MANIFEST └── Lltag │ ├── FLAC.pm │ ├── OGG.pm │ ├── MP3.pm │ ├── Misc.pm │ ├── Rename.pm │ ├── MP3v2.pm │ ├── Tags.pm │ ├── Parse.pm │ └── CDDB.pm ├── doc ├── Makefile ├── config └── howto.html ├── README ├── lltag_formats.5 ├── formats ├── Makefile ├── lltag_config.5 ├── COPYING ├── Changes └── lltag.1 /VERSION: -------------------------------------------------------------------------------- 1 | 0.14.6 2 | -------------------------------------------------------------------------------- /lib/Makefile.PL.in: -------------------------------------------------------------------------------- 1 | use ExtUtils::MakeMaker; 2 | 3 | WriteMakefile ( 4 | NAME => 'Lltag', 5 | VERSION => "@VERSION@", 6 | ); 7 | -------------------------------------------------------------------------------- /lib/MANIFEST: -------------------------------------------------------------------------------- 1 | MANIFEST 2 | Makefile.PL 3 | Lltag/Tags.pm 4 | Lltag/Misc.pm 5 | Lltag/MP3.pm 6 | Lltag/MP3v2.pm 7 | Lltag/OGG.pm 8 | Lltag/FLAC.pm 9 | Lltag/CDDB.pm 10 | Lltag/Parse.pm 11 | Lltag/Rename.pm 12 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install uninstall 2 | 3 | install: 4 | install -d -m 0755 $(DOCDIR) 5 | find . -type d -exec install -d -m 0755 $(DOCDIR)/{} \; 6 | find . -type f -not -name Makefile -exec install -m 0644 {} $(DOCDIR)/{} \; 7 | 8 | uninstall: 9 | find . -type f -not -name Makefile -exec rm -f $(DOCDIR)/{} \; 10 | find . -depth -type d -exec rmdir $(DOCDIR)/{} \; 11 | rmdir $(DOCDIR) || true 12 | -------------------------------------------------------------------------------- /doc/config: -------------------------------------------------------------------------------- 1 | # lltag configuration file example. 2 | # 3 | # The following options may be stored in /etc/lltag/config, 4 | # a per-user ${HOME}/.lltag/config or in a file passed 5 | # on the command-line with --config. 6 | 7 | # Add a user-defined format. 8 | # format = "" 9 | 10 | # Try to guess if user-defined formats do not match. 11 | # guess = 0 12 | 13 | # Try CDDB 14 | # cddb = 0 15 | 16 | # CDDB server 17 | # cddb_server_name = "tracktype.org" 18 | # cddb_server_port = 80 19 | 20 | # Do not use file path when matching filename. 21 | # no_path = 0 22 | 23 | # Explicit tag. 24 | # Might be used multiple times. 25 | # tag = "TAG=value" 26 | 27 | # Allow no or multiple spaces. 28 | # spaces = 0 29 | 30 | # Upcase first letters of words in tags. 31 | # maj = 0 32 | 33 | # Replace from with to in all tags with s/from/to/ in tags. 34 | # title,number:s/from/to/ replace in title and number tags only. 35 | # Might be used multiple times. 36 | # regexp = "" 37 | 38 | # Replace |-separated strings with space in tags. 39 | # sep = "" 40 | 41 | # Force mp3, ogg of flac instead of by-extension detection. 42 | # type = 43 | 44 | # Clear all tags of audio files. 45 | # clear_tags = 0 46 | 47 | # Append tags only instead of replacing old ones. 48 | # append_tags = 0 49 | 50 | # Do not actually tag files. 51 | # no_tagging = 0 52 | 53 | # Rename file according to format. 54 | # rename_format = "" 55 | 56 | # Lowcase tags before renaming. 57 | # rename_min = 0 58 | 59 | # Replace from with to in all tags with s/from/to/ before renaming. 60 | # title,number:s/from/to/ replace in title and number tags only. 61 | # Might be used multiple times. 62 | # rename_regexp = "" 63 | 64 | # Replace space with s in tags before renaming. 65 | # rename_sep = "" 66 | 67 | # Assume the rename format provides an extension. 68 | # rename_ext = 0 69 | 70 | # Do nothing but show what would have been done. 71 | # dry_run = 0 72 | 73 | # Tag without asking for confirmation when guessing 74 | # and rename without asking for confirmation. 75 | # yes = 0 76 | 77 | # Always ask for confirmation before tagging. 78 | # ask = 0 79 | 80 | # Recursively traverse all given subdirectories. 81 | # recursive = 0 82 | 83 | # Messages verbosity level 84 | # verbose = 0 85 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | lltag is a frontend to tag (and rename) mp3/ogg/flac files automagically. 2 | 3 | See http://bgoglin.free.fr/lltag/ for details. 4 | 5 | lltag installation instructions 6 | =============================== 7 | 8 | You don't care about this if you use Debian or Gentoo or any other 9 | distribution that includes prepackaged lltag versions. 10 | 11 | Run 'make' followed by 'make install' as root. 12 | *Both* command-lines must use the same variables for configuring 13 | installation paths. 14 | 15 | By default, everything is installed in /usr/local/. 16 | Most directories might be changed by overriding their default values 17 | on the command-line. 18 | For instance, a traditional installation (binary in /usr/bin, data in 19 | /usr/share, and configuration in /etc) might be achieved with: 20 | 21 | $ make PREFIX=/usr SYSCONFDIR=/etc MANDIR=/usr/share/man 22 | $ make install PREFIX=/usr SYSCONFDIR=/etc MANDIR=/usr/share/man 23 | 24 | If you don't want or can't install as root, you may use 25 | $ make PREFIX=/home/login/where/you/want 26 | $ make install PREFIX=/home/login/where/you/want 27 | 28 | Note that 'make uninstall' (with same options) allows to uninstall. 29 | 30 | lltag requirements 31 | ================== 32 | 33 | lltag theoretically only requires Perl base to work. However, depending 34 | on what type of files you want to work on, you'll need to install 35 | either the Perl MP3::Tag module (for MP3 files) or mp3info (for MP3 files, 36 | without ID3v2 support), vorbiscomment (for OGG) or metaflac (for FLAC). 37 | Additionally, when using CDDB features, LWP (the libwww-perl module) is 38 | required. 39 | These dependencies are automatically installed when installing 40 | Debian or Gentoo prepackaged versions, or at least recommended. 41 | 42 | lltag may also benefit from a smart readline perl library when 43 | the user edits the tags by hand. 44 | The recommended library is Term::ReadLine::Gnu (also called 45 | GNU Readline Library Wrapper Module) which provides great 46 | inline editing and GNU history features. 47 | Term::ReadLine::Perl may also be used but it might not be able 48 | to save the history between two lltag invocation. 49 | 50 | If you experience any problem, 51 | please see http://bgoglin.free.fr/lltag 52 | or report to . 53 | -------------------------------------------------------------------------------- /lltag_formats.5: -------------------------------------------------------------------------------- 1 | .\" Process this file with 2 | .\" groff -man -Tascii foo.1 3 | .\" 4 | .TH lltag_formats 5 "NOVEMBER 2006" 5 | 6 | 7 | 8 | .SH NAME 9 | formats \- Internal formats database file for lltag 10 | 11 | 12 | 13 | 14 | .SH DESCRIPTION 15 | The internal format database is usually stored in 16 | .IR /etc/lltag/formats . 17 | The user may override this file by defining a 18 | .IR $HOME/.lltag/formats . 19 | If this file exists, the system-wide one is ignored. 20 | 21 | 22 | 23 | These files contain entries starting with a line such as: 24 | 25 | .I [%n - %a - %t] 26 | .RS 27 | A title between bracket that will be displayed at runtime. 28 | .RE 29 | 30 | 31 | 32 | Then, the following 3 lines must be given to explain how the format 33 | is actually used to parse filenames: 34 | 35 | .I type = basename 36 | .RS 37 | The type is either 38 | .I basename 39 | (to parse the last part of the path to a file) 40 | or 41 | .I path 42 | (to parse the directory part of the path to a file). 43 | When actually parsing the path to target files, 44 | .I basename 45 | and 46 | .I path 47 | parsers will be assembled. 48 | All possible combination will be tried. 49 | .RE 50 | 51 | 52 | 53 | .I regexp = %L%N%S-%S%A%S-%S%A%L 54 | .RS 55 | A string composed of any characters, with the following special fields: 56 | 57 | .I %L 58 | for delimiter (empty by default, multiple spaces if 59 | .I --spaces 60 | was given) 61 | 62 | .I %S 63 | for a space (or multiple spaces if 64 | .I --spaces 65 | was given) 66 | 67 | .I %N 68 | for numbers 69 | 70 | .I %A 71 | for an alphanumeric string without / 72 | 73 | .I %P 74 | for any path (alphanumeric string with /) 75 | 76 | .I %% 77 | for % 78 | .RE 79 | 80 | 81 | 82 | .I indices = NUMBER,ARTIST,TITLE 83 | .RS 84 | A list of fields to match (either given by their full name or associated letter) 85 | corresponding to each %N or %A field in the previous format. 86 | It may be 87 | .BR AUTHOR " (or " a "), " 88 | .BR ALBUM " (" A "), " 89 | .BR GENRE " (" g "), " 90 | .BR NUMBER " (" n "), " 91 | .BR TITLE " (" t "), " 92 | .BR DATE " (" d "), " 93 | .BR COMMENT " (" c ") or " 94 | .BR IGNORE " (" i ")." 95 | See also 96 | .B FORMAT 97 | in the manpage of 98 | .I lltag 99 | for details about these fields. 100 | 101 | 102 | 103 | 104 | .SH SEE ALSO 105 | .PP 106 | .BR lltag (1) 107 | 108 | 109 | 110 | 111 | .SH AUTHOR 112 | Brice Goglin 113 | -------------------------------------------------------------------------------- /lib/Lltag/FLAC.pm: -------------------------------------------------------------------------------- 1 | package Lltag::FLAC ; 2 | 3 | use strict ; 4 | 5 | require Lltag::Tags ; 6 | require Lltag::Misc ; 7 | 8 | sub test_metaflac { 9 | my $self = shift ; 10 | # cannot test with "metaflac -h" since it returns 1 11 | my ($status, @output) = Lltag::Misc::system_with_output ("metaflac", "/dev/null") ; 12 | print "metaflac does not seem to work, disabling 'Flac' backend.\n" 13 | if $status and $self->{verbose_opt} ; 14 | return $status ; 15 | } 16 | 17 | sub read_tags { 18 | my $self = shift ; 19 | my $file = shift ; 20 | my ($status, @output) = Lltag::Misc::system_with_output 21 | ("metaflac", "--list", "--block-type=VORBIS_COMMENT", $file) ; 22 | return undef 23 | if $status ; 24 | @output = map { 25 | my $line = $_ ; 26 | $line =~ s/^\s*comment\[\d+\]\s*:\s*(.*)/$1/i ; 27 | $line =~ s/^tracknumber=/NUMBER=/i ; 28 | $line =~ s/^track number=/NUMBER=/i ; 29 | $line 30 | } ( grep { /comment\[\d+\]/ } @output ) ; 31 | return Lltag::Tags::convert_tag_stream_to_values ($self, @output) ; 32 | } 33 | 34 | sub set_tags { 35 | my $self = shift ; 36 | my $file = shift ; 37 | my $values = shift ; 38 | 39 | my %field_name_flac_translations = 40 | ( 41 | 'NUMBER' => 'TRACKNUMBER', 42 | ) ; 43 | my @flac_tagging_cmd = ( 'metaflac' ) ; 44 | my @flac_tagging_clear_option = ( '--remove-all-tags' ) ; 45 | 46 | my @system_args 47 | = ( @flac_tagging_cmd , 48 | # clear all tags 49 | @flac_tagging_clear_option , 50 | # apply new tags 51 | ( map { 52 | my $flacname = $_ ; 53 | $flacname = $field_name_flac_translations{$_} if defined $field_name_flac_translations{$_} ; 54 | my @tags = Lltag::Tags::get_tag_value_array ($self, $values, $_) ; 55 | map { ( "--set-tag", $flacname."=".$_ ) } @tags 56 | } @{$self->{field_names}} 57 | ), 58 | # apply non-regular tags 59 | ( map { 60 | my $flacname = $_ ; 61 | my @tags = Lltag::Tags::get_tag_value_array ($self, $values, $_) ; 62 | map { ( "--set-tag", $flacname."=".$_ ) } @tags 63 | } Lltag::Tags::get_values_non_regular_keys ($self, $values) 64 | ), 65 | $file ) ; 66 | 67 | Lltag::Tags::set_tags_with_external_prog ($self, @system_args) ; 68 | } 69 | 70 | sub new { 71 | my $self = shift ; 72 | 73 | return undef 74 | if test_metaflac $self ; 75 | 76 | return { 77 | name => "Flac (using metaflac)", 78 | type => "flac", 79 | extension => "flac", 80 | read_tags => \&read_tags, 81 | set_tags => \&set_tags, 82 | } ; 83 | } 84 | 85 | 1 ; 86 | -------------------------------------------------------------------------------- /formats: -------------------------------------------------------------------------------- 1 | # lltag internal format database 2 | # each entry is composed of: 3 | # [natural format] 4 | # format will be shown when matching 5 | # type = basename or path 6 | # does this format apply to the wall path or only the basename ? 7 | # regexp = 8 | # internal regexp with %L for a delimiter (empty by default, multiple spaces if --spaces was passed) 9 | # %S for a space (or multiple spaces if --spaces was passed) 10 | # %N for a numeric string 11 | # %A for an alphanumeric string without / 12 | # %P for any path (alphanumeric string with /) 13 | # %% for % 14 | # any other character will remain unchanged 15 | # indices = list of comma-separed tag name indicating the corresponding 16 | # %N or %A means in the regexp. 17 | # the field name may be replaced by the corresponding letter: 18 | # a for ARTIST 19 | # t for TITLE 20 | # A for ALBUM 21 | # g for GENRE 22 | # n for NUMBER 23 | # d for DATE 24 | # c for COMMENT 25 | # i for IGNORE 26 | 27 | [%n - %a - %t] 28 | type = basename 29 | regexp = %L%N%S-%S%A%S-%S%A%L 30 | indices = NUMBER,ARTIST,TITLE 31 | 32 | [%n) %a - %t] 33 | type = basename 34 | regexp = %L%N)%S%A%S-%S%A%L 35 | indices = NUMBER,ARTIST,TITLE 36 | 37 | [%n - %t] 38 | type = basename 39 | regexp = %L%N%S-%S%A%L 40 | indices = NUMBER,TITLE 41 | 42 | [%n. %t] 43 | type = basename 44 | regexp = %L%N.%S%A%L 45 | indices = NUMBER,TITLE 46 | 47 | [%n) %t] 48 | type = basename 49 | regexp = %L%N)%S%A%L 50 | indices = NUMBER,TITLE 51 | 52 | [%a - %n - %t] 53 | type = basename 54 | regexp = %L%A%S-%S%N%S-%S%A%L 55 | indices = ARTIST,NUMBER,TITLE 56 | 57 | [%a - %t] 58 | type = basename 59 | regexp = %L%A%S-%S%A%L 60 | indices = ARTIST,TITLE 61 | 62 | [%t] 63 | type = basename 64 | regexp = %L%A%L 65 | indices = TITLE 66 | 67 | [%a/%a - %A] 68 | type = path 69 | regexp = %P%L%A%L/%L%A%S-%S%A%L 70 | indices = ARTIST,ARTIST,ALBUM 71 | 72 | [%a/%A (%d)] 73 | type = path 74 | regexp = %P%L%A%L/%L%A%L%S(%N)%L 75 | indices = ARTIST,ALBUM,DATE 76 | 77 | [%a/%A [%d]] 78 | type = path 79 | regexp = %P%L%A%L/%L%A%L%S[%N]%L 80 | indices = ARTIST,ALBUM,DATE 81 | 82 | [%a/%A] 83 | type = path 84 | regexp = %P%L%A%L/%L%A%L 85 | indices = ARTIST,ALBUM 86 | 87 | [%a - %A] 88 | type = path 89 | regexp = %P%L%A%S-%S%A%L 90 | indices = ARTIST,ALBUM 91 | 92 | [%a] 93 | type = path 94 | regexp = %P%L%A%L 95 | indices = ARTIST 96 | 97 | [%A] 98 | type = path 99 | regexp = %P%L%A%L 100 | indices = ALBUM 101 | 102 | -------------------------------------------------------------------------------- /lib/Lltag/OGG.pm: -------------------------------------------------------------------------------- 1 | package Lltag::OGG ; 2 | 3 | use strict ; 4 | 5 | require Lltag::Tags ; 6 | require Lltag::Misc ; 7 | 8 | sub test_vorbiscomment { 9 | my $self = shift ; 10 | my ($status, @output) = Lltag::Misc::system_with_output ("vorbiscomment", "-h") ; 11 | print "vorbiscomment does not seem to work, disabling 'OGG' backend.\n" 12 | if $status and $self->{verbose_opt} ; 13 | return $status ; 14 | } 15 | 16 | sub read_tags { 17 | my $self = shift ; 18 | my $file = shift ; 19 | my ($status, @output) = Lltag::Misc::system_with_output 20 | ("vorbiscomment", "-l", $file) ; 21 | return undef 22 | if $status ; 23 | @output = map { 24 | my $line = $_ ; 25 | $line =~ s/^TRACKNUMBER=/NUMBER=/ ; 26 | $line 27 | } @output ; 28 | return Lltag::Tags::convert_tag_stream_to_values ($self, @output) ; 29 | } 30 | 31 | sub set_tags { 32 | my $self = shift ; 33 | my $file = shift ; 34 | my $values = shift ; 35 | 36 | my %field_name_ogg_translations = 37 | ( 38 | 'NUMBER' => 'TRACKNUMBER', 39 | ) ; 40 | my @ogg_tagging_cmd = ( 'vorbiscomment', '-q' ) ; 41 | my @ogg_tagging_clear_option = ( '-w' ) ; 42 | 43 | # apply regular tags 44 | my @regular_tags_args = 45 | ( map { 46 | my $oggname = $_ ; 47 | $oggname = $field_name_ogg_translations{$_} if defined $field_name_ogg_translations{$_} ; 48 | my @tags = (Lltag::Tags::get_tag_value_array $self, $values, $_) ; 49 | map { ( "-t" , $oggname."=".$_ ) } @tags 50 | } @{$self->{field_names}} 51 | ) ; 52 | # apply non-regular tags 53 | my @non_regular_tags_args = 54 | ( map { 55 | my $oggname = $_ ; 56 | my @tags = (Lltag::Tags::get_tag_value_array $self, $values, $_) ; 57 | map { ( "-t" , $oggname."=".$_ ) } @tags 58 | } Lltag::Tags::get_values_non_regular_keys ($self, $values) 59 | ) ; 60 | # work-around vorbiscomment which does not like when tags is passed 61 | my @workaround_args = (scalar @regular_tags_args + @non_regular_tags_args) ? () : ("-c", "/dev/null") ; 62 | 63 | my @system_args 64 | = ( @ogg_tagging_cmd , 65 | # clear all tags 66 | @ogg_tagging_clear_option , 67 | @regular_tags_args , 68 | @non_regular_tags_args , 69 | @workaround_args , 70 | $file ) ; 71 | 72 | Lltag::Tags::set_tags_with_external_prog ($self, @system_args) ; 73 | } 74 | 75 | sub new { 76 | my $self = shift ; 77 | 78 | return undef 79 | if test_vorbiscomment $self ; 80 | 81 | return { 82 | name => "OGG (using vorbiscomment)", 83 | type => "ogg", 84 | extension => "ogg", 85 | read_tags => \&read_tags, 86 | set_tags => \&set_tags, 87 | } ; 88 | } 89 | 90 | 1 ; 91 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME = lltag 2 | ifeq ($(shell [ -d .git ] && echo 1),1) 3 | VERSION = $(shell cat VERSION)+git$(shell date +%Y%m%d).$(shell git show -s --pretty=format:%h) 4 | else 5 | VERSION = $(shell cat VERSION) 6 | endif 7 | 8 | LIB_SUBDIR = lib 9 | DOC_SUBDIR = doc 10 | 11 | DESTDIR = 12 | PREFIX = /usr/local 13 | EXEC_PREFIX = $(PREFIX) 14 | BINDIR = $(EXEC_PREFIX)/bin 15 | DATADIR = $(PREFIX)/share 16 | SYSCONFDIR = $(PREFIX)/etc 17 | MANDIR = $(PREFIX)/man 18 | DOCDIR = $(DATADIR)/doc 19 | PERL_INSTALLDIRS = 20 | 21 | TARBALL = $(NAME)-$(VERSION) 22 | DEBIAN_TARBALL = $(NAME)_$(VERSION).orig 23 | 24 | .PHONY: lltag clean install uninstall tarball 25 | 26 | lltag:: lltag.in VERSION build-lib 27 | sed -e 's!@SYSCONFDIR@!$(DESTDIR)$(SYSCONFDIR)!g' -e 's!@VERSION@!$(DESTDIR)$(VERSION)!g' \ 28 | < lltag.in > lltag 29 | chmod 755 lltag 30 | 31 | clean:: clean-lib 32 | rm -f lltag 33 | 34 | install:: install-lib lltag 35 | install -d -m 0755 $(DESTDIR)$(BINDIR)/ $(DESTDIR)$(SYSCONFDIR)/lltag/ 36 | install -m 0755 lltag $(DESTDIR)$(BINDIR)/lltag 37 | install -m 0644 formats $(DESTDIR)$(SYSCONFDIR)/lltag/ 38 | 39 | uninstall:: uninstall-lib 40 | rm $(DESTDIR)$(BINDIR)/lltag 41 | rm $(DESTDIR)$(SYSCONFDIR)/lltag/formats 42 | rmdir $(DESTDIR)$(SYSCONFDIR)/lltag/ 43 | 44 | tarball:: 45 | mkdir /tmp/$(TARBALL) 46 | cp lltag.in /tmp/$(TARBALL) 47 | cp formats /tmp/$(TARBALL) 48 | cp lltag.1 lltag_config.5 lltag_formats.5 /tmp/$(TARBALL) 49 | cp Makefile /tmp/$(TARBALL) 50 | cp COPYING README VERSION /tmp/$(TARBALL) 51 | cp Changes /tmp/$(TARBALL) 52 | cp -a $(DOC_SUBDIR)/ /tmp/$(TARBALL) 53 | cp -a $(LIB_SUBDIR) /tmp/$(TARBALL) 54 | cd /tmp && tar cfz $(DEBIAN_TARBALL).tar.gz $(TARBALL) 55 | cd /tmp && tar cfj $(TARBALL).tar.bz2 $(TARBALL) 56 | mv /tmp/$(DEBIAN_TARBALL).tar.gz /tmp/$(TARBALL).tar.bz2 .. 57 | rm -rf /tmp/$(TARBALL) 58 | 59 | # Perl modules 60 | .PHONY: build-lib clean-lib install-lib uninstall-lib prepare-lib 61 | 62 | $(LIB_SUBDIR)/Makefile.PL: $(LIB_SUBDIR)/Makefile.PL.in VERSION 63 | sed -e 's!@VERSION@!$(VERSION)!g' < $(LIB_SUBDIR)/Makefile.PL.in > $(LIB_SUBDIR)/Makefile.PL 64 | 65 | $(LIB_SUBDIR)/Makefile: $(LIB_SUBDIR)/Makefile.PL 66 | cd $(LIB_SUBDIR) && perl Makefile.PL INSTALLDIRS=$(PERL_INSTALLDIRS) 67 | 68 | prepare-lib: $(LIB_SUBDIR)/Makefile 69 | 70 | build-lib: prepare-lib 71 | $(MAKE) -C $(LIB_SUBDIR) 72 | 73 | install-lib: prepare-lib 74 | $(MAKE) -C $(LIB_SUBDIR) install PREFIX= SITEPREFIX=$(PREFIX) PERLPREFIX=$(PREFIX) VENDORPREFIX=$(PREFIX) 75 | 76 | clean-lib: prepare-lib 77 | $(MAKE) -C $(LIB_SUBDIR) distclean 78 | rm $(LIB_SUBDIR)/Makefile.PL 79 | 80 | uninstall-lib: prepare-lib 81 | $(MAKE) -C $(LIB_SUBDIR) uninstall 82 | 83 | # Install the doc, only called on-demand by distrib-specific Makefile 84 | .PHONY: install-doc uninstall-doc 85 | 86 | install-doc: 87 | $(MAKE) -C $(DOC_SUBDIR) install DOCDIR=$(DESTDIR)$(DOCDIR) 88 | 89 | uninstall-doc: 90 | $(MAKE) -C $(DOC_SUBDIR) uninstall DOCDIR=$(DESTDIR)$(DOCDIR) 91 | 92 | # Install the manpages, only called on-demand by distrib-specific Makefile 93 | .PHONY: install-man uninstall-man 94 | 95 | install-man:: 96 | install -d -m 0755 $(DESTDIR)$(MANDIR)/man1/ $(DESTDIR)$(MANDIR)/man5/ 97 | install -m 0644 lltag.1 $(DESTDIR)$(MANDIR)/man1/ 98 | install -m 0644 lltag_config.5 $(DESTDIR)$(MANDIR)/man5/ 99 | install -m 0644 lltag_formats.5 $(DESTDIR)$(MANDIR)/man5/ 100 | 101 | uninstall-man:: 102 | rm $(DESTDIR)$(MANDIR)/man1/lltag.1 103 | rm $(DESTDIR)$(MANDIR)/man5/lltag_config.5 104 | rm $(DESTDIR)$(MANDIR)/man5/lltag_formats.5 105 | -------------------------------------------------------------------------------- /lltag_config.5: -------------------------------------------------------------------------------- 1 | .\" Process this file with 2 | .\" groff -man -Tascii foo.1 3 | .\" 4 | .TH lltag_config 5 "NOVEMBER 2006" 5 | 6 | 7 | 8 | .SH NAME 9 | config \- Configuration file for lltag 10 | 11 | 12 | 13 | .SH DESCRIPTION 14 | The following options may be stored in 15 | .I /etc/lltag/config 16 | or the user's 17 | .IR $HOME/.lltag/config , 18 | or in any file passed with 19 | .IR --config . 20 | 21 | Such a configuration file may also be generated with 22 | .IR --gencfg . 23 | 24 | 25 | 26 | .SH Obtaining tags 27 | 28 | .I format = \fI"string" 29 | .RS 30 | Add a user-defined format 31 | .RB [ -R ]. 32 | Might be used multiple times. 33 | Default is to guess if no user-defined formats and no default field values are given. 34 | .RE 35 | .I guess = <0/1> 36 | .RS 37 | Try to guess if user-defined formats do not match 38 | .RB [ -G ]. 39 | Default is 40 | .BR 0 " (" disabled ") when no user-defined formats and no explicit values are given." 41 | .RE 42 | .I tag = 43 | .RS 44 | Add an explicit tag 45 | .RB [ --tag ]. 46 | Might be used multiple times. 47 | .RE 48 | 49 | 50 | .SH Tweaking filename parsing 51 | 52 | .I no_path = <0/1> 53 | .RS 54 | Do not use file path when matching filename 55 | .RB [ -p ]. 56 | Default is 57 | .BR 0 " (" disabled ")." 58 | .RE 59 | .I spaces = <0/1> 60 | .RS 61 | Allow no or multiple spaces 62 | .RB [ --spaces ]. 63 | Default is 64 | .BR 0 " (" disabled ")." 65 | .RE 66 | 67 | 68 | .SH Cleaning obtained tags 69 | 70 | .I edit = <0/1> 71 | .RS 72 | Edit tags immediately 73 | .RB [ --edit ]. 74 | Default is 75 | .BR 0 " (" disabled ")." 76 | .RE 77 | .I maj = <0/1> 78 | .RS 79 | Upcase first letters of words in tags 80 | .RB [ --maj ]. 81 | Default is 82 | .BR 0 " (" disabled ")." 83 | .RE 84 | .I regexp = \fI"s/from/to/" 85 | .RS 86 | Replace \fIfrom\fR with \fIto\fR in all tags. 87 | .I title,number:s/from/to/ 88 | replaces in title and number tags only. 89 | Might be used multiple times 90 | .RB [ --regexp ]. 91 | Default is to not apply any regexp. 92 | .RE 93 | .I sep = \fI"string" 94 | .RS 95 | Replace |-separated strings with space in tags. 96 | Default is to not replace any separator. 97 | .RE 98 | 99 | 100 | .SH Configuration of tag application 101 | 102 | .I type = 103 | .RS 104 | Force mp3, ogg of flac instead of by-extension detection 105 | .RB [ --mp3 ", " --ogg " and " --flac ]. 106 | Default is 107 | .BR none . 108 | .RE 109 | .I clear_tags = <0/1> 110 | .RS 111 | Clear all tags of audio files. 112 | .BR [ --clear ]. 113 | Default is 114 | .BR 0 " (" disabled ")." 115 | .RE 116 | .I append_tags = <0/1> 117 | .RS 118 | Append tags only instead of replacing old ones. 119 | .RB [ --append ]. 120 | Default is 121 | .BR 0 " (" disabled ")." 122 | .RE 123 | .I no_tagging = <0/1> 124 | .RS 125 | Do not actually tag files 126 | .RB [ --no-tagging ]. 127 | Default is 128 | .BR 0 " (" disabled ")." 129 | .RE 130 | .I preserve_time = <0/1> 131 | .RS 132 | Preserve file modification time during tagging 133 | .RB [ --preserve-time ]. 134 | Default is 135 | .BR 0 " (" disabled ")." 136 | .RE 137 | 138 | 139 | .SH Renaming 140 | 141 | .I rename_format = \fI"string" 142 | .RS 143 | Rename file according to format 144 | .RB [ --rename ]. 145 | Default is to not rename. 146 | .RE 147 | .I rename_min = <0/1> 148 | .RS 149 | Lowcase tags before renaming 150 | .RB [ --rename-min ]. 151 | Default is 152 | .BR 0 " (" disabled ")." 153 | .RE 154 | .I rename_regexp = \fI"s/from/to/" 155 | .RS 156 | Replace \fIfrom\fR with \fIto\fR in all tags before renaming. 157 | .I title,number:s/from/to/ 158 | replaces in title and number tags only. 159 | Might be used multiple times 160 | .RB [ --rename-regexp ]. 161 | Default is to not apply any regexp. 162 | .RE 163 | .I rename_sep = \fI"string" 164 | .RS 165 | Replace spaces with a string in tags before renaming 166 | .RB [ --rename-sep ]. 167 | Default is to not replace any separator. 168 | .RE 169 | .I rename_slash = \fI"string" 170 | .RS 171 | Replace slashes with a string in tags before renaming 172 | .RB [ --rename-slash ]. 173 | Default is to replace with a dash. 174 | .RE 175 | .I rename_ext = <0/1> 176 | .RS 177 | Assume the rename format provides an extension 178 | .RB [ --rename-ext ]. 179 | Default is 180 | .BR 0 " (" disabled ")." 181 | .RE 182 | 183 | 184 | .SH Miscellaneous 185 | 186 | .I dry_run = <0/1> 187 | .RS 188 | Do nothing but show what would have been done 189 | .RB [ --dry-run ]. 190 | Default is 191 | .BR 0 " (" disabled ")." 192 | .RE 193 | .I yes = <0/1> 194 | .RS 195 | Tag without asking for confirmation when guessing 196 | and rename without asking for confirmation 197 | .RB [ --yes ]. 198 | Default is 199 | .BR 0 " (" disabled ")." 200 | .RE 201 | .I ask = <0/1> 202 | .RS 203 | Always ask for confirmation before tagging 204 | .RB [ --ask ]. 205 | Default is 206 | .BR 0 " (" disabled ")." 207 | .RE 208 | .I recursive = <0/1> 209 | .RS 210 | Recursively traverse all given subdirectories 211 | .RB [ -R ]. 212 | Default is 213 | .BR 0 " (" disabled ")." 214 | .RE 215 | .I verbose = 216 | .RS 217 | Message verbosity level 218 | .RB [ -v " and " -q ]. 219 | Default is 220 | .BR 0 " (" "only important messages" ")." 221 | Other possible values are 222 | .BR 1 " (" "show usage information when a menu is displayed for the first time" ")" 223 | and 224 | .BR 2 " (" "always show usage information before a menu appears" ")." 225 | .RE 226 | 227 | 228 | .SH CDDB configuration 229 | 230 | .I cddb_server_name = "hostname" 231 | .RS 232 | Change the CDDB server name. 233 | Default is 234 | .BR www.freedb.org . 235 | .RE 236 | .I cddb_server_port = 237 | .RS 238 | Change the CDDB server port. 239 | Default is 240 | .BR 80 " (" HTTP ")." 241 | .RE 242 | 243 | 244 | 245 | .SH SEE ALSO 246 | .PP 247 | .BR lltag (1) 248 | 249 | The 250 | .I config 251 | template file provided within the documentation directory. 252 | 253 | 254 | 255 | .SH AUTHOR 256 | Brice Goglin 257 | -------------------------------------------------------------------------------- /lib/Lltag/MP3.pm: -------------------------------------------------------------------------------- 1 | package Lltag::MP3 ; 2 | 3 | use strict ; 4 | 5 | require Lltag::Tags ; 6 | require Lltag::Misc ; 7 | 8 | sub test_mp3info { 9 | my $self = shift ; 10 | my ($status, @output) = Lltag::Misc::system_with_output ("mp3info", "-h") ; 11 | print "mp3info does not seem to work, disabling 'MP3' backend.\n" 12 | if $status and $self->{verbose_opt} ; 13 | return $status ; 14 | } 15 | 16 | ####################################################### 17 | 18 | # valid ID3v1 genres in mp3info 19 | my @mp3info_genres = ("", # 0 20 | "Blues", "Classic Rock", "Country", "Dance", "Disco", # 5 21 | "Funk", "Grunge", "Hip-Hop", "Jazz", "Metal", # 10 22 | "New Age", "Oldies", "Other", "Pop", "R&B", # 15 23 | "Rap", "Reggae", "Rock", "Techno", "Industrial", # 20 24 | "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack", # 25 25 | "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", # 30 26 | "Fusion", "Trance", "Classical", "Instrumental", "Acid", # 35 27 | "House", "Game", "Sound Clip", "Gospel", "Noise", # 40 28 | "AlternRock", "Bass", "Soul", "Punk", "Space", # 45 29 | "Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", # 50 30 | "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", # 55 31 | "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta Rap", # 60 32 | "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", # 65 33 | "Cabaret", "New Wave", "Psychedelic", "Rave", "Showtunes", # 70 34 | "Trailer", "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", # 75 35 | "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock", # 80 36 | "Folk", "Folk/Rock", "National Folk", "Swing", "Fast-Fusion", # 85 37 | "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", # 90 38 | "Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", # 95 39 | "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", # 100 40 | "Humour", "Speech", "Chanson", "Opera", "Chamber Music", # 105 41 | "Sonata", "Symphony", "Booty Bass", "Primus", "Porn Groove", # 110 42 | "Satire", "Slow Jam", "Club", "Tango", "Samba", # 115 43 | "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", # 120 44 | "Duet", "Punk Rock", "Drum Solo", "A Cappella", "Euro-House", # 125 45 | "Dance Hall", "Goa", "Drum & Bass", "Club-House", "Hardcore", # 130 46 | "Terror", "Indie", "BritPop", "Negerpunk", "Polsk Punk", # 135 47 | "Beat", "Christian Gangsta Rap", "Heavy Metal", "Black Metal", "Crossover", # 140 48 | "Contemporary Christian", "Christian Rock", "Merengue", "Salsa", "Thrash Metal", # 145 49 | "Anime", "JPop", "Synthpop", 50 | ) ; 51 | 52 | sub check_mp3info_genre { 53 | my $genre = shift ; 54 | return scalar ( grep { lc($genre) eq lc($_) } @mp3info_genres ) ; 55 | } 56 | 57 | sub check_id3v1_tracknumber { 58 | my $number = shift ; 59 | return ( $number =~ m/^\d+$/ and $number <= 255 ) ; 60 | } 61 | 62 | sub fix_values_for_mp3info { 63 | my $self = shift ; 64 | my $values = shift ; 65 | 66 | # only regular fields are supported 67 | foreach my $field (Lltag::Tags::get_values_non_regular_keys ($self, $values)) { 68 | Lltag::Misc::print_warning (" ", "Cannot set $field in MP3 ID3v1 tags") ; 69 | delete $values->{$field} ; 70 | } 71 | 72 | # remove unsupported genres and keep a single value 73 | my @supported_genres = () ; 74 | foreach my $genre (Lltag::Tags::get_tag_value_array ($self, $values, 'GENRE')) { 75 | if (check_mp3info_genre $genre) { 76 | push @supported_genres, $genre ; 77 | } else { 78 | Lltag::Misc::print_warning (" ", "Genre $genre is not supported in ID3v1 MP3 tags") ; 79 | } 80 | } 81 | delete $values->{GENRE} ; 82 | if (@supported_genres > 1) { 83 | @{$values->{GENRE}}= @supported_genres ; 84 | } elsif (@supported_genres == 1) { 85 | $values->{GENRE} = $supported_genres[0] ; 86 | } 87 | 88 | # remove unsupported tracknumbers and keep a single value 89 | my @supported_numbers = () ; 90 | foreach my $number (Lltag::Tags::get_tag_value_array ($self, $values, 'NUMBER')) { 91 | if (check_id3v1_tracknumber $number) { 92 | push @supported_numbers, $number ; 93 | } else { 94 | Lltag::Misc::print_warning (" ", "Track number $number is not supported in ID3v1 MP3 tags") ; 95 | } 96 | } 97 | delete $values->{NUMBER} ; 98 | if (@supported_numbers > 1) { 99 | @{$values->{NUMBER}} = @supported_numbers ; 100 | } elsif (@supported_numbers == 1) { 101 | $values->{NUMBER} = $supported_numbers[0] ; 102 | } 103 | 104 | # keep a single value 105 | foreach my $field (keys %{$values}) { 106 | if (ref($values->{$field}) eq 'ARRAY') { 107 | my $val = Lltag::Tags::get_tag_unique_value ($self, $values, $field) ; 108 | delete $values->{$field} ; 109 | $values->{$field} = $val ; 110 | Lltag::Misc::print_warning (" ", "Multiple $field values not supported in ID3v1 MP3 tags, keeping only $val.") ; 111 | } 112 | } 113 | } 114 | 115 | ####################################################### 116 | 117 | sub read_tags { 118 | my $self = shift ; 119 | my $file = shift ; 120 | my ($status, @output) = Lltag::Misc::system_with_output 121 | ("mp3info", "-p", "ARTIST=%a\nALBUM=%l\nTITLE=%t\nNUMBER=%n\nGENRE=%g\nDATE=%y\nCOMMENT=%c\n", $file) ; 122 | return undef 123 | if $status ; 124 | return Lltag::Tags::convert_tag_stream_to_values ($self, @output) ; 125 | } 126 | 127 | sub set_tags { 128 | my $self = shift ; 129 | my $file = shift ; 130 | my $values = shift ; 131 | my %field_name_mp3info_option = 132 | ( 133 | 'ARTIST' => 'a', 134 | 'TITLE' => 't', 135 | 'ALBUM' => 'l', 136 | 'NUMBER' => 'n', 137 | 'GENRE' => 'g', 138 | 'DATE' => 'y', 139 | 'COMMENT' => 'c' 140 | ) ; 141 | my @mp3_tagging_cmd = ( 'mp3info' ) ; 142 | my @mp3_tagging_clear_option = map { ( "-$_" , "" ) } (values %field_name_mp3info_option) ; 143 | 144 | fix_values_for_mp3info $self, $values ; 145 | 146 | my @system_args 147 | = ( @mp3_tagging_cmd , 148 | # clear all tags 149 | @mp3_tagging_clear_option , 150 | # apply new tags 151 | ( map { 152 | ( "-".$field_name_mp3info_option{$_} , $values->{$_} ) 153 | } (keys %{$values}) 154 | ), 155 | $file ) ; 156 | 157 | Lltag::Tags::set_tags_with_external_prog ($self, @system_args) ; 158 | } 159 | 160 | sub new { 161 | my $self = shift ; 162 | 163 | return undef 164 | if test_mp3info $self ; 165 | 166 | return { 167 | name => "MP3 (using mp3info)", 168 | type => "mp3", 169 | extension => "mp3", 170 | read_tags => \&read_tags, 171 | set_tags => \&set_tags, 172 | } ; 173 | } 174 | 175 | 1 ; 176 | -------------------------------------------------------------------------------- /lib/Lltag/Misc.pm: -------------------------------------------------------------------------------- 1 | package Lltag::Misc ; 2 | 3 | use strict ; 4 | 5 | use Term::ReadLine; 6 | use Term::ANSIColor ; 7 | 8 | # are we running in a normal terminal ? if not, disable readline, colors and bold/underline formatting 9 | my $stdio_is_a_tty = 0 ; 10 | 11 | ################################################################### 12 | # rewrite of system which returns a descriptor of a stream 13 | # containing both stdout and stderr 14 | sub system_with_output { 15 | pipe (my $pipe_out, my $pipe_in) ; 16 | my $pid = fork() ; 17 | if ($pid < 0) { 18 | # in the father, when fork failed 19 | close $pipe_in ; 20 | close $pipe_out ; 21 | return (-1, "Failed to fork to execute command line: ". join (" ", @_) ."\n") ; 22 | } elsif ($pid > 0) { 23 | # in the father, when fork done 24 | close $pipe_in ; 25 | waitpid($pid, 0); 26 | my $status = $? >> 8 ; 27 | $status = -1 28 | if $status == 255 ; 29 | my @lines = <$pipe_out> ; 30 | close $pipe_out ; 31 | return ( $status , @lines ) ; 32 | } else { 33 | # in the child 34 | close $pipe_out ; 35 | open STDERR, ">&", $pipe_in ; 36 | open STDOUT, ">&", $pipe_in ; 37 | { exec @_ } ; 38 | print $pipe_in "Failed to execute command line: ". join (" ", @_) ."\n" ; 39 | print $pipe_in "Please install $_[0] properly (see README).\n" 40 | if $!{ENOENT} or $!{EPERM} ; 41 | close $pipe_in ; 42 | exit -1 ; 43 | } 44 | } 45 | 46 | ################################################################### 47 | # configure readline depending on the features provided by the installation 48 | my $term ; 49 | my $attribs ; 50 | my $readline_firsttime ; 51 | my $myreadline ; 52 | my $history_dir ; 53 | my $history_file ; 54 | 55 | # dumb readline replacement 56 | sub dummy_readline { 57 | my $indent = shift ; 58 | my $prompt = shift ; 59 | my $preput = shift ; 60 | my $clear_allowed = shift ; 61 | # 0 = no clearing, 1 = clearing allowed and documented, -1 = clearing allowed (normal behavior) 62 | $preput = "" if not defined $preput ; 63 | if ($readline_firsttime) { 64 | print $indent."You might want to install an advanced Perl readline module such as 'Term::ReadLine::GNU'.\n" ; 65 | print $indent."The current value is given in parenthesis, to keep it" 66 | . ($clear_allowed>0 ? ", to clear it" : "") 67 | . ".\n" ; 68 | $readline_firsttime = 0 ; 69 | } 70 | ASK: 71 | my $val = $term->readline ("$indent$prompt".($preput ? " ($preput)" : "")." ? ") ; 72 | return $preput if !$val ; 73 | $val = "" if $val eq "CLEAR" or $val eq "" ; 74 | if (!$val and !$clear_allowed) { 75 | print "$indent Clearing is not allowed here.\n" ; 76 | goto ASK ; 77 | } 78 | print "\n" unless defined $val ; 79 | return $val ; 80 | } 81 | 82 | # true readline wrapper 83 | sub real_readline { 84 | my $indent = shift ; 85 | my $prompt = shift ; 86 | my $preput = shift ; 87 | my $clear_allowed = shift ; 88 | $preput = "" if not defined $preput ; 89 | ASK: 90 | my $val = $term->readline ("$indent$prompt ? ", $preput) ; 91 | if (!$val and !$clear_allowed) { 92 | print "$indent Clearing is not allowed here.\n" ; 93 | goto ASK ; 94 | } 95 | print "\n" unless defined $val ; 96 | return $val ; 97 | } 98 | 99 | # the actual wrapper 100 | sub readline { 101 | die "ERROR: Interactive mode not available in this environment.\n" unless $stdio_is_a_tty ; 102 | return &$myreadline (@_) ; 103 | } 104 | 105 | # initialization 106 | sub init_readline { 107 | my $self = shift ; 108 | $history_dir = $self->{user_lltag_dir} ; 109 | $history_file = $self->{lltag_edit_history_filename} ; 110 | 111 | # detect whether readline works 112 | eval { 113 | my ($IN,$OUT) = Term::ReadLine->findConsole(); 114 | open IN, "<$IN" || die "Cannot open $IN for read\n"; close IN ; 115 | open OUT, ">$OUT" || die "Cannot open $OUT for write\n"; close OUT ; 116 | } or return ; 117 | $stdio_is_a_tty = 1 ; 118 | 119 | $term = Term::ReadLine->new('lltag editor') ; 120 | $attribs = $term->Attribs ; 121 | $term->ornaments('md,me,,') ; 122 | $readline_firsttime = 1 ; 123 | 124 | # read the history file 125 | eval { 126 | if (-f $history_dir."/".$history_file) { 127 | $term->ReadHistory ($history_dir."/".$history_file) 128 | or warn "Failed to open history file $history_dir/$history_file: $!\n" ; 129 | } 130 | } unless $term->Features->{ReadHistory} ; 131 | 132 | if ($term->Features->{preput}) { 133 | $myreadline = \&real_readline ; 134 | $term->MinLine(3) ; 135 | } else { 136 | $myreadline = \&dummy_readline ; 137 | } 138 | } 139 | 140 | # exit, saves readline history if supported by the installation 141 | sub exit_readline { 142 | return unless $stdio_is_a_tty ; 143 | 144 | # only keep the last 100 entries 145 | eval { 146 | $term->StifleHistory (100); 147 | } unless $term->Features->{StifleHistory} ; 148 | 149 | # save the history file 150 | eval { 151 | if (!-d $history_dir."/") { 152 | mkdir $history_dir 153 | or warn "Failed to create $history_dir directory to store the history file: $!.\n" ; 154 | } 155 | $term->WriteHistory ($history_dir."/".$history_file) 156 | or warn "Failed to write history file $history_dir/$history_file: $!.\n" ; 157 | } unless $term->Features->{WriteHistory} ; 158 | } 159 | 160 | ################################################################### 161 | # Print a usage header in underlined 162 | 163 | sub print_usage_header { 164 | print shift ; 165 | print color 'underline' if $stdio_is_a_tty; 166 | print shift ; 167 | print " - Usage:" ; 168 | print color 'reset' if $stdio_is_a_tty ; 169 | print "\n" ; 170 | } 171 | 172 | ################################################################### 173 | # Print a notice or a warning in underlined 174 | 175 | sub print_notice { 176 | print shift ; 177 | print color 'underline' if $stdio_is_a_tty ; 178 | print "NOTICE:" ; 179 | print color 'reset' if $stdio_is_a_tty ; 180 | print " ".(shift)."\n" ; 181 | } 182 | 183 | sub print_warning { 184 | print shift ; 185 | print color 'underline' if $stdio_is_a_tty ; 186 | print "WARNING:" ; 187 | print color 'reset' if $stdio_is_a_tty ; 188 | print " ".(shift)."\n" ; 189 | } 190 | 191 | ################################################################### 192 | # Print an error in underlined and bold 193 | 194 | sub format_error { 195 | if ($stdio_is_a_tty) { 196 | return (color 'bold').(color 'underline')."ERROR:".(color 'reset')." " 197 | .(color 'bold').(shift).(color 'reset') ; 198 | } else { 199 | return "ERROR: ".shift unless $stdio_is_a_tty ; 200 | } 201 | } 202 | 203 | sub print_error { 204 | print shift ; 205 | print ((format_error(shift))."\n") ; 206 | } 207 | 208 | sub die_error { 209 | print ((format_error(shift))."\n") ; 210 | exit_readline () ; 211 | exit -1 ; 212 | } 213 | 214 | 1 ; 215 | -------------------------------------------------------------------------------- /lib/Lltag/Rename.pm: -------------------------------------------------------------------------------- 1 | package Lltag::Rename ; 2 | 3 | use strict ; 4 | 5 | use Lltag::Misc ; 6 | 7 | # constants for rename format specific letters 8 | use constant DIRNAME_LETTER => "P" ; 9 | use constant BASENAME_LETTER => "F" ; 10 | use constant EXTENSION_LETTER => "E" ; 11 | 12 | # confirmation behavior 13 | my $current_rename_yes_opt ; 14 | 15 | ####################################################### 16 | # rename specific usage 17 | 18 | sub rename_usage { 19 | my $self = shift ; 20 | print " Renaming options:\n" ; 21 | print " --rename Rename file according to format\n" ; 22 | print " --rename-min Lowcase tags before renaming\n" ; 23 | print " --rename-sep Replace spaces with s in tags before renaming\n" ; 24 | print " --rename-slash Replace slashes with s in tags before renaming\n" ; 25 | print " --rename-regexp Apply a replace regexp to tags before renaming\n" ; 26 | print " --rename-ext Assume the rename format provides an extension\n" ; 27 | } 28 | 29 | ####################################################### 30 | # rename format specific usage 31 | 32 | sub rename_format_usage { 33 | my $self = shift ; 34 | print " %".BASENAME_LETTER." means the original basename of the file\n" ; 35 | print " %".EXTENSION_LETTER." means the original extension of the file\n" ; 36 | print " %".DIRNAME_LETTER." means the original path of the file\n" ; 37 | } 38 | 39 | ####################################################### 40 | # init 41 | 42 | my $rename_confirm_usage_forced ; 43 | 44 | sub init_renaming { 45 | my $self = shift ; 46 | 47 | # default confirmation behavior 48 | $current_rename_yes_opt = $self->{yes_opt} ; 49 | 50 | # need to show menu usage once ? 51 | $rename_confirm_usage_forced = $self->{menu_usage_once_opt} ; 52 | } 53 | 54 | ####################################################### 55 | # rename confirmation 56 | 57 | sub rename_confirm_usage { 58 | Lltag::Misc::print_usage_header (" ", "Renaming files") ; 59 | print " y => Yes, rename this file (default)\n" ; 60 | print " a => Always rename without asking\n" ; 61 | print " e => Edit the filename before tagging\n" ; 62 | print " n/q => No, don't rename this file\n" ; 63 | print " h => Show this help\n" ; 64 | $rename_confirm_usage_forced = 0 ; 65 | } 66 | 67 | ####################################################### 68 | # main rename routine 69 | 70 | sub rename_with_values { 71 | my $self = shift ; 72 | my $file = shift ; 73 | my $extension = shift ; 74 | my $values = shift ; 75 | 76 | my $rename_values = {} ; 77 | my $undefined = 0 ; 78 | 79 | print " Renaming with format '$self->{rename_opt}'...\n" ; 80 | 81 | # make sure we find tags through their upcase names 82 | my $ucvalues = Lltag::Tags::clone_tag_values_uc ($self, $values) ; 83 | 84 | foreach my $field (keys %{$ucvalues}) { 85 | # use the first tag for renaming 86 | my $val = Lltag::Tags::get_tag_unique_value ($self, $ucvalues, $field) ; 87 | $val = lc ($val) 88 | if $self->{rename_min_opt} ; 89 | $val =~ s/ /$self->{rename_sep_opt}/g 90 | if $self->{rename_sep_opt} ; 91 | $val =~ s/\//$self->{rename_slash_opt}/g ; 92 | map { $val = Lltag::Tags::apply_regexp_to_tag ($val, $_, $field) } @{$self->{rename_regexp_opts}} ; 93 | $rename_values->{$field} = $val ; 94 | } 95 | 96 | my $format_string = $self->{rename_opt} ; 97 | my @array = split(//, $format_string) ; 98 | for(my $i = 0; $i < @array - 1; $i++) { 99 | 100 | # normal characters 101 | next if $array[$i] ne "%" ; 102 | 103 | # remove % and check next char 104 | splice (@array, $i, 1) ; 105 | # replace the char with the matching 106 | my $char = $array[$i] ; 107 | next if $char eq "%" ; 108 | if ($char =~ m/$self->{field_letters_union}/) { 109 | my $field = $self->{field_letter_name}{$char} ; 110 | my $val = $rename_values->{$field} ; 111 | # rename does not contain an array anymore 112 | if (not defined $val) { 113 | $undefined++ ; 114 | Lltag::Misc::print_warning (" ", "Undefined field '".$field."'") ; 115 | $val = "" ; 116 | } 117 | if ($char eq 'n') { 118 | # initialize track number to 0 if empty 119 | $val = "0" if !$val ; 120 | # make it at least 2 digits 121 | $val = '0'.$val if $val < 10 and length $val < 2 ; 122 | } 123 | $array[$i] = $val ; 124 | 125 | } elsif ($char eq BASENAME_LETTER) { 126 | my $basename ; 127 | if ($file =~ m@([^/]+)\.[^./]+$@) { 128 | $basename = $1 ; 129 | } elsif ($file =~ m@([^/]+)$@) { 130 | $basename = $1 ; 131 | } else { 132 | $basename = $file ; 133 | } 134 | $array[$i] = $basename ; 135 | 136 | } elsif ($char eq EXTENSION_LETTER) { 137 | my $extension ; 138 | if ($file =~ m@\.([^./]+)$@) { 139 | $extension = $1 ; 140 | } else { 141 | $extension = "" ; 142 | } 143 | $array[$i] = $extension ; 144 | 145 | } elsif ($char eq DIRNAME_LETTER) { 146 | my $path ; 147 | if ($file =~ m@^(.*/)[^/]+@) { 148 | $path = $1 ; 149 | } else { 150 | $path = "" ; 151 | } 152 | $array[$i] = $1 ; 153 | 154 | } else { 155 | $array[$i] = "%".$char ; 156 | } 157 | } 158 | 159 | my $new_name = join ("", @array) ; 160 | 161 | $new_name .= ".". $extension 162 | unless $self->{rename_ext_opt} ; 163 | 164 | print " New filename is '$new_name'\n" ; 165 | 166 | # confirm if required or if any field undefined 167 | if ($undefined or !$current_rename_yes_opt) { 168 | 169 | rename_confirm_usage 170 | if $rename_confirm_usage_forced ; 171 | 172 | ASK_CONFIRM: 173 | my $reply = Lltag::Misc::readline (" ", "Really rename the file [yaeq] (default is yes, h for help)", "", -1) ; 174 | 175 | # if ctrl-d, do not rename 176 | $reply = 'q' unless defined $reply ; 177 | 178 | if ($reply eq "" or $reply =~ m/^y/i) { 179 | goto RENAME_IT ; 180 | 181 | } elsif ($reply =~ m/^a/) { 182 | $current_rename_yes_opt = 1 ; 183 | goto RENAME_IT ; 184 | 185 | } elsif ($reply =~ m/^n/ or $reply =~ m/^q/) { 186 | return ; 187 | 188 | } elsif ($reply =~ m/^e/) { 189 | my $newnew_name = Lltag::Misc::readline (" ", "New filename", $new_name, 0) ; 190 | 191 | # if ctrl-d, keep same filename 192 | $new_name = $newnew_name if defined $newnew_name ; 193 | 194 | goto ASK_CONFIRM ; 195 | 196 | } else { 197 | rename_confirm_usage ; 198 | goto ASK_CONFIRM ; 199 | } 200 | } 201 | 202 | RENAME_IT: 203 | if ($new_name eq $file) { 204 | print " Filename would not change, not renaming\n" ; 205 | return ; 206 | } 207 | 208 | if (-e $new_name) { 209 | print " File $new_name already exists, not renaming\n" ; 210 | return ; 211 | } 212 | 213 | return 214 | if $self->{dry_run_opt} ; 215 | 216 | my $remain = $new_name ; 217 | my $path = '' ; 218 | while ($remain =~ m@^([^/]*/+)(.*)$@) { 219 | $path .= $1 ; 220 | $remain = $2 ; 221 | if (!-d $path) { 222 | print " Creating directory '$path'\n" ; 223 | if (!mkdir $path) { 224 | Lltag::Misc::print_error (" ", "Failed to create directory ($!).") ; 225 | return ; 226 | } 227 | } 228 | } 229 | 230 | print " Renaming.\n" ; 231 | rename $file, $new_name 232 | or Lltag::Misc::print_error (" ", "Failed to rename ($!).") ; 233 | } 234 | 235 | 1 ; 236 | -------------------------------------------------------------------------------- /lib/Lltag/MP3v2.pm: -------------------------------------------------------------------------------- 1 | package Lltag::MP3v2 ; 2 | 3 | use strict ; 4 | 5 | require Lltag::Tags ; 6 | require Lltag::Misc ; 7 | 8 | use constant MP3V2_READ_V1 => 1 ; 9 | use constant MP3V2_READ_V2 => 2 ; 10 | use constant MP3V2_READ_V1_V2 => 12 ; 11 | use constant MP3V2_READ_V2_V1 => 21 ; 12 | 13 | sub test_MP3Tag { 14 | my $self = shift ; 15 | if (not eval { require MP3::Tag ; } ) { 16 | print "MP3::Tag does not seem to be available, disabling 'MP3v2' backend.\n" 17 | if $self->{verbose_opt} ; 18 | return -1 ; 19 | } 20 | return 0 ; 21 | } 22 | 23 | ################################################# 24 | # Convert v1 tag to lltag tag name, keep a unique non-null one 25 | 26 | sub read_v1_tag { 27 | my $self = shift ; 28 | my $values = shift ; 29 | my $v1_field = shift ; 30 | my $value = shift ; 31 | 32 | # not needed 33 | return if $v1_field eq 'parent' or $v1_field eq 'mp3' ; 34 | 35 | # ignore for now, use when we have the list of genres 36 | return if $v1_field eq 'genreID' ; 37 | 38 | # translate into common field names 39 | my $field = uc($v1_field) ; 40 | $field =~ s/YEAR/DATE/ ; 41 | $field =~ s/TRACK/NUMBER/ ; 42 | 43 | if (grep { $_ =~ $field } @{$self->{field_names}}) { 44 | if (exists $values->{$field}) { 45 | Lltag::Misc::print_warning (" ", "Duplicated MP3v1 tag '$field', overwriting.") ; 46 | } 47 | $values->{$field} = $value 48 | if $value ; 49 | } else { 50 | Lltag::Misc::print_warning (" ", "Unrecognized MP3v1 tag '$v1_field', ignoring.") ; 51 | } 52 | } 53 | 54 | ################################################# 55 | # Convert v2 tag to lltag tag name, append all non-null ones 56 | 57 | sub read_v2_tag { 58 | my $self = shift ; 59 | my $values = shift ; 60 | my $v2_field = shift ; 61 | my $value = shift ; 62 | 63 | # TODO: restore them too ? 64 | return if $v2_field eq "Comments -> Description" 65 | or $v2_field eq "Comments -> encoding" 66 | or $v2_field eq "Comments -> Language" ; 67 | 68 | my %v2_field_name_translations = 69 | ( 70 | "Lead performer(s)/Soloist(s)" => "ARTIST", 71 | "Title/songname/content description" => "TITLE", 72 | "Album/Movie/Show title" => "ALBUM", 73 | "Track number/Position in set" => "NUMBER", 74 | "Content type" => "GENRE", 75 | "Year" => "DATE", 76 | "Comments -> Text" => "COMMENT", 77 | ) ; 78 | 79 | # translate into common field names 80 | my $field ; 81 | if (exists $v2_field_name_translations{$v2_field}) { 82 | $field = $v2_field_name_translations{$v2_field} ; 83 | } else { 84 | $field = uc($v2_field) ; 85 | } 86 | 87 | # remove the track total from the track number to avoid renaming problems with slashes or so 88 | if ($field eq "NUMBER") { 89 | if ($value =~ /^(.\d+)/) { 90 | $value = $1 ; 91 | } else { 92 | return ; 93 | } 94 | } 95 | 96 | Lltag::Tags::append_tag_multiple_value ($self, $values, $field, $value) ; 97 | } 98 | 99 | ################################################# 100 | # Merge v1 and v2 tags, and deal with conflicts 101 | 102 | sub merge_v1_v2_tags { 103 | my $self = shift ; 104 | my $v1_values = shift ; 105 | my $v2_values = shift ; 106 | 107 | if ($self->{mp3v2_read_opt} eq MP3V2_READ_V1_V2) { 108 | print " Merging MP3 v1 and v2 tags...\n" 109 | if $self->{verbose_opt} ; 110 | # we should append v2 to v1 below 111 | # switch v1 and v2, so that we can append v1 to v2 below 112 | my $tmp = $v1_values ; 113 | $v1_values = $v2_values ; 114 | $v2_values = $tmp ; 115 | } else { 116 | print " Merging MP3 v2 and v1 tags...\n" 117 | if $self->{verbose_opt} ; 118 | } 119 | 120 | # append v1 to v2 121 | foreach my $field (keys %{$v1_values}) { 122 | Lltag::Tags::append_tag_multiple_value ($self, $v2_values, $field, $v1_values->{$field}) ; 123 | } 124 | 125 | return $v2_values ; 126 | } 127 | 128 | ################################################# 129 | # Read both v1 and v2 if they exist and return their merge 130 | 131 | sub read_tags { 132 | my $self = shift ; 133 | my $file = shift ; 134 | 135 | my $mp3 = MP3::Tag->new ($file) ; 136 | $mp3->get_tags(); 137 | 138 | # Extract ID3v2 first, if it exists 139 | my $v2_values = undef ; 140 | if ($self->{mp3v2_read_opt} ne MP3V2_READ_V1 141 | and exists $mp3->{ID3v2}) { 142 | $v2_values = {} ; 143 | print " Found a MP3 v2 tag, reading it...\n" 144 | if $self->{verbose_opt} ; 145 | my $id3v2 = $mp3->{ID3v2} ; 146 | my $frameIDs_hash = $id3v2->get_frame_ids('truename'); 147 | foreach my $frame (keys %$frameIDs_hash) { 148 | # drop private frames 149 | next if $frame eq "PRIV" ; 150 | 151 | my ($info, $name, @infos) = $id3v2->get_frame($frame); 152 | unshift @infos, $info ; 153 | foreach $info (@infos) { 154 | if (ref $info) { 155 | while(my ($key, $value) = each %$info) { 156 | read_v2_tag $self, $v2_values, "$name -> $key", $value ; 157 | } 158 | } else { 159 | read_v2_tag $self, $v2_values, $name, $info ; 160 | } 161 | } 162 | } 163 | } 164 | 165 | # Extract ID3v1 last, if it exists, since v2 is generally preferred 166 | my $v1_values = undef ; 167 | if ($self->{mp3v2_read_opt} ne MP3V2_READ_V2 168 | and exists $mp3->{ID3v1}) { 169 | $v1_values = {} ; 170 | print " Found a MP3 v1 tag, reading it...\n" 171 | if $self->{verbose_opt} ; 172 | my $id3v1 = $mp3->{ID3v1} ; 173 | map { read_v1_tag $self, $v1_values, $_, $id3v1->{$_} } (keys %{$id3v1}) ; 174 | } 175 | 176 | return $v2_values unless defined $v1_values ; 177 | return $v1_values unless defined $v2_values ; 178 | return merge_v1_v2_tags $self, $v1_values, $v2_values ; 179 | } 180 | 181 | ################################################# 182 | # Set tags 183 | 184 | sub set_one_v2_tag { 185 | my $id3v2 = shift ; 186 | my $value = shift ; 187 | my @frame_args = @_ ; 188 | 189 | if (ref($value) eq 'ARRAY') { 190 | foreach my $val (@{$value}) { 191 | $id3v2->add_frame(@_, $val) ; 192 | } 193 | } else { 194 | $id3v2->add_frame(@_, $value) ; 195 | } 196 | } 197 | 198 | sub set_tags { 199 | my $self = shift ; 200 | my $file = shift ; 201 | my $values = shift ; 202 | 203 | # TODO: dry-run 204 | # TODO: disable v1 or v2 ? 205 | 206 | my $mp3 = MP3::Tag->new ($file) ; 207 | $mp3->get_tags(); 208 | 209 | # clear existing tags 210 | if (exists $mp3->{ID3v1}) { 211 | $mp3->{ID3v1}->remove_tag ; 212 | } 213 | if (exists $mp3->{ID3v2}) { 214 | $mp3->{ID3v2}->remove_tag ; 215 | } 216 | 217 | # add a new v1 tag 218 | my $id3v1 = $mp3->new_tag("ID3v1"); 219 | 220 | map { 221 | # warning about unknown v1 tag in verbose mode only, since v2 tag will be ok 222 | Lltag::Misc::print_warning (" ", "Cannot set $_ in mp3v1 tags") 223 | if $self->{verbose_opt} ; 224 | } (Lltag::Tags::get_values_non_regular_keys ($self, $values)) ; 225 | 226 | map { 227 | # only one tag is allowed in v1, use the first one 228 | my $value = Lltag::Tags::get_tag_unique_value ($self, $values, $_) ; 229 | # convert to MP3v1 tag name 230 | my $field = lc($_) ; 231 | $field =~ s/date/year/ ; 232 | $field =~ s/number/track/ ; 233 | # set tag 234 | $id3v1->$field ($value) ; 235 | } ( grep { defined $values->{$_} } @{$self->{field_names}} ) ; 236 | # commit changes 237 | $id3v1->write_tag () ; 238 | 239 | # add a new v2 tag 240 | my $id3v2 = $mp3->new_tag("ID3v2"); 241 | my %v2_frame_name_translations = 242 | ( 243 | "ARTIST" => "TPE1", 244 | "TITLE" => "TIT2", 245 | "ALBUM" => "TALB", 246 | "NUMBER" => "TRCK", 247 | "GENRE" => "TCON", 248 | "DATE" => "TYER", 249 | ) ; 250 | map { 251 | my $field = $_ ; 252 | my $frame ; 253 | 254 | if (exists $v2_frame_name_translations{$field}) { 255 | set_one_v2_tag $id3v2, $values->{$field}, $v2_frame_name_translations{$field} ; 256 | 257 | } elsif ($field eq "COMMENT") { 258 | set_one_v2_tag $id3v2, $values->{$field}, "COMM", "", "" ; 259 | 260 | } else { 261 | # FIXME: set other fields as comments ? 262 | print "Cannot set $field in MP3 ID3v2 tags\n" ; 263 | } 264 | } (keys %{$values}) ; 265 | # commit changes 266 | $id3v2->write_tag () ; 267 | 268 | } 269 | 270 | ################################################# 271 | # Initialization 272 | 273 | sub new { 274 | my $self = shift ; 275 | 276 | return undef 277 | if test_MP3Tag $self ; 278 | 279 | return { 280 | name => "MP3v2 (using MP3::Tag)", 281 | type => "mp3", 282 | extension => "mp3", 283 | read_tags => \&read_tags, 284 | set_tags => \&set_tags, 285 | } ; 286 | } 287 | 288 | 1 ; 289 | -------------------------------------------------------------------------------- /doc/howto.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | lltag - Automatic command-line mp3/ogg/flac file tagger and renamer - How-To? 7 | 8 | 9 | 10 | 11 |

lltag How-To?

12 | 13 |
14 | 15 | 16 | 17 |

Basics

18 | 19 | 20 | 21 |

How do I use lltag?

22 | 23 |
 24 |   lltag myfile1.mp3 myfile2.ogg
 25 | 
26 | 27 | It is also possible to tag all files in subdirectories with: 28 |
 29 |   lltag -R mymusic/
 30 | 
31 | 32 | All mp3, ogg and flac are processed, depending on their extension. 33 | If the extension is wrong, it is possible to force the file processing type: 34 |
 35 |   lltag --ogg myfile.mp3
 36 | 
37 | 38 | 39 | 40 |

How do I see current tags in files?

41 | 42 | All the tags currently existing in files may be displayed: 43 |
 44 |   lltag -S *
 45 | 
46 | 47 | It is also possible to only display some tags: 48 |
 49 |   lltag --show-tags artist,album,title,number *
 50 | 
51 | 52 | 53 | 54 |

How do I set tags by hand?

55 | 56 | Any tag may be changed by hand: 57 |
 58 |   lltag --artist "myartist" --album "myalbum" --genre "rock" --comment="very cool" *.mp3
 59 | 
60 | 61 | File types that support random tag names (ogg and flac currently) may also get random tags: 62 |
 63 |   lltag --tag foo=nil bar.ogg
 64 | 
65 | 66 | It is also possible to clear a tag: 67 |
 68 |   lltag --tag foo= bar.ogg
 69 | 
70 | 71 | And all tags may be removed from all files: 72 |
 73 |   lltag --clear *.flac
 74 | 
75 | 76 | 77 | 78 |

Disabling, Confirming, Forcing

79 | 80 | 81 | 82 |

How do I try lltag without actually tagging?

83 | 84 |
 85 |   lltag --dry-run
 86 | 
87 | 88 | 89 | 90 |

How do I change what confirmation lltag wants?

91 | 92 | It is possible to remove any need for confirmation when no problem occurs: 93 |
 94 |   lltag --yes myfile.flac
 95 | 
96 | 97 | It is also possible to force confirmation when lltag parses the 98 | filename with user-provided formats: 99 |
100 |   lltag --ask myfile.ogg
101 | 
102 | 103 | Finally, during confirmation, it is possible to use Always to enable 104 | --yes at runtime. 105 | 106 | 107 | 108 |

Parsing

109 | 110 | 111 | 112 |

How do I see how lltag parses filename?

113 | 114 | lltag uses internal format that are defined in /etc/lltag/formats/. 115 | You might see them with: 116 |
117 |   lltag -L
118 | 
119 | 120 | 121 | 122 |

How do I force lltag to use another format?

123 | 124 | If internal formats do not fit the way your filename is written, you may pass 125 | additional formats with: 126 |
127 |   lltag -F "%a-%t" -F "%n)%a-%t" myfile1.mp3 myfile2.flac
128 | 
129 | 130 | It is also possible to try user-provided formats and then try internal formats: 131 |
132 |   lltag -F "%a-%t" -F "%n)%a-%t" -G myfile1.mp3 myfile2.flac
133 | 
134 | 135 | 136 | 137 |

What is the preferred parser?

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

Getting tags from the online CDDB

154 | 155 | 156 | 157 |

What is CDDB and why does lltag need it?

158 | 159 | CDDB is a huge online database of CD describing their artist, album, date, 160 | tracks, ... everything that lltag considers as tags. 161 | 162 | By default, lltag accesses the database on www.freedb.org 163 | through the HTTP protocol. The server and its port may be changed on the 164 | command-line: 165 |
166 |   lltag --cddb --cddb-server mycddb.mydomain.org:443 *
167 | 
168 | 169 | In case of a HTTP proxy being required for HTTP request, lltag 170 | may also be configured to use it: 171 |
172 |   lltag --cddb --cddb-proxy myhttpproxy.mydomain.org:3128 *
173 | 
174 | 175 | 176 | 177 |

How do get tags from CDDB?

178 | 179 | The common way to search a CD on CDDB is to pass keywords, which return a list 180 | of matching CDs. Then, you choose a CD in the list, and a track in the CD tracks. 181 |
182 | 183 | When entering the lltag CDDB menu for the first time, you need to 184 | enter keywords to find the CD that match the audio file you are processing. 185 | lltag will return a list of CD. At this point, you may either go back 186 | to search with other keywords, or enter the index of the CD that you want. 187 |
188 | 189 | Then, lltag will display of the CD you have chosen and of its tracks. 190 | You may then choose a track and use it to tag/rename your file, or go back to 191 | choose another CD, or go back more to change the keywords. 192 |
193 | 194 | When coming back to CDDB the next time, lltag will propose the next 195 | track of the previous CD that you used, so that processing an entire album 196 | is quick and easy. 197 | 198 | 199 | 200 |

How do choose a precise CD in CDDB?

201 | 202 | All CD that are registered in CDDB are identified by a category and an 203 | hexadecimal identifier. When searching by keywords, lltag will 204 | return a list of matching CD with their category/identifier in parentheses. 205 |
206 | 207 | It is also possible to directly enter this category/identifier instead 208 | of keywords to get a precise CD quickly. 209 | 210 | 211 | 212 |

Is the interactive interface always required to access CDDB?

213 | 214 | The CDDB interface seems to be designed for interactive usage since the user 215 | is supposed to choose a CD in the list of CDs returned by the keywords query, 216 | and then choose a track in the CD. 217 | But, it is also possible to use CDDB automatically if the CD id is known and 218 | the files are ordered by track numbers. 219 |
220 | 221 | The CD id may be given on the command line: 222 |
223 |   lltag --cddb-query rock/a0b2c4d7 --yes *
224 | 
225 | Using --yes enables automatic mode which means lltag will tag the 226 | first file as the first track of the CD returned by CDDB, the second file as 227 | the second track, ... 228 | 229 | If lltag ever reaches the end of the CD, it will return to interactive 230 | mode so that the user may choose another CD to tag the remaining files. 231 | 232 | 233 | 234 |

Managing tags

235 | 236 | 237 | 238 |

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

239 | 240 | Each tag field may receive a specific value even if it is not obtained through 241 | parsing the filename or so: 242 |
243 |   lltag -a MyArtist -y 1990 myfile.mp3
244 | 
245 | 246 | All regular fields may be defined like the above with -a, -t, 247 | -A, -n, -g, -d or -c. 248 | These (and any random tag) may also be set through --tag FIELD=value. 249 | 250 | These values are called explicit values. They are always added to the ones 251 | obtained through parsing or so, even if it means that the target tag will 252 | have multiple values. 253 | Playing with --clear or --append enables configuration 254 | of how they are actually added. 255 | 256 | 257 | 258 |

How do I cleanup tags?

259 | 260 | If your filenames are dirty, it is possible to ask lltag to be flexible 261 | with spaces. Any space in the format might be matched with 0 or several spaces 262 | with: 263 |
264 |   lltag --spaces myfile.mp3
265 | 
266 | 267 | The first letter of each word in each tag might be upcased with: 268 |
269 |   lltag --maj myfile.mp3
270 | 
271 | 272 | If the filename contains special characters that have to be translated into 273 | spaces for tags: 274 |
275 |   lltag --sep _|-|: myfile.mp3
276 | 
277 | 278 | 279 | 280 |

Renaming

281 | 282 | 283 | 284 |

How do I rename files after tagging?

285 | 286 | Files might be renamed using the tags that were gotten from original filename parsing: 287 |
288 |   lltag myfile.mp3 --rename "%a - %t.mp3"
289 | 
290 | 291 | Tags might be lowcased before renaming, and spaces might be replaced with another character: 292 |
293 |   lltag myfile.mp3 --rename "%a - %t.mp3" --rename-min --rename-sep _
294 | 
295 | 296 | 297 | 298 |

How do I rename files without tagging?

299 | 300 | lltag may also be used as a renaming program without any tagging: 301 | 302 |
303 |   lltag --no-tagging -F "%A/%n. %a - %t" --rename "%a - %A/%n - %t" */*.ogg
304 | 
305 | 306 | Note that lltag may rename using existing tags even if the old filename 307 | is useless. Matching with "%i" to ignore the old filename might be useful 308 | in this case. 309 | 310 |
311 |   lltag --no-tagging --rename "%a - %A/%n - %t" */*.ogg
312 | 
313 | 314 | 315 | 316 |
317 | 318 |
319 | 320 |
321 | The lltag team. 322 |
323 | 324 |
325 | $Id: howto.html,v 1.2 2006/05/07 00:57:26 bgoglin Exp $ 326 |
327 | 328 | 329 | 330 | -------------------------------------------------------------------------------- /lib/Lltag/Tags.pm: -------------------------------------------------------------------------------- 1 | package Lltag::Tags ; 2 | 3 | use strict ; 4 | no strict "refs" ; 5 | 6 | ####################################################### 7 | # init 8 | 9 | my $edit_values_usage_forced ; 10 | 11 | sub init_tagging { 12 | my $self = shift ; 13 | 14 | # need to show menu usage once ? 15 | $edit_values_usage_forced = $self->{menu_usage_once_opt} ; 16 | } 17 | 18 | ####################################################### 19 | # display tag values 20 | 21 | sub display_one_tag_value { 22 | my $self = shift ; 23 | my $values = shift ; 24 | my $field = shift ; 25 | my $prefix = shift ; 26 | 27 | if ($field =~ / -> _/) { 28 | print $prefix.ucfirst($field).": \n" 29 | } elsif (ref($values->{$field}) ne 'ARRAY') { 30 | print $prefix.ucfirst($field).": " 31 | . ($values->{$field} eq "" ? "" : $values->{$field}) ."\n" 32 | } else { 33 | my @vals = @{$values->{$field}} ; 34 | for(my $i = 0; $i < @vals; $i++) { 35 | print $prefix.ucfirst($field)." #".($i+1).": ".$vals[$i]."\n" 36 | } 37 | } 38 | } 39 | 40 | sub display_tag_values { 41 | my $self = shift ; 42 | my $values = shift ; 43 | my $prefix = shift ; 44 | 45 | # display regular tags first 46 | foreach my $field (@{$self->{field_names}}) { 47 | next unless defined $values->{$field} ; 48 | display_one_tag_value $self, $values, $field, $prefix ; 49 | } 50 | 51 | # display misc tags later 52 | foreach my $field (keys %{$values}) { 53 | next if grep { $field eq $_ } @{$self->{field_names}} ; 54 | display_one_tag_value $self, $values, $field, $prefix ; 55 | } 56 | } 57 | 58 | ####################################################### 59 | # various tag management routines 60 | 61 | # clone tag values (to be able to modify without changing the original) 62 | sub clone_tag_values { 63 | my $old_values = shift ; 64 | return undef unless defined $old_values ; 65 | 66 | # clone the hash 67 | my %new_values = %{$old_values} ; 68 | 69 | for my $field (keys %new_values) { 70 | if (ref($new_values{$field}) eq 'ARRAY') { 71 | # clone the array pointed by the ref in the hash 72 | @{$new_values{$field}} = @{$new_values{$field}} ; 73 | } 74 | } 75 | 76 | return \%new_values ; 77 | } 78 | 79 | # add a value to a field, creating an array if required 80 | sub append_tag_value { 81 | my $self = shift ; 82 | my $values = shift ; 83 | my $field = shift ; 84 | my $value = shift ; 85 | if (not defined $values->{$field}) { 86 | $values->{$field} = $value ; 87 | } elsif (ref($values->{$field}) ne 'ARRAY') { 88 | # create an array (except if we already have this value) 89 | my $tmp = $values->{$field} ; 90 | if ($tmp ne $value) { 91 | # need to delete the hash ref before changing its type 92 | delete $values->{$field} ; 93 | @{$values->{$field}} = ($tmp, $value) ; 94 | } 95 | } else { 96 | # append to the array (except if we already have this value) 97 | push @{$values->{$field}}, $value 98 | unless grep { $value eq $_ } @{$values->{$field}} ; 99 | } 100 | } 101 | 102 | # add a value or an array of values to a tag 103 | sub append_tag_multiple_value { 104 | my $self = shift ; 105 | my $values = shift ; 106 | my $field = shift ; 107 | my $multiple_value = shift ; 108 | 109 | if (ref($multiple_value) ne 'ARRAY') { 110 | append_tag_value $self, $values, $field, $multiple_value ; 111 | } else { 112 | map { 113 | append_tag_value $self, $values, $field, $_ ; 114 | } @{$multiple_value} ; 115 | } 116 | } 117 | 118 | # append a hash of values (either unique or arrays) into another hash 119 | sub append_tag_values { 120 | my $self = shift ; 121 | my $old_values = shift ; 122 | my $new_values = shift ; 123 | 124 | foreach my $field (keys %{$new_values}) { 125 | append_tag_multiple_value $self, $old_values, $field, $new_values->{$field} ; 126 | } 127 | } 128 | 129 | # add a set of new values into an old hash, depending of clear/append options 130 | sub merge_new_tag_values { 131 | my $self = shift ; 132 | my $old_values = shift ; 133 | my $new_values = shift ; 134 | 135 | if ($self->{clear_opt}) { 136 | $old_values = {} ; 137 | } 138 | 139 | foreach my $field (keys %{$new_values}) { 140 | delete $old_values->{$field} 141 | if defined $old_values->{$field} and !$self->{append_opt} ; 142 | append_tag_multiple_value $self, $old_values, $field, $new_values->{$field} ; 143 | } 144 | 145 | return $old_values ; 146 | } 147 | 148 | # return values for a field as an array 149 | sub get_tag_value_array { 150 | my $self = shift ; 151 | my $values = shift ; 152 | my $field = shift ; 153 | if (not defined $values->{$field}) { 154 | return () ; 155 | } elsif (ref ($values->{$field}) ne 'ARRAY') { 156 | return ($values->{$field}) ; 157 | } else { 158 | return @{$values->{$field}} ; 159 | } 160 | } 161 | 162 | # return a unique value for a field 163 | sub get_tag_unique_value { 164 | my $self = shift ; 165 | my $values = shift ; 166 | my $field = shift ; 167 | my @array = get_tag_value_array $self, $values, $field ; 168 | die "Trying to return a unique tag value on an empty array.\n" 169 | if ! @array ; 170 | return $array[0] ; 171 | } 172 | 173 | # return non-regular keys whose value is defined 174 | sub get_values_non_regular_keys { 175 | my $self = shift ; 176 | my $values = shift ; 177 | return grep { 178 | my $key = $_ ; 179 | !(grep { $_ eq $key } @{$self->{field_names}}) 180 | } (keys %{$values}) ; 181 | } 182 | 183 | # handle explicit tag values 184 | sub process_explicit_tag_value { 185 | my $self = shift ; 186 | my $string = shift ; 187 | if ($string =~ m/^([^=]+)=(.*)$/) { 188 | append_tag_value $self, $self->{explicit_values}, $1, $2 ; 189 | } else { 190 | die "Explicit tags must be given as 'TAG=value'.\n" ; 191 | } 192 | } 193 | 194 | ####################################################### 195 | # extract tags from the stream 196 | # helper to be used by backends who get the tags as the stream output of another program 197 | 198 | sub convert_tag_stream_to_values { 199 | my $self = shift ; 200 | my $values = {} ; 201 | 202 | while (my $line = shift @_) { 203 | chomp $line ; 204 | my ($field, $value) = ($line =~ m/^(.*?)=(.*)$/) ; 205 | next if !$value ; 206 | 207 | # remove the track total from the track number to avoid renaming problems with slashes or so 208 | if ($field eq "NUMBER") { 209 | if ($value =~ /^(\d+)/) { 210 | $value = $1 ; 211 | } else { 212 | return ; 213 | } 214 | } 215 | 216 | Lltag::Tags::append_tag_value ($self, $values, $field, $value) ; 217 | } 218 | 219 | return $values ; 220 | } 221 | 222 | ####################################################### 223 | # get tagging command line, display it if required, execute it if required 224 | # output the errors, ... 225 | # helper to be used by backends who set the tags with another program 226 | 227 | sub set_tags_with_external_prog { 228 | my $self = shift ; 229 | 230 | # show command line and really tag if asked 231 | if ($self->{dry_run_opt} or $self->{verbose_opt}) { 232 | print " '". +(join "' '", @_) ."'\n" ; 233 | } 234 | if (!$self->{dry_run_opt}) { 235 | print " Tagging.\n" ; 236 | my ($status, @output) = Lltag::Misc::system_with_output (@_) ; 237 | if ($status) { 238 | print " Tagging failed, command line was: '". join ("' '", @_) ."'.\n" ; 239 | while (my $line = shift @output) { 240 | print "# $line" ; 241 | } 242 | } 243 | } 244 | } 245 | 246 | ####################################################### 247 | # edit current tags 248 | 249 | use constant EDIT_SUCCESS => 0 ; 250 | use constant EDIT_CANCEL => -1; 251 | 252 | sub edit_values_usage { 253 | my $self = shift ; 254 | my $values = shift ; 255 | 256 | Lltag::Misc::print_usage_header (" ", "Editing") ; 257 | 258 | # print all fields, including the undefined ones 259 | foreach my $field (@{$self->{field_names}}) { 260 | print " ".$self->{field_name_letter}{$field} 261 | ." => Edit ".ucfirst($field).$self->{field_name_trailing_spaces}{$field} 262 | ."\n" ; 263 | } 264 | print " tag FOO => Edit tag FOO\n" ; 265 | print " V => View current fields\n" ; 266 | print " y/E => End edition\n" ; 267 | print " q/C => Cancel edition\n" ; 268 | print " During edition, enter to drop a value.\n" ; 269 | 270 | $edit_values_usage_forced = 0 ; 271 | } 272 | 273 | sub edit_one_value { 274 | my $self = shift ; 275 | my $values = shift ; 276 | my $field = shift ; 277 | 278 | if (ref($values->{$field}) eq 'ARRAY') { 279 | my @oldvals = @{$values->{$field}} ; 280 | my @newvals = () ; 281 | for(my $i=0; $i<@oldvals; $i++) { 282 | my $value = Lltag::Misc::readline (" ", ucfirst($field)." field #".($i+1), $oldvals[$i], 1) ; 283 | 284 | if (defined $value) { 285 | push @newvals, $value 286 | unless $value eq "" ; 287 | } else { 288 | # if ctrl-d, reset to same value, without removing it if empty 289 | push @newvals, $oldvals[$i] ; 290 | } 291 | } 292 | delete $values->{$field} ; 293 | if (@newvals == 1) { 294 | $values->{$field} = $newvals[0] ; 295 | } elsif (@newvals) { 296 | @{$values->{$field}} = @newvals ; 297 | } else { 298 | $values->{$field} = "" ; 299 | } 300 | 301 | } else { 302 | my $value = Lltag::Misc::readline (" ", ucfirst($field)." field", $values->{$field}, 1) ; 303 | 304 | # if ctrl-d, change nothing 305 | if (defined $value) { 306 | if ($value eq "DELETE" or $value eq "") { 307 | delete $values->{$field} ; 308 | } else { 309 | $values->{$field} = $value ; 310 | } 311 | } 312 | } 313 | } 314 | 315 | # edit values in place 316 | sub edit_values { 317 | my $self = shift ; 318 | my $values = shift ; 319 | 320 | edit_values_usage $self, $values 321 | if $edit_values_usage_forced ; 322 | 323 | print " Current tag values are:\n" ; 324 | display_tag_values $self, $values, " " ; 325 | 326 | while (1) { 327 | my $edit_reply = Lltag::Misc::readline (" ", "Edit a field [". $self->{field_letters_string} ."Vyq] (no default, h for help)", "", -1) ; 328 | 329 | # if ctrl-d, cancel editing 330 | $edit_reply = 'q' unless defined $edit_reply ; 331 | 332 | if ($edit_reply =~ m/^tag (.+)/) { 333 | edit_one_value $self, $values, $1 ; 334 | 335 | } elsif ($edit_reply =~ m/^($self->{field_letters_union})/) { 336 | edit_one_value $self, $values, $self->{field_letter_name}{$1} ; 337 | 338 | } elsif ($edit_reply =~ m/^y/ or $edit_reply =~ m/^E/) { 339 | return EDIT_SUCCESS ; 340 | 341 | } elsif ($edit_reply =~ m/^q/ or $edit_reply =~ m/^C/) { 342 | return EDIT_CANCEL ; 343 | 344 | } elsif ($edit_reply =~ m/^V/) { 345 | print " Current tag values are:\n" ; 346 | display_tag_values $self, $values, " " ; 347 | 348 | } else { 349 | edit_values_usage $self, $values ; 350 | } 351 | } 352 | } 353 | 354 | ####################################################### 355 | # apply user-given regexp 356 | 357 | sub apply_regexp_to_tag { 358 | my $val = shift ; 359 | my $regexp = shift ; 360 | my $tag = shift ; 361 | 362 | # parse the regexp 363 | if ($regexp =~ m@(?:([^:]+):)?s/([^/]+)/([^/]*)/$@) { 364 | my @tags = () ; 365 | @tags = split (/,/, $1) if $1; 366 | my $from = $2 ; 367 | my $to = $3 ; 368 | $val =~ s/$from/$to/g 369 | if !@tags or grep { $tag eq $_ } @tags ; 370 | } else { 371 | die "Unrecognized user regexp '$regexp'.\n" ; 372 | } 373 | 374 | return $val ; 375 | } 376 | 377 | ####################################################### 378 | # clone tag values (to be able to modify without changing the original) and merge tags case-insensitively 379 | 380 | sub clone_tag_values_uc { 381 | my $self = shift ; 382 | my $old_values = shift ; 383 | 384 | return undef unless defined $old_values ; 385 | 386 | # clone the hash 387 | my %new_values; 388 | 389 | # use upcase values first 390 | for my $field (keys %{$old_values}) { 391 | if ($field eq uc($field)) { 392 | append_tag_multiple_value $self, \%new_values, uc($field), $old_values->{$field} ; 393 | } 394 | } 395 | # other values then 396 | for my $field (keys %{$old_values}) { 397 | if ($field ne uc($field)) { 398 | append_tag_multiple_value $self, \%new_values, uc($field), $old_values->{$field} ; 399 | } 400 | } 401 | 402 | return \%new_values ; 403 | } 404 | 405 | 1 ; 406 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 675 Mass Ave, Cambridge, MA 02139, USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | Appendix: How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 19yy 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License 307 | along with this program; if not, write to the Free Software 308 | Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) 19yy name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Library General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /lib/Lltag/Parse.pm: -------------------------------------------------------------------------------- 1 | package Lltag::Parse ; 2 | 3 | use strict ; 4 | no strict "refs" ; # for ${$i} 5 | 6 | # ignoring fields during parsing 7 | use constant IGNORE_LETTER => 'i' ; 8 | use constant IGNORE_NAME => 'IGNORE' ; 9 | 10 | # subregexp 11 | my $match_path = '(?:[^/]*\/)*' ; 12 | my $match_any = '((?:[^ /]+ +)*[^ /]+)' ; 13 | my $match_num = '([0-9]+)' ; 14 | my $match_space = ' '; 15 | my $match_spaces = ' *' ; 16 | my $match_limit = '' ; 17 | 18 | # the parser that the user wants to always use 19 | my $preferred_parser = undef ; 20 | 21 | # confirmation behavior 22 | my $current_parse_ask_opt ; 23 | my $current_parse_yes_opt ; 24 | 25 | ####################################################### 26 | # Parsing return values 27 | use constant PARSE_SUCCESS_PREFERRED => 1 ; 28 | use constant PARSE_SUCCESS => 0 ; 29 | use constant PARSE_ABORT => -1 ; 30 | use constant PARSE_SKIP_PARSER => -2 ; 31 | use constant PARSE_SKIP_PATH_PARSER => -3 ; 32 | use constant PARSE_NO_MATCH => -4 ; 33 | 34 | # Parsing acceptable behavior 35 | use constant PARSE_MAY_SKIP_PARSER => 1 ; 36 | use constant PARSE_MAY_SKIP_PATH_PARSER => 2 ; 37 | use constant PARSE_MAY_PREFER => 4 ; 38 | 39 | ####################################################### 40 | # initialization 41 | 42 | my $confirm_parser_usage_forced ; 43 | 44 | sub init_parsing { 45 | my $self = shift ; 46 | 47 | # default confirmation behavior 48 | $current_parse_ask_opt = $self->{ask_opt} ; 49 | $current_parse_yes_opt = $self->{yes_opt} ; 50 | 51 | # spaces_opt changes matching regexps 52 | $match_limit = $match_space = $match_spaces if $self->{spaces_opt} ; 53 | 54 | # need to show menu usage once ? 55 | $confirm_parser_usage_forced = $self->{menu_usage_once_opt} ; 56 | } 57 | 58 | ####################################################### 59 | # parsing format specific usage 60 | 61 | sub parsing_format_usage { 62 | my $self = shift ; 63 | print " %".IGNORE_LETTER." means that the text has to be ignored\n" ; 64 | print " %% means %\n" ; 65 | } 66 | 67 | ####################################################### 68 | # parsing confirmation 69 | 70 | sub confirm_parser_letters { 71 | my $behaviors = shift ; 72 | my $string = "[y" ; 73 | $string .= "u" if $behaviors & PARSE_MAY_PREFER ; 74 | $string .= "a" ; 75 | $string .= "n" if $behaviors & PARSE_MAY_SKIP_PARSER ; 76 | $string .= "p" if $behaviors & PARSE_MAY_SKIP_PATH_PARSER ; 77 | $string .= "q]" ; 78 | return $string ; 79 | } 80 | 81 | sub confirm_parser_usage { 82 | my $behaviors = shift ; 83 | Lltag::Misc::print_usage_header (" ", "Parsing filenames") ; 84 | print " y => Yes, use this matching (default)\n" ; 85 | print " u => Use this format for all files until one does not match\n" 86 | if $behaviors & PARSE_MAY_PREFER ; 87 | print " a => Always yes, stop asking for a confirmation\n" ; 88 | print " n => No, try the next matching format\n" 89 | if $behaviors & PARSE_MAY_SKIP_PARSER ; 90 | print " p => No, try the next path matching format\n" 91 | if $behaviors & PARSE_MAY_SKIP_PATH_PARSER ; 92 | print " q => Quit parsing, stop trying to parse this filename\n" ; 93 | print " h => Show this help\n" ; 94 | 95 | $confirm_parser_usage_forced = 0 ; 96 | } 97 | 98 | sub confirm_parser { 99 | my $self = shift ; 100 | my $file = shift ; 101 | my $confirm = shift ; 102 | my $behaviors = shift ; 103 | my $values = shift ; 104 | 105 | # prefer this type of tagging ? 106 | my $preferred = 0 ; 107 | 108 | # confirm if required 109 | if ($current_parse_ask_opt or ($confirm and !$current_parse_yes_opt)) { 110 | 111 | confirm_parser_usage $behaviors 112 | if $confirm_parser_usage_forced ; 113 | 114 | while (1) { 115 | my $reply = Lltag::Misc::readline (" ", "Use this matching ".(confirm_parser_letters ($behaviors))." (default is yes, h for help)", "", -1) ; 116 | 117 | # if ctrl-d, stop trying to parse 118 | $reply = 'q' unless defined $reply ; 119 | 120 | if ($reply eq "" or $reply =~ m/^y/) { 121 | last ; 122 | 123 | } elsif ($reply =~ m/^a/) { 124 | $current_parse_ask_opt = 0 ; $current_parse_yes_opt = 1 ; 125 | last ; 126 | 127 | } elsif ($behaviors & PARSE_MAY_PREFER and $reply =~ m/^u/) { 128 | $preferred = 1 ; 129 | $current_parse_ask_opt = 0 ; $current_parse_yes_opt = 1 ; 130 | last ; 131 | 132 | } elsif ($behaviors & PARSE_MAY_SKIP_PARSER and $reply =~ m/^n/) { 133 | return (PARSE_SKIP_PARSER, undef) ; 134 | 135 | } elsif ($behaviors & PARSE_MAY_SKIP_PATH_PARSER and $reply =~ m/^p/) { 136 | return (PARSE_SKIP_PATH_PARSER, undef) ; 137 | 138 | } elsif ($reply =~ m/^q/) { 139 | return (PARSE_ABORT, undef) ; 140 | 141 | } else { 142 | confirm_parser_usage $behaviors ; 143 | } 144 | } 145 | } 146 | 147 | if ($preferred) { 148 | return (PARSE_SUCCESS_PREFERRED, $values) ; 149 | } else { 150 | return (PARSE_SUCCESS, $values) ; 151 | } 152 | } 153 | 154 | ####################################################### 155 | # actual parsing 156 | 157 | sub apply_parser { 158 | my $self = shift ; 159 | my $file = shift ; 160 | my $parsename = shift ; 161 | my $parser = shift ; 162 | my $confirm = shift ; 163 | my $behaviors = shift ; 164 | 165 | my @matches ; 166 | 167 | # protect against bad regexp, just in case (we should have found problems during initialization) 168 | eval { 169 | @matches = ($parsename =~ m/^$parser->{regexp}$/) ; 170 | 1 ; # be sure to return success when the regexp does not match 171 | } or 172 | Lltag::Misc::die_error ("Failed to apply parser '$parser->{title}', regexp '$parser->{regexp}' is invalid?") ; 173 | 174 | # we ensure earlier that there is at least one field to match, so an error will return () 175 | return (PARSE_SKIP_PARSER, undef) unless @matches ; 176 | 177 | print " '$parser->{title}' matches this file...\n" ; 178 | 179 | my @field_table = @{$parser->{field_table}} ; 180 | 181 | # check the number of matches 182 | Lltag::Misc::die_error ("Matched ".(scalar @matches)." fields instead of ".(scalar @field_table).", parser invalid?") 183 | unless @matches == @field_table ; 184 | 185 | my $values = {} ; 186 | 187 | # traverse matches 188 | for(my $i=0; $i<@field_table; $i++) { 189 | 190 | my $field = $field_table[$i] ; 191 | if ($field ne IGNORE_NAME) { 192 | my $val = $matches[$i] ; 193 | 194 | # apply maj, sep and regexp to the value 195 | $val =~ s/\b(.)/uc $1/eg if $self->{maj_opt} ; 196 | $val =~ s/($self->{sep_opt})/ /g if defined $self->{sep_opt} ; 197 | map { $val = Lltag::Tags::apply_regexp_to_tag ($val, $_, $field) } @{$self->{regexp_opts}} ; 198 | 199 | # check whether it's already defined. 200 | # TODO: append ? 201 | if (defined $values->{$field}) { 202 | Lltag::Misc::print_warning (" ", ucfirst($field)." already set to '".$values->{$field} 203 | ."', skipping new value '$val'") 204 | if $values->{$field} ne $val ; 205 | next ; 206 | } 207 | 208 | # ok 209 | $values->{$field} = $val ; 210 | if ($self->{verbose_opt} or $confirm or $current_parse_ask_opt) { 211 | print " ". ucfirst($field) 212 | .$self->{field_name_trailing_spaces}{$field} .": ". $val ."\n" ; 213 | } 214 | } 215 | } 216 | 217 | return confirm_parser ($self, $file, $confirm, $behaviors, $values) ; 218 | } 219 | 220 | ####################################################### 221 | # internal parsers 222 | 223 | my @internal_basename_parsers = () ; 224 | my @internal_path_parsers = () ; 225 | 226 | sub add_internal_parser { 227 | my $self = shift ; 228 | my $file = shift ; 229 | my $startline = shift ; 230 | my $type = shift ; 231 | my $title = shift ; 232 | my $regexp = shift ; 233 | my $regexp_size = shift ; 234 | my $field_table = shift ; 235 | 236 | if ($type and $title and $regexp and @{$field_table}) { 237 | my $parser ; 238 | $parser->{title} = $title ; 239 | $parser->{regexp} = $regexp ; 240 | @{$parser->{field_table}} = @{$field_table} ; 241 | 242 | # check whether there are the same number of fields in the regexp and in the field_table 243 | 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} ).").") 244 | unless $regexp_size == scalar @{$field_table} ; 245 | 246 | # check whether the regexp is applicable 247 | eval { 248 | my $dummy = ("dummy" =~ m@^$regexp/[^/]+$@) ; 249 | # be sure to return success even if not matched 250 | 1 ; 251 | } or 252 | # print the parser and its formats file (not the line since we may be way later already 253 | Lltag::Misc::die_error (" Parser '$title' regexp '$regexp' looks invalid at line $startline in file '$file'.") ; 254 | 255 | # add the parser 256 | if ($type eq "basename" or $type eq "filename") { 257 | # TODO: drop filename support on september 20 2006 258 | print " Got basename format '$title' (regexp '$regexp')\n" if $self->{verbose_opt} ; 259 | push (@internal_basename_parsers, $parser) ; 260 | } elsif ($type eq "path") { 261 | print " Got path format '$title' (regexp '$regexp')\n" if $self->{verbose_opt} ; 262 | push (@internal_path_parsers, $parser) ; 263 | } 264 | } elsif ($type or $title or $regexp or @{$field_table}) { 265 | Lltag::Misc::die_error ("Incomplete format at line $startline in file '$file'.") ; 266 | } 267 | } 268 | 269 | sub read_internal_parsers { 270 | my $self = shift ; 271 | 272 | # get parsers from configuration files 273 | my $file ; 274 | if (open FORMAT, "$self->{user_lltag_dir}/$self->{lltag_format_filename}") { 275 | $file = "$self->{user_lltag_dir}/$self->{lltag_format_filename}" ; 276 | } elsif (open FORMAT, "$self->{common_lltag_dir}/$self->{lltag_format_filename}") { 277 | $file = "$self->{common_lltag_dir}/$self->{lltag_format_filename}" ; 278 | } else { 279 | print "Did not find any format file.\n" ; 280 | goto NO_FORMATS_FILE_FOUND; 281 | } 282 | print "Reading format file '$file'...\n" if $self->{verbose_opt} ; 283 | 284 | my $startline = undef ; 285 | my $type = undef ; 286 | my $title = undef ; 287 | my $regexp = undef ; 288 | my $regexp_size = undef ; 289 | my @field_table = () ; 290 | 291 | while () { 292 | chomp $_ ; 293 | next if /^#/ ; 294 | next if /^$/ ; 295 | 296 | if (/^\[(.*)\]$/) { 297 | add_internal_parser $self, $file, $startline, $type, $title, $regexp, $regexp_size, \@field_table ; 298 | $startline = $. ; 299 | $type = undef ; 300 | $regexp = undef ; 301 | $regexp_size = undef ; 302 | @field_table = () ; 303 | $title = $1 ; 304 | # stocker la ligne ? 305 | 306 | } elsif (/^type = (.*)$/) { 307 | Lltag::Misc::die_error ("Unsupported format type '$1' at line $. in file '$file'.") 308 | if $1 ne "basename" and $1 ne "filename" and $1 ne "path" ; 309 | # TODO: drop filename support on september 20 2006 310 | $type = $1 ; 311 | 312 | } elsif (/^regexp = (.*)$/) { 313 | $regexp = $1 ; 314 | # escape special characters 315 | # FIXME: add *+$^ ? 316 | $regexp =~ s/\./\\./g ; 317 | $regexp =~ s/\(/\\\(/g ; 318 | $regexp =~ s/\)/\\\)/g ; 319 | $regexp =~ s/\[/\\\[/g ; 320 | $regexp =~ s/\]/\\\]/g ; 321 | $regexp =~ s@/@\\/@g ; 322 | $regexp_size = 0 ; 323 | 324 | # do the replacement progressively so that %% and %x and not mixed 325 | while ($regexp =~ m/(%(?:P|L|S|N|A|%))/) { 326 | if ($1 eq '%P') { 327 | $regexp =~ s/%P/$match_path/ ; 328 | } elsif ($1 eq '%L') { 329 | $regexp =~ s/%L/$match_limit/ ; 330 | } elsif ($1 eq '%S') { 331 | $regexp =~ s/%S/$match_space/ ; 332 | } elsif ($1 eq '%N') { 333 | $regexp =~ s/%N/$match_num/ ; 334 | $regexp_size++; 335 | } elsif ($1 eq '%A') { 336 | $regexp =~ s/%A/$match_any/ ; 337 | $regexp_size++; 338 | } elsif ($1 eq '%%') { 339 | $regexp =~ s/%%/%/ ; 340 | } 341 | } 342 | 343 | Lltag::Misc::die_error ("Parser '$title' at line $startline in file '$file' needs at least one matching %A or %N in its regexp.") 344 | unless $regexp_size ; 345 | 346 | } elsif (/^indices = (.*)$/) { 347 | my @name_table = split (/,/, $1) ; 348 | Lltag::Misc::die_error ("Parser '$title' at line $startline in file '$file' needs at least one indice.") 349 | unless @name_table ; 350 | @field_table = map { 351 | my $field ; 352 | if (defined $self->{field_name_letter}{$_} or $_ eq IGNORE_NAME) { 353 | # full field name, keep as it is 354 | $field = $_ 355 | } elsif (defined $self->{field_letter_name}{$_}) { 356 | # field letter 357 | $field = $self->{field_letter_name}{$_} ; 358 | } elsif ($_ eq IGNORE_LETTER) { 359 | # ignore letter 360 | $field = IGNORE_NAME ; 361 | } else { 362 | Lltag::Misc::die_error ("Unrecognized field '$_' on line $. in file '$file'.") ; 363 | } 364 | $field } @name_table ; 365 | 366 | } else { 367 | Lltag::Misc::die_error ("Unrecognized line $. in file '$file': '$_'.") ; 368 | } 369 | } 370 | close FORMAT ; 371 | 372 | # save the last format 373 | add_internal_parser $self, $file, $startline, $type, $title, $regexp, $regexp_size, \@field_table ; 374 | 375 | NO_FORMATS_FILE_FOUND: 376 | } 377 | 378 | sub list_internal_parsers { 379 | # path+basename 380 | foreach my $path_parser (@internal_path_parsers) { 381 | foreach my $basename_parser (@internal_basename_parsers) { 382 | print " $path_parser->{title}/$basename_parser->{title}\n" ; 383 | } 384 | } 385 | # basename only 386 | foreach my $basename_parser (@internal_basename_parsers) { 387 | print " $basename_parser->{title}\n" ; 388 | } 389 | } 390 | 391 | sub merge_internal_parsers { 392 | my $path_parser = shift ; 393 | my $basename_parser = shift ; 394 | my $parser ; 395 | $parser->{title} = "$path_parser->{title}/$basename_parser->{title}" ; 396 | $parser->{regexp} = "$path_parser->{regexp}/$basename_parser->{regexp}" ; 397 | @{$parser->{field_table}} = (@{$path_parser->{field_table}}, @{$basename_parser->{field_table}}) ; 398 | return $parser ; 399 | } 400 | 401 | sub apply_internal_basename_parsers { 402 | my $self = shift ; 403 | my $file = shift ; 404 | my $parsename = shift ; 405 | 406 | # no path, only try each basename parser 407 | foreach my $basename_parser (@internal_basename_parsers) { 408 | # try to tag, with confirmation 409 | my ($res, $values) = apply_parser $self, $file, $parsename, $basename_parser, 1, PARSE_MAY_PREFER|PARSE_MAY_SKIP_PARSER ; 410 | if ($res == PARSE_SUCCESS || $res == PARSE_SUCCESS_PREFERRED || $res == PARSE_ABORT) { 411 | if ($res == PARSE_SUCCESS_PREFERRED) { 412 | $preferred_parser = $basename_parser ; 413 | } 414 | return ($res, $values) ; 415 | } 416 | # try next parser 417 | die "Unknown tag return value: $res.\n" # this is a bug 418 | if $res != PARSE_SKIP_PARSER ; 419 | } 420 | return (PARSE_NO_MATCH, undef) ; 421 | } 422 | 423 | sub apply_internal_path_basename_parsers { 424 | my $self = shift ; 425 | my $file = shift ; 426 | my $parsename = shift ; 427 | 428 | # try each path parser and each basename parser 429 | foreach my $path_parser (@internal_path_parsers) { 430 | # match the path only first, to reduce number of (path,basename) parsers to try, 431 | # and to check that there are no '/' afterwards 432 | 433 | # protect against bad regexp, just in case (we should have found problems during initialization) 434 | my $res ; 435 | eval { 436 | $res = ($parsename =~ m@^$path_parser->{regexp}/[^/]+$@) ; 437 | 1 ; # be sure to return success when the regexp does not match 438 | } or 439 | Lltag::Misc::die_error ("Failed to apply parser '$path_parser->{title}', regexp '$path_parser->{regexp}' is invalid?") ; 440 | 441 | if ($res) { 442 | foreach my $basename_parser (@internal_basename_parsers) { 443 | my $whole_parser = merge_internal_parsers ($path_parser, $basename_parser) ; 444 | # try to tag, with confirmation 445 | my ($res, $values) = apply_parser $self, $file, $parsename, $whole_parser, 1, PARSE_MAY_PREFER|PARSE_MAY_SKIP_PARSER|PARSE_MAY_SKIP_PATH_PARSER ; 446 | if ($res == PARSE_SUCCESS || $res == PARSE_SUCCESS_PREFERRED || $res == PARSE_ABORT) { 447 | if ($res == PARSE_SUCCESS_PREFERRED) { 448 | $preferred_parser = $whole_parser ; 449 | } 450 | return ($res, $values) ; 451 | } 452 | # try next path parser if asked 453 | goto NEXT_PATH_PARSER 454 | if $res == PARSE_SKIP_PATH_PARSER ; 455 | 456 | # try next parser 457 | die "Unknown tag return value: $res.\n" # this is a bug 458 | if $res != PARSE_SKIP_PARSER ; 459 | } 460 | } 461 | NEXT_PATH_PARSER: 462 | } 463 | return (PARSE_NO_MATCH, undef) ; 464 | } 465 | 466 | ####################################################### 467 | # user parsers 468 | 469 | # list of user-provided parsers 470 | my @user_parsers ; 471 | 472 | # change a format strings into usable infos 473 | sub generate_user_parser { 474 | my $self = shift ; 475 | my $format_string = shift ; 476 | 477 | print "Generating parser for format '". $format_string ."'...\n" ; 478 | 479 | my $parser ; 480 | $parser->{title} = $format_string ; 481 | 482 | # merge spaces if --spaces was passed 483 | if ($self->{spaces_opt}) { 484 | $format_string =~ s/ +/ /g ; 485 | } 486 | 487 | # create the regexp and store indice fields 488 | my @array = split(//, $format_string) ; 489 | my @field_table = () ; 490 | for(my $i = 0; $i < @array; $i++) { 491 | 492 | my $char = $array[$i] ; 493 | # normal characters 494 | if ($char ne "%") { 495 | 496 | if ($char eq " ") { 497 | # replace spaces with general space matching regexp 498 | $array[$i] = $match_space ; 499 | 500 | } elsif ($char eq "/") { 501 | # replace / with space flexible matching regexp 502 | $array[$i] = $match_limit."/".$match_limit ; 503 | 504 | } elsif (index ("()[]", $char) != -1) { 505 | # escape regexp control characters 506 | $array[$i] = "\\".$char ; 507 | 508 | } 509 | # keep this character 510 | next ; 511 | } 512 | 513 | if ($i == @array - 1) { 514 | Lltag::Misc::die_error ("Format '". $format_string ."' ends with '%' without operator letter.") ; 515 | } 516 | 517 | # remove % and check next char 518 | splice (@array, $i, 1) ; 519 | # replace the char with the matching 520 | $char = $array[$i] ; 521 | next if $char eq "%" ; 522 | if ($char eq "n") { 523 | $array[$i] = $match_num ; 524 | } elsif ($char =~ m/$self->{field_letters_union}/) { 525 | $array[$i] = $match_any ; 526 | } elsif ($char eq IGNORE_LETTER) { # looks like constants do not work in regexp 527 | $array[$i] = $match_any ; 528 | } else { 529 | Lltag::Misc::die_error ("Format '". $format_string ."' contains unrecognized operator '%". $array[$i] ."'.") ; 530 | } 531 | # store the indice 532 | if ($char eq IGNORE_LETTER) { 533 | push @field_table, IGNORE_NAME ; 534 | } else { 535 | push @field_table, $self->{field_letter_name}{$char} ; 536 | } 537 | } 538 | @{$parser->{field_table}} = @field_table ; 539 | 540 | Lltag::Misc::die_error ("Format '$format_string' does not contain any matching field.") 541 | unless @field_table ; 542 | 543 | # done 544 | if ($self->{spaces_opt}) { 545 | $parser->{regexp} = $match_limit. join("", @array) .$match_limit ; 546 | } else { 547 | $parser->{regexp} = join("", @array) ; 548 | } 549 | 550 | # check insolvable regexp 551 | for(my $i = 0; $i < @array - 1; $i++) { 552 | my $char = $array[$i] ; 553 | my $nextchar = $array[$i+1] ; 554 | if ( $char eq $match_any and 555 | ( $nextchar eq $match_any or $nextchar eq $match_num ) ) { 556 | Lltag::Misc::print_warning (" ", "Format '". $format_string 557 | ."' leads to problematic subregexp '". $char.$nextchar 558 | ."' that won't probably match as desired") ; 559 | } 560 | } 561 | 562 | if ($self->{verbose_opt}) { 563 | print " Format string will parse with: ". $parser->{regexp} ."\n" ; 564 | print " Fields are: ". (join ',', @field_table) ."\n" ; 565 | } 566 | 567 | return $parser ; 568 | } 569 | 570 | sub generate_user_parsers { 571 | my $self = shift ; 572 | @user_parsers = map ( generate_user_parser ($self, $_), @{$self->{user_format_strings}} ) ; 573 | } 574 | 575 | sub apply_user_parsers { 576 | my $self = shift ; 577 | my $file = shift ; 578 | my $parsename = shift ; 579 | 580 | # try each format until one works 581 | foreach my $parser (@user_parsers) { 582 | # try to tag, without confirmation 583 | my ($res, $values) = apply_parser $self, $file, $parsename, $parser, 0, PARSE_MAY_PREFER|PARSE_MAY_SKIP_PARSER ; 584 | if ($res == PARSE_SUCCESS || $res == PARSE_SUCCESS_PREFERRED || $res == PARSE_ABORT) { 585 | if ($res == PARSE_SUCCESS_PREFERRED) { 586 | $preferred_parser = $parser ; 587 | } 588 | return ($res, $values) ; 589 | } 590 | print " '". $parser->{title} ."' does not match.\n" ; 591 | # try next parser 592 | die "Unknown tag return value: $res.\n" # this is a bug 593 | if $res != PARSE_SKIP_PARSER ; 594 | } 595 | return (PARSE_NO_MATCH, undef) ; 596 | } 597 | 598 | ####################################################### 599 | # high-level parsing routines 600 | 601 | sub try_to_parse_with_preferred { 602 | my $self = shift ; 603 | my $file = shift ; 604 | my $parsename = shift ; 605 | 606 | my $values = undef ; 607 | my $res ; 608 | 609 | # try the preferred parser first 610 | return (PARSE_NO_MATCH, undef) 611 | unless defined $preferred_parser ; 612 | 613 | print " Trying to parse filename with the previous matching parser...\n" ; 614 | 615 | # there can't be any confirmation here, SKIP is not possible 616 | ($res, $values) = apply_parser $self, $file, $parsename, $preferred_parser, 0, 0 ; 617 | if ($res != PARSE_SKIP_PARSER) { 618 | # only SUCCESS if possible 619 | die "Unknown tag return value: $res.\n" # this is a bug 620 | if $res != PARSE_SUCCESS ; 621 | return ($res, $values) ; 622 | 623 | } else { 624 | Lltag::Misc::print_notice (" ", "'$preferred_parser->{title}' does not match anymore, returning to original mode") ; 625 | $current_parse_ask_opt = $self->{ask_opt} ; $current_parse_yes_opt = $self->{yes_opt} ; 626 | $preferred_parser = undef ; 627 | return (PARSE_NO_MATCH, undef) ; 628 | } 629 | } 630 | 631 | my $user_parsers_initialized = 0 ; 632 | my $internal_parsers_initialized = 0 ; 633 | 634 | sub try_to_parse { 635 | my $self = shift ; 636 | my $file = shift ; 637 | my $parsename = shift ; 638 | my $try_internals = shift ; 639 | 640 | my $values = undef ; 641 | my $res ; 642 | 643 | # initialize user parsers once 644 | if (!$user_parsers_initialized) { 645 | generate_user_parsers ($self) ; 646 | $user_parsers_initialized = 1 ; 647 | } 648 | 649 | # try user provided parsers first 650 | if (@user_parsers) { 651 | print " Trying to parse filename with user-provided formats...\n" ; 652 | ($res, $values) = apply_user_parsers $self, $file, $parsename ; 653 | return ($res, $values) 654 | if $res == PARSE_SUCCESS or $res == PARSE_SUCCESS_PREFERRED or $res == PARSE_ABORT ; 655 | } 656 | 657 | # try to guess my internal format database then 658 | if ($try_internals) { 659 | print " Trying to parse filename with internal formats...\n" ; 660 | 661 | # initialize internal parsers once 662 | if (!$internal_parsers_initialized) { 663 | read_internal_parsers ($self) ; 664 | $internal_parsers_initialized = 1 ; 665 | } 666 | 667 | if ($self->{no_path_opt} or $parsename !~ m@/@) { 668 | ($res, $values) = apply_internal_basename_parsers $self, $file, $parsename ; 669 | } else { 670 | ($res, $values) = apply_internal_path_basename_parsers $self, $file, $parsename ; 671 | } 672 | return ($res, $values) 673 | if $res == PARSE_SUCCESS or $res == PARSE_SUCCESS_PREFERRED or $res == PARSE_ABORT ; 674 | } 675 | 676 | if ($try_internals or @user_parsers) { 677 | print " Didn't find any parser!\n" ; 678 | } 679 | 680 | return (PARSE_NO_MATCH, undef) ; 681 | } 682 | 683 | 1 ; 684 | -------------------------------------------------------------------------------- /lib/Lltag/CDDB.pm: -------------------------------------------------------------------------------- 1 | package Lltag::CDDB ; 2 | 3 | use strict ; 4 | 5 | use Lltag::Misc ; 6 | 7 | use I18N::Langinfo qw(langinfo CODESET) ; 8 | 9 | # return values that are passed to lltag 10 | use constant CDDB_SUCCESS => 0 ; 11 | use constant CDDB_ABORT => -1 ; 12 | 13 | # local return values 14 | use constant CDDB_ABORT_TO_KEYWORDS => -10 ; 15 | use constant CDDB_ABORT_TO_CDIDS => -11 ; 16 | 17 | # keep track of where we were during the previous CDDB access 18 | my $previous_cdids = undef ; 19 | my $previous_cd = undef ; 20 | my $previous_track = undef ; 21 | 22 | # confirmation behavior 23 | my $current_cddb_yes_opt = undef ; 24 | 25 | # HTTP browser 26 | my $browser ; 27 | 28 | ######################################### 29 | # init 30 | 31 | my $cddb_supported = 1 ; 32 | 33 | my $cddb_track_usage_forced ; 34 | my $cddb_cd_usage_forced ; 35 | my $cddb_keywords_usage_forced ; 36 | 37 | sub init_cddb { 38 | my $self = shift ; 39 | 40 | if (not eval { require LWP ; } ) { 41 | print "LWP (libwww-perl module) does not seem to be available, disabling CDDB.\n" 42 | if $self->{verbose_opt} ; 43 | $cddb_supported = 0 ; 44 | return ; 45 | } 46 | 47 | # default confirmation behavior 48 | $current_cddb_yes_opt = $self->{yes_opt} ; 49 | 50 | # HTTP browser 51 | $browser = LWP::UserAgent->new; 52 | # use HTTP_PROXY environment variable 53 | $browser->env_proxy ; 54 | 55 | # need to show menu usage once ? 56 | $cddb_track_usage_forced = $self->{menu_usage_once_opt} ; 57 | $cddb_cd_usage_forced = $self->{menu_usage_once_opt} ; 58 | $cddb_keywords_usage_forced = $self->{menu_usage_once_opt} ; 59 | } 60 | 61 | ######################################### 62 | # freedb.org specific code 63 | # NOT USED ANYMORE since Magix acquired freedb.org 64 | # and closed the online search module for now 65 | ######################################### 66 | 67 | sub freedborg_cddb_response { 68 | my $self = shift ; 69 | my $path = shift ; 70 | 71 | print " Sending CDDB request...\n" ; 72 | print " '$path'\n" if $self->{verbose_opt} ; 73 | my $response = $browser->get( 74 | "http://" 75 | . $self->{cddb_server_name} 76 | . ($self->{cddb_server_port} != 80 ? $self->{cddb_server_port} : "") 77 | . $path 78 | . "\n" 79 | ) ; 80 | 81 | if (!$response->is_success) { 82 | Lltag::Misc::print_error (" ", 83 | "HTTP request to CDDB server (" 84 | . $self->{cddb_server_name} .":". $self->{cddb_server_port} 85 | . ") failed.") ; 86 | return undef ; 87 | } 88 | if ($response->content_type ne 'text/html') { 89 | Lltag::Misc::print_error (" ", 90 | "Weird CDDB response (type ".$response->content_type.") from server " 91 | . $self->{cddb_server_name} .":". $self->{cddb_server_port} 92 | . ".") ; 93 | return undef ; 94 | } 95 | # TODO: grep for something to be sure it worked 96 | 97 | return $response->content ; 98 | } 99 | 100 | sub freedborg_cddb_query_cd_by_keywords { 101 | my $self = shift ; 102 | my $keywords = shift ; 103 | 104 | # extract fields and cat from the keywords 105 | my @fields = () ; 106 | my @cats = () ; 107 | my @keywords_list = () ; 108 | foreach my $word (split / +/, $keywords) { 109 | if ($word =~ m/^fields=(.+)$/) { 110 | push @fields, (split /\++/, $1) ; 111 | } elsif ($word =~ m/^cats=(.+)$/) { 112 | push @cats, (split /\++/, $1) ; 113 | } else { 114 | push @keywords_list, $word ; 115 | } 116 | } 117 | # assemble remaining keywords with "+" 118 | $keywords = join "+", @keywords_list ; 119 | 120 | # by default, search in all cats, within artist and title only 121 | @cats = ( "all" ) unless @cats ; 122 | @fields = ( "artist", "title" ) unless @fields ; 123 | 124 | my $query_fields = (grep { $_ eq "all" } @fields) ? "allfields=YES" : "allfields=NO".(join ("", map { "&fields=$_" } @fields)) ; 125 | my $query_cats = (grep { $_ eq "all" } @cats) ? "allcats=YES" : "allcats=NO".(join ("", map { "&cats=$_" } @cats)) ; 126 | 127 | my $response = freedborg_cddb_response $self, "/freedb_search.php?words=${keywords}&${query_fields}&${query_cats}&grouping=none&x=0&y=0" ; 128 | return (CDDB_ABORT, undef) unless defined $response ; 129 | 130 | my @cdids = () ; 131 | my $samename = undef ; 132 | my $same = 0 ; 133 | 134 | foreach my $line (split /\n/, $response) { 135 | next if $line !~ //) { 137 | $same = 0 ; 138 | $samename = undef ; 139 | } else { 140 | $same = 1; 141 | } 142 | my @links = split (/(.*)@) { 146 | my %cdid = ( CAT => $1, ID => $2, NAME => $same ? $samename : $3 ) ; 147 | push @cdids, \%cdid ; 148 | $samename = $cdid{NAME} unless $same ; 149 | $same = 1; 150 | } 151 | } 152 | } 153 | 154 | return (CDDB_SUCCESS, \@cdids) ; 155 | } 156 | 157 | sub freedborg_cddb_query_tracks_by_id { 158 | my $self = shift ; 159 | my $cat = shift ; 160 | my $id = shift ; 161 | my $name = shift ; 162 | 163 | my $response = freedborg_cddb_response $self, "/freedb_search_fmt.php?cat=${cat}&id=${id}" ; 164 | return (CDDB_ABORT, undef) unless defined $response ; 165 | 166 | my $cd ; 167 | $cd->{CAT} = $cat ; 168 | $cd->{ID} = $id ; 169 | 170 | foreach my $line (split /\n/, $response) { 171 | if ($line =~ m/tracks: (\d+)/i) { 172 | $cd->{TRACKS} = $1 ; 173 | } elsif ($line =~ m/total time: ([\d:]+)/i) { 174 | $cd->{"TOTAL TIME"} = $1 ; 175 | } elsif ($line =~ m/genre: (\w+)/i) { 176 | $cd->{GENRE} = $1 ; 177 | } elsif ($line =~ m/id3g: (\d+)/i) { 178 | $cd->{ID3G} = $1 ; 179 | } elsif ($line =~ m/year: (\d+)/i) { 180 | $cd->{DATE} = $1 ; 181 | } elsif ($line =~ m@ *(\d+)\. *(-?[\d:]+)(.*)@) { 182 | # '-?' because there are some buggy entries... 183 | my %track = ( TITLE => $3, TIME => $2 ) ; 184 | $cd->{$1} = \%track ; 185 | } elsif ($line =~ m@

(.+ / .+)

@) { 186 | if (defined $name) { 187 | if ($name ne $1) { 188 | Lltag::Misc::print_warning (" ", "Found CD name '$1' instead of '$name', this entry might be corrupted") ; 189 | } 190 | } else { 191 | $name = $1 ; 192 | } 193 | } 194 | } 195 | 196 | return (CDDB_SUCCESS, undef) 197 | unless defined $name ; 198 | 199 | # FIXME: are we sure no artist or album may contain " / " ? 200 | $name =~ m@^(.+) / (.+)$@ ; 201 | $cd->{ARTIST} = $1 ; 202 | $cd->{ALBUM} = $2 ; 203 | 204 | # FIXME: check number and indexes of tracks ? 205 | 206 | return (CDDB_SUCCESS, $cd) ; 207 | } 208 | 209 | sub freedborg_cddb_query_cd_by_keywords_usage { 210 | my $indent = shift ; 211 | print $indent." => CDDB query for CD matching the keywords\n" ; 212 | print $indent." Search in all CD categories within fields 'artist' and 'title' by default\n" ; 213 | print $indent." cats=foo+bar => Search in CD categories 'foo' and 'bar' only\n" ; 214 | print $indent." fields=all => Search keywords in all fields\n" ; 215 | print $indent." fields=foo+bar => Search keywords in fields 'foo' and 'bar'\n" ; 216 | print $indent."/ => CDDB query for CD matching category and id\n" ; 217 | } 218 | 219 | my $freedborg_cddb_backend = { 220 | cddb_query_cd_by_keywords => \&freedborg_cddb_query_cd_by_keywords, 221 | cddb_query_tracks_by_id => \&freedborg_cddb_query_tracks_by_id, 222 | cddb_query_cd_by_keywords_usage => \&freedborg_cddb_query_cd_by_keywords_usage, 223 | } ; 224 | 225 | ######################################### 226 | # tracktype.org specific code 227 | # USED since november 2006 228 | ######################################### 229 | 230 | sub tracktypeorg_cddb_response { 231 | my $self = shift ; 232 | my $path = shift ; 233 | my $postdata = shift ; 234 | 235 | my $response ; 236 | print " Sending CDDB request...\n" ; 237 | if (defined $postdata) { 238 | print " 'POST $path'\n" if $self->{verbose_opt} ; 239 | $response = $browser->post( 240 | "http://" 241 | . $self->{cddb_server_name} 242 | . ($self->{cddb_server_port} != 80 ? $self->{cddb_server_port} : "") 243 | . $path, 244 | $postdata 245 | ) ; 246 | } else { 247 | print " 'GET $path'\n" if $self->{verbose_opt} ; 248 | $response = $browser->get( 249 | "http://" 250 | . $self->{cddb_server_name} 251 | . ($self->{cddb_server_port} != 80 ? $self->{cddb_server_port} : "") 252 | . $path 253 | ) ; 254 | } 255 | 256 | if (!$response->is_success) { 257 | Lltag::Misc::print_error (" ", 258 | "HTTP request to CDDB server (" 259 | . $self->{cddb_server_name} .":". $self->{cddb_server_port} 260 | . ") failed.") ; 261 | return undef ; 262 | } 263 | if ($response->content_type ne 'text/plain') { 264 | Lltag::Misc::print_error (" ", 265 | "Weird CDDB response (type ".$response->content_type.") from server " 266 | . $self->{cddb_server_name} .":". $self->{cddb_server_port} 267 | . ".") ; 268 | return undef ; 269 | } 270 | 271 | my $content = $response->content ; 272 | 273 | # deal with windows line-break 274 | $content =~ s/\r\n/\n/g ; 275 | 276 | # convert from utf8 if not using a utf8 locale 277 | utf8::decode($content) 278 | unless $self->{utf8} ; 279 | 280 | return $content ; 281 | } 282 | 283 | sub tracktypeorg_cddb_query_cd_by_keywords { 284 | my $self = shift ; 285 | my $keywords = shift ; 286 | 287 | my %postdata = ( "hello" => "lltag", 288 | "proto" => 4, 289 | "cmd" => "cddb album $keywords", 290 | ) ; 291 | my $response = tracktypeorg_cddb_response $self, "/~cddb/cddb.cgi", \%postdata ; 292 | return (CDDB_ABORT, undef) unless defined $response ; 293 | 294 | my @lines = (split /\n/, $response) ; 295 | # check status in header 296 | my $header = shift @lines ; 297 | # TODO: check status 298 | 299 | my @cdids = () ; 300 | foreach my $line (@lines) { 301 | if ($line =~ m@^([^ ]+) ([^ ]+) (.+ / .+)$@) { 302 | my %cdid = ( CAT => $1, ID => $2, NAME => $3 ) ; 303 | push @cdids, \%cdid ; 304 | } 305 | } 306 | 307 | return (CDDB_SUCCESS, \@cdids) ; 308 | } 309 | 310 | sub tracktypeorg_cddb_query_tracks_by_id { 311 | my $self = shift ; 312 | my $cat = shift ; 313 | my $id = shift ; 314 | my $name = shift ; 315 | 316 | my $response = tracktypeorg_cddb_response $self, "/freedb/${cat}/${id}" ; 317 | return (CDDB_ABORT, undef) unless defined $response ; 318 | 319 | # TODO: grep for something to be sure it worked 320 | 321 | my $cd ; 322 | $cd->{CAT} = $cat ; 323 | $cd->{ID} = $id ; 324 | $cd->{TRACKS} = 0 ; 325 | 326 | foreach my $line (split /\n/, $response) { 327 | next if $line =~ /^#/ ; 328 | if ($line =~ m/^DISCID=(.+)/) { 329 | if ($id ne $1) { 330 | Lltag::Misc::print_warning (" ", "Found CD id '$1' instead of '$id', this entry might be corrupted") ; 331 | } 332 | } elsif ($line =~ m@^DTITLE=(.*)@) { 333 | if (defined $name) { 334 | if ($name ne $1) { 335 | Lltag::Misc::print_warning (" ", "Found CD name '$1' instead of '$name', this entry might be corrupted") ; 336 | } 337 | } else { 338 | $name = $1 ; 339 | } 340 | } elsif ($line =~ m/^DYEAR=(.*)/) { 341 | $cd->{DATE} = $1 ; 342 | } elsif ($line =~ m/^DGENRE=(.*)/) { 343 | $cd->{GENRE} = $1 ; 344 | } elsif ($line =~ m/^TTITLE(\d+)=(.*)/) { 345 | my $num = $1 + 1; 346 | if ($num != $cd->{TRACKS} + 1) { 347 | Lltag::Misc::print_warning (" ", "Found CD track '$num' instead of '".($cd->{TRACKS}+1)."', this entry might be corrupted") ; 348 | } 349 | my %track = ( TITLE => $2 ) ; 350 | $cd->{$num} = \%track ; 351 | $cd->{TRACKS} = $num ; 352 | } 353 | } 354 | 355 | return (CDDB_SUCCESS, undef) 356 | unless defined $name ; 357 | 358 | # FIXME: are we sure no artist or album may contain " / " ? 359 | $name =~ m@^(.+) / (.+)$@ ; 360 | $cd->{ARTIST} = $1 ; 361 | $cd->{ALBUM} = $2 ; 362 | 363 | return (CDDB_SUCCESS, $cd) ; 364 | } 365 | 366 | sub tracktypeorg_cddb_query_cd_by_keywords_usage { 367 | my $indent = shift ; 368 | print $indent." => CDDB query for CD matching the keywords\n" ; 369 | print $indent." Search in all CD categories within fields 'artist' OR 'album'\n" ; 370 | print $indent."/ => CDDB query for CD matching category and id\n" ; 371 | } 372 | 373 | my $tracktypeorg_cddb_backend = { 374 | cddb_query_cd_by_keywords => \&tracktypeorg_cddb_query_cd_by_keywords, 375 | cddb_query_tracks_by_id => \&tracktypeorg_cddb_query_tracks_by_id, 376 | cddb_query_cd_by_keywords_usage => \&tracktypeorg_cddb_query_cd_by_keywords_usage, 377 | } ; 378 | 379 | my $cddb_backend = $tracktypeorg_cddb_backend ; 380 | 381 | ###################################################### 382 | # interactive menu to browse CDDB, tracks in a CD 383 | 384 | sub cddb_track_usage { 385 | Lltag::Misc::print_usage_header (" ", "Choose Track in CDDB CD") ; 386 | print " => Choose a track of the current CD (current default is Track $previous_track)\n" ; 387 | print " a => Choose a track and do not ask for confirmation anymore\n" ; 388 | print " a => Use default track and do not ask for confirmation anymore\n" ; 389 | print " E => Edit current CD common tags\n" ; 390 | print " V => View the list of CD matching the keywords\n" ; 391 | print " c => Change the CD chosen in keywords query results list\n" ; 392 | print " k => Start again CDDB query with different keywords\n" ; 393 | print " q => Quit CDDB query\n" ; 394 | print " h => Show this help\n" ; 395 | 396 | $cddb_track_usage_forced = 0 ; 397 | } 398 | 399 | sub print_cd { 400 | my $cd = shift ; 401 | map { 402 | print " $_: $cd->{$_}\n" ; 403 | } grep { $_ !~ /^\d+$/ } (keys %{$cd}) ; 404 | my $track_format = " Track %0".(length $cd->{TRACKS})."d: %s%s\n" ; 405 | for(my $i=1; $i <= $cd->{TRACKS}; $i++) { 406 | my $track = $cd->{$i} ; 407 | my $title = "" ; 408 | $title = $track->{TITLE} if exists $track->{TITLE} and defined $track->{TITLE} ; 409 | my $time = "" ; 410 | $time = " ($track->{TIME})" if exists $track->{TIME} and defined $track->{TIME} ; 411 | printf ($track_format, $i, $title, $time) ; 412 | } 413 | } 414 | 415 | sub get_cddb_tags_from_tracks { 416 | my $self = shift ; 417 | my $cd = shift ; 418 | my $tracknumber = undef ; 419 | 420 | # update previous_track to 1 or ++ 421 | $previous_track = 0 422 | unless defined $previous_track ; 423 | $previous_track++ ; 424 | 425 | # if automatic mode and still in the CD, let's go 426 | if ($current_cddb_yes_opt and $previous_track <= $cd->{TRACKS}) { 427 | $tracknumber = $previous_track ; 428 | Lltag::Misc::print_notice (" ", "Automatically choosing next CDDB track, #$tracknumber...") ; 429 | goto FOUND ; 430 | } 431 | 432 | # either in non-automatic or reached the end of the CD, dump the contents 433 | print_cd $cd ; 434 | 435 | # reached the end of CD, reset to the beginning 436 | if ($previous_track == $cd->{TRACKS} + 1) { 437 | $previous_track = 1; 438 | if ($current_cddb_yes_opt) { 439 | Lltag::Misc::print_notice (" ", "Reached the end of the CD, returning to interactive mode") ; 440 | # return to previous confirmation behavior 441 | $current_cddb_yes_opt = $self->{yes_opt} ; 442 | } 443 | } 444 | 445 | cddb_track_usage 446 | if $cddb_track_usage_forced ; 447 | 448 | while (1) { 449 | my $reply = Lltag::Misc::readline (" ", "Enter track index [aEVckq]". 450 | " (default is Track $previous_track, h for help)", "", -1) ; 451 | 452 | # if ctrl-d, abort cddb 453 | $reply = 'q' unless defined $reply ; 454 | 455 | $reply = $previous_track 456 | if $reply eq '' ; 457 | 458 | return (CDDB_ABORT, undef) 459 | if $reply =~ m/^q/ ; 460 | 461 | return (CDDB_ABORT_TO_KEYWORDS, undef) 462 | if $reply =~ m/^k/ ; 463 | 464 | return (CDDB_ABORT_TO_CDIDS, undef) 465 | if $reply =~ m/^c/ ; 466 | 467 | if ($reply =~ m/^E/) { 468 | # move editable values into a temporary hash 469 | my $values_to_edit = {} ; 470 | foreach my $key (keys %{$cd}) { 471 | next if $key eq 'TRACKS' or $key =~ /^\d+$/ ; 472 | $values_to_edit->{$key} = $cd->{$key} ; 473 | delete $cd->{$key} ; 474 | } 475 | # clone them so that we can restore them if canceled 476 | my $values_edited = Lltag::Tags::clone_tag_values ($values_to_edit) ; 477 | # edit them 478 | my $res = Lltag::Tags::edit_values ($self, $values_edited) ; 479 | # replace the edited values with the originals if canceled 480 | $values_edited = $values_to_edit if $res == Lltag::Tags->EDIT_CANCEL ; 481 | # move them back 482 | foreach my $key (keys %{$values_edited}) { 483 | $cd->{$key} = $values_edited->{$key} ; 484 | } 485 | next ; 486 | } 487 | 488 | if ($reply =~ m/^V/) { 489 | print_cd $cd ; 490 | next ; 491 | } ; 492 | 493 | if ($reply =~ m/^a/) { 494 | $reply = $previous_track ; 495 | $current_cddb_yes_opt = 1 ; 496 | } 497 | if ($reply =~ m/^(\d+) *a/) { 498 | $current_cddb_yes_opt = 1 ; 499 | $reply = $1 ; 500 | } 501 | 502 | if ($reply =~ m/^\d+$/ and $reply >= 1 and $reply <= $cd->{TRACKS}) { 503 | $tracknumber = $reply ; 504 | last ; 505 | } 506 | 507 | cddb_track_usage () ; 508 | } 509 | 510 | FOUND: 511 | my $track = $cd->{$tracknumber} ; 512 | # get the track tags 513 | my %values ; 514 | foreach my $key (keys %{$cd}) { 515 | next if $key eq 'TRACKS' or $key =~ /^\d+$/ ; 516 | $values{$key} = $cd->{$key} ; 517 | } 518 | $values{TITLE} = $track->{TITLE} if exists $track->{TITLE} ; 519 | $values{NUMBER} = $tracknumber ; 520 | 521 | # save the previous track number 522 | $previous_track = $tracknumber ; 523 | 524 | return (CDDB_SUCCESS, \%values) ; 525 | } 526 | 527 | ########################################################## 528 | # interactive menu to browse CDDB, CDs in a query results 529 | 530 | sub cddb_cd_usage { 531 | Lltag::Misc::print_usage_header (" ", "Choose CD in CDDB Query Results") ; 532 | print " => Choose a CD in the current keywords query results list\n" ; 533 | print " V => View the list of CD matching the keywords\n" ; 534 | print " k => Start again CDDB query with different keywords\n" ; 535 | print " q => Quit CDDB query\n" ; 536 | print " h => Show this help\n" ; 537 | 538 | $cddb_cd_usage_forced = 0 ; 539 | } 540 | 541 | sub print_cdids { 542 | my $cdids = shift ; 543 | 544 | my $cdid_format = " %0".(length (scalar @{$cdids}))."d: %s (cat=%s, id=%s)\n" ; 545 | for(my $i=0; $i < @{$cdids}; $i++) { 546 | my $cdid = $cdids->[$i] ; 547 | printf ($cdid_format, $i+1, $cdid->{NAME}, $cdid->{CAT}, $cdid->{ID}) ; 548 | } 549 | } 550 | 551 | # returns (SUCCESS, undef) if CDDB returned an bad/empty CD 552 | sub get_cddb_tags_from_cdid { 553 | my $self = shift ; 554 | my $cdid = shift ; 555 | 556 | my $cddb_query_tracks_by_id_func = $cddb_backend->{cddb_query_tracks_by_id} ; 557 | my ($res, $cd) = &{$cddb_query_tracks_by_id_func} ($self, $cdid->{CAT}, $cdid->{ID}, $cdid->{NAME}) ; 558 | return (CDDB_ABORT, undef) if $res == CDDB_ABORT ; 559 | 560 | if (!$cd or !$cd->{TRACKS}) { 561 | print " There is no tracks in this CD.\n" ; 562 | return (CDDB_SUCCESS, undef) ; 563 | } 564 | 565 | $previous_cd = $cd ; 566 | undef $previous_track ; 567 | 568 | return get_cddb_tags_from_tracks $self, $cd ; 569 | } 570 | 571 | sub get_cddb_tags_from_cdids { 572 | my $self = shift ; 573 | my $cdids = shift ; 574 | 575 | AGAIN: 576 | print_cdids $cdids ; 577 | 578 | cddb_cd_usage 579 | if $cddb_cd_usage_forced ; 580 | 581 | while (1) { 582 | my $reply = Lltag::Misc::readline (" ", "Enter CD index [Vkq] (no default, h for help)", "", -1) ; 583 | 584 | # if ctrl-d, abort cddb 585 | $reply = 'q' unless defined $reply ; 586 | 587 | next if $reply eq '' ; 588 | 589 | return (CDDB_ABORT, undef) 590 | if $reply =~ m/^q/ ; 591 | 592 | return (CDDB_ABORT_TO_KEYWORDS, undef) 593 | if $reply =~ m/^k/ ; 594 | 595 | goto AGAIN 596 | if $reply =~ m/^V/ ; 597 | 598 | if ($reply =~ m/^\d+$/ and $reply >= 1 and $reply <= @{$cdids}) { 599 | # do the actual query for CD contents 600 | my ($res, $values) = get_cddb_tags_from_cdid $self, $cdids->[$reply-1] ; 601 | goto AGAIN if $res == CDDB_ABORT_TO_CDIDS or ($res == CDDB_SUCCESS and not defined $values) ; 602 | return ($res, $values) ; 603 | } 604 | 605 | cddb_cd_usage () ; 606 | } 607 | } 608 | 609 | ########################################################## 610 | # interactive menu to browse CDDB, keywords query 611 | 612 | sub cddb_keywords_usage { 613 | Lltag::Misc::print_usage_header (" ", "CDDB Query by Keywords") ; 614 | my $cddb_query_cd_by_keywords_usage_func = $cddb_backend->{cddb_query_cd_by_keywords_usage} ; 615 | &{$cddb_query_cd_by_keywords_usage_func} (" ") ; 616 | print " q => Quit CDDB query\n" ; 617 | print " h => Show this help\n" ; 618 | 619 | $cddb_keywords_usage_forced = 0 ; 620 | } 621 | 622 | sub get_cddb_tags { 623 | my $self = shift ; 624 | my ($res, $values) ; 625 | 626 | if (!$cddb_supported) { 627 | print " Cannot use CDDB without LWP (libwww-perl module).\n" ; 628 | goto ABORT ; 629 | } 630 | 631 | if (defined $previous_cd) { 632 | bless $previous_cd ; 633 | print " Going back to previous CD cat=$previous_cd->{CAT} id=$previous_cd->{ID}\n" ; 634 | ($res, $values) = get_cddb_tags_from_tracks $self, $previous_cd ; 635 | if ($res == CDDB_ABORT_TO_CDIDS) { 636 | bless $previous_cdids ; 637 | ($res, $values) = get_cddb_tags_from_cdids $self, $previous_cdids ; 638 | } 639 | goto OUT if $res == CDDB_SUCCESS ; 640 | goto ABORT if $res == CDDB_ABORT ; 641 | } 642 | 643 | cddb_keywords_usage 644 | if $cddb_keywords_usage_forced ; 645 | 646 | while (1) { 647 | my $keywords ; 648 | if (defined $self->{requested_cddb_query}) { 649 | $keywords = $self->{requested_cddb_query} ; 650 | print " Using command-line given keywords '$self->{requested_cddb_query}'...\n" ; 651 | undef $self->{requested_cddb_query} ; 652 | # FIXME: either put it in the history, or preput it next time 653 | } else { 654 | $keywords = Lltag::Misc::readline (" ", "Enter CDDB query [q] (no default, h for help)", "", -1) ; 655 | # if ctrl-d, abort cddb 656 | $keywords = 'q' unless defined $keywords ; 657 | } 658 | 659 | next if $keywords eq '' ; 660 | 661 | # be careful to match the whole reply, not only the first char 662 | # since multiple chars are valid keyword queries 663 | 664 | goto ABORT 665 | if $keywords eq 'q' ; 666 | 667 | if ($keywords eq 'h') { 668 | cddb_keywords_usage () ; 669 | next ; 670 | } 671 | 672 | # it this a category/id ? 673 | if ($keywords =~ m@^\s*(\w+)/([\da-f]+)\s*$@) { 674 | my $cdid ; 675 | $cdid->{CAT} = $1 ; 676 | $cdid->{ID} = $2 ; 677 | # FIXME: do not show 'c' for goto to CD list in there 678 | ($res, $values) = get_cddb_tags_from_cdid $self, $cdid ; 679 | goto OUT if $res == CDDB_SUCCESS and defined $values ; 680 | goto ABORT if $res == CDDB_ABORT ; 681 | next ; 682 | } 683 | 684 | # do the actual query for CD id with keywords 685 | my $cdids ; 686 | my $cddb_query_cd_by_keywords_func = $cddb_backend->{cddb_query_cd_by_keywords} ; 687 | ($res, $cdids) = &{$cddb_query_cd_by_keywords_func} ($self, $keywords) ; 688 | goto ABORT if $res == CDDB_ABORT ; 689 | 690 | if (!@{$cdids}) { 691 | print " No CD found.\n" ; 692 | next ; 693 | } 694 | 695 | $previous_cdids = $cdids ; 696 | $previous_cd = undef ; 697 | 698 | ($res, $values) = get_cddb_tags_from_cdids $self, $cdids ; 699 | next if $res == CDDB_ABORT_TO_KEYWORDS ; 700 | goto OUT ; 701 | } 702 | 703 | OUT: 704 | goto ABORT if $res == CDDB_ABORT ; 705 | return ($res, $values) ; 706 | 707 | ABORT: 708 | $previous_cdids = undef ; 709 | $previous_cd = undef ; 710 | $previous_track = undef ; 711 | return (CDDB_ABORT, undef); 712 | } 713 | 714 | 1 ; 715 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | lltag (0.14.6) 2 | 3 | * The lltag website has moved to http://bgoglin.free.fr/lltag 4 | * The repository moved to http://github.com/bgoglin/lltag 5 | * The mailing list is now lltag@googlegroups.com 6 | 7 | -- Brice Goglin Sun, 06 Aug 2017 21:45:00 +0200 8 | 9 | lltag (0.14.5) 10 | 11 | * When renaming, use tags even if their name is not upper-case 12 | as usual (reported by Mathieu Roy in Debian bug #666677). 13 | 14 | -- Brice Goglin Tue, 02 Aug 2016 22:15:00 +0100 15 | 16 | lltag (0.14.4) 17 | 18 | * Fix a typo in usage (reported by Jonas Kölker in Gna! bug #17836). 19 | * Fix miscellaneous typos everywhere, reported by Debian's lintian. 20 | 21 | -- Brice Goglin Tue, 02 Aug 2011 13:32:00 +0100 22 | 23 | lltag (0.14.3) 24 | 25 | * Add -T and --preserve-time, and the preserve_time configuration 26 | file option, to preserve file modification time during tagging 27 | (requested by Adam Rosi-Kessel in Gna! bug #12367). 28 | * Do not try to display binary data tags such as cover front picture 29 | (requested by Alexandre Buisse). 30 | * Do not try to initialize readline in non-interactive environment, 31 | only fail if readline is actually needed. 32 | Thanks Miroslaw Zalewski in Debian bug #558831. 33 | * Do not apply colors or bold/underline formatting to output messages 34 | in non-interactive environment. 35 | * Try command-line given user formats (with -F) before those given 36 | in the config file (with 'format = ...'). 37 | * Add %n) %a - %t to the internal format database. 38 | * Add --id3v2, synonym for --mp3v2 (requested by Alexandre Buisse). 39 | * Display basename parsers both with and without path parsers when 40 | listing internal parsers. 41 | * Display the usage when there is nothing to do (requested by Alexandre 42 | Buisse). 43 | * Fix the case of some options in the manpage. 44 | 45 | -- Brice Goglin Sun, 21 Mar 2010 00:06:00 +0100 46 | 47 | lltag (0.14.2) 48 | 49 | * Merge the whole concept of default and additional values into 50 | "explicit tag values" which may be set with either -a, -A, -t, 51 | -n, -d, -g and -c or the generic --tag command line option. 52 | The default_* configuration options are removed. 53 | Based on complaints from Alexandre Buisse and Olivier Schwander. 54 | + Fix the way they are exported in the config file. 55 | * Replace slashes with dashes before renaming 56 | (reported by Mark McEver in Gna! bug #10127). 57 | + Add --rename-slash to change dash into any other string. 58 | * Fix track numbers into actual numbers (without track total). 59 | 60 | -- Brice Goglin Sun, 16 Dec 2007 12:27:00 +0200 61 | 62 | lltag (0.14.1) 63 | 64 | * Make sure the last character of user-provided format is correctly 65 | escaped (reported by Benjamin Saunders). 66 | * Keep CDDB tags in UTF-8 when using a UTF-8 locale 67 | (reported by Tino Keitel in Debian bug #418951). 68 | * Set verbose level to 0 by default so that menu usage information 69 | is not displayed unless the user explicitly request it, either 70 | by passing -v on the command, or by typing 'h' in a menu 71 | (requested by Alexandre Buisse). 72 | + Fix documentation about verbosity levels in the manpages. 73 | * Fix typo in lltag.1 (reported by Georg Neis in Debian bug #438795). 74 | * Fix typo in lltag_config.5 (reported by Emmanuel Jeandel). 75 | 76 | -- Brice Goglin Sat, 22 Sep 2007 11:25:00 +0200 77 | 78 | lltag (0.14) 79 | 80 | * Add an optional ID3v2 tag support for MP3 files using MP3::Tag 81 | (requested by lots of people), enabled with --mp3v2 for now. 82 | + Add --mp3read option to specify whether ID3v1 and v2 should be 83 | read, and in which order (default is 21). 84 | * Large rework of the core loop: 85 | + Display existing tags with the new ones to be set. 86 | + Add O to display existing tags, R to revert to them, Z to reset 87 | to no tags at all, and n to skip tagging and jump to renaming. 88 | + Also try internal parsers when parsing from the main menu even 89 | if -G was not given on the command line. 90 | + Support renaming from existing tags without any need to parse 91 | and/or reapply tags when --rename and --no-tagging are passed 92 | with nothing else (requested by Alexandre Buisse). 93 | + Make sure guessing is enabled as default only when there is 94 | really nothing else to do. 95 | + Make sure we apply additional values to new tags before merging 96 | and defaults at the very end, so that --clear and --append are 97 | correctly processed. 98 | * Tag edition improvements: 99 | + Support edition of existing tags and tags with multiple values. 100 | + Support edition of all CD tags returned by CDDB. 101 | + Add -E/--edit (and edit option in the configuration file) to 102 | edit tags immediately. 103 | * Make the old MP3 backend more safe: 104 | + Check genre and tracknumbers and ignore invalid values to avoid 105 | mp3info failures. 106 | + Add warnings about multiple values and special tags that are not 107 | supported. 108 | * Add -q/--quiet to reduce verbosity, useful to disable displaying 109 | of menu usage when a menu appears for the first time 110 | (requested by Alexandre Buisse). 111 | + The verbose option in the config file is now a verbose level, 112 | with its default being 1. 113 | * Miscellaneous fixes: 114 | + Support removing of all tags. 115 | + Handle Ctrl-d in interactive menus and make it cancel the current 116 | operation without quitting, as opposed to Ctrl-c 117 | (requested by Alexandre Buisse). 118 | + Uniformize the letter to enter to view current values (changed 119 | 'v' into 'V' in the CDDB menus). 120 | * Documentation updates and fixes: 121 | + Add an EXAMPLES section at the end of the lltag.1 manpage 122 | (requested by Stefano Sabatini in Debian bug #406213) and also 123 | insert more basic command lines in the howto.html documentation. 124 | + Explain the difference between default and additional values in 125 | the howto. 126 | + Explain how the strategies are used, how new tags are generated 127 | and applied, in the DESCRIPTION section of lltag.1. 128 | + Have the lltag_config.5 manpage also point to the config template 129 | file provided in the documentation directory. 130 | * Lots of other minor fixes. 131 | 132 | -- Brice Goglin Mon, 09 Apr 2007 11:22:00 +0200 133 | 134 | lltag (0.13.1) 135 | 136 | * Multiple fixes regarding parsing: 137 | + Try to apply internal parsers immediately after reading the formats 138 | file and catch error gracefully instead of dying later horribly. 139 | + Catch errors gracefully when actually parsing filenames too. 140 | + Report the beginning line (instead of a later one) in the formats 141 | file when meeting an invalid parser. 142 | + Check that the formats given either with -F or in the formats file 143 | have correct number of fields to match, and that we actually get the 144 | same number of fields after matching. 145 | + Escape []() correctly in internal parsers (reported by David Mohr). 146 | + Print the regexp when listing internal parsers in verbose mode 147 | (requested by David Mohr). 148 | + Improve documentation about the impact of --spaces on internal parsers. 149 | + Add 2 new internal formats ("%a/%A [%d]" and "%a/%A (%d)") by David 150 | Mohr. 151 | * Multiple cleanup in the documentation: 152 | + Install the config file in the doc/ directory instead of /etc/lltag/ 153 | since it only contains documentation, no actual configuration. 154 | + Move configuration option documentation into the new lltag_config.5 155 | manpage, and the documentation of internal formats database file in 156 | lltag_formats.5. 157 | + Add install-man, uninstall-man, install-doc and uninstall-doc targets 158 | in the Makefile to install manpages and documentation on demand. 159 | + Some fixes in the manpage. 160 | * Hack the parameters that are passed during Perl modules installation 161 | to deal with MakeMaker not using PREFIX in the common way. Without 162 | this change, setting PREFIX=/usr/local on the make install command 163 | line was installing in /usr/local/local/perl (reported by David Mohr). 164 | 165 | -- Brice Goglin Fri, 01 Dec 2006 18:22:00 +0100 166 | 167 | lltag (0.13) 168 | 169 | * Fix CDDB, was broken since the HTTP search interface of freedb.org is 170 | unavailable for now (closes Hamish Moffatt's Debian bug #397626). 171 | + CDDB now uses TrackType.org instead of freedb.org. 172 | - Reorganize the CDDB code to make it easy to use other online CDDB 173 | site, using kind of a backend interface. 174 | + Rewrite HTTP requests using libwww-perl. 175 | + Drop --cddb-proxy and use the HTTP_PROXY variable in the 176 | environment (set it to something like "http://my.proxy.com"). 177 | + libwww-perl (LWP) is now recommended since required for CDDB access. 178 | * Several important fixes regarding internal tag management: 179 | + --clear, --append, additional tags (passed with --tag) 180 | + Handling of multiple values for the same tag. 181 | - When tags have multiple value, do not keep the same value twice. 182 | * Improve manpage and documentation: 183 | + Add "files..." to the synopsis in the manpage since lltag only 184 | operates on files that are given on the command line (not on the 185 | whole directory) and add a message in verbose mode when no files 186 | are given (reported by Hamish Moffatt in Debian bug #397627). 187 | + Add the configuration file option name near the corresponding 188 | command line option in the manpage. 189 | + Add a note, near -g, about the fact that the genre string might 190 | have to match within a list of specified genres. 191 | + Add missing options in the comments in the config file. 192 | + Improve requirements in the README. 193 | + Main description at the beginning of the manpage. 194 | + Various fixes in the manpage. 195 | * --nopath (i.e. -p) is now --no-path (and no_path in the config file). 196 | * When --tag is passed, disable -G by default. 197 | * Change the internal backend API to manipulate hash of values instead 198 | of talking to external programs, to help upcoming backends. 199 | - Conversion from hash to external programs (command line and output 200 | stream) is moved to new helper functions for backends that need them. 201 | 202 | -- Brice Goglin Thu, 16 Nov 2006 22:53:00 +0100 203 | 204 | lltag (0.12.2) 205 | 206 | * Fix %i processing (was broken since 0.12). 207 | * Fix regexp application. 208 | * Update the How-to about automatic CDDB and --cddb-query. 209 | * Improve error messaging. 210 | * Improve messaging in automatic mode. 211 | * Cleanup Perl regexps. 212 | 213 | -- Brice Goglin Mon, 23 Oct 2006 13:38:00 +0200 214 | 215 | lltag (0.12.1) 216 | 217 | * Improve automatization of CDDB: 218 | + Add --cddb-query to search in CDDB automatically from the 219 | command-line with either keywords or category/id. 220 | + Add 'a' and ' a' to switch to automatic CDDB mode 221 | while interactively choosing a track in a CD. 222 | + Fix automization of CDDB with --yes. 223 | * When using 'a' to switch to automatic mode, only change the mode 224 | for the local menu (i.e. automatic parsing does not make automatic 225 | tagging or CDDB usage). 226 | * If CDDB query by keywords finds nothing, go back to keywords query 227 | instead of asking the user to choose in the (empty) list. 228 | * Add %F, %E and %P to be replaced by original basename, extension 229 | and path of the file when renaming. 230 | * Add --type to make file type selection more flexible. 231 | * Use DELETE or to erase a tag when editing. 232 | * Add a easy-to-use backend interface to help adding support for 233 | other file types. 234 | + Backend information (registrations and failures) are displayed 235 | in verbose mode -v). 236 | * Fix Perl modularization, no need to use Exporter module since we 237 | access modular functions by prefixing with the module name. 238 | * Improve some messages. 239 | 240 | -- Brice Goglin Tue, 03 Oct 2006 16:22:00 +0200 241 | 242 | lltag (0.12) 243 | 244 | * Add CDDB support with -C or --cddb (or C in the main menu): 245 | + Search CD ids by keywords, with ability to choose categories 246 | and fields to look in. 247 | + Lookup a CD by giving its category and CD id directly. 248 | + Keep the previous CD contents in memory so that the next track 249 | might be tagged immediately. 250 | + Edit CDDB common tags before extracting a track from a CD. 251 | + Configurable HTTP CDDB server and proxy. 252 | * Rework interactive menus to integrate CDDB better: 253 | + Add a main menu from where CDDB, parsing and editing is called. 254 | + Use 'q' to leave submenus (parsing, CDDB, renaming and editing). 255 | + Use 'q' instead of 's' to skip a file from the main menu. 256 | + Use 'y' to exit editing after save the changes. 257 | + Add 'Q' to exit completely. 258 | + Do not match user replies case-insensitively. 259 | + Large rework of the manpage to document the new menus. 260 | * Some new entries in the How-To. 261 | * Do not write guess option in the generated configuration file 262 | unless explicitely asked by the user. 263 | * Add a doc/ subdirectory: 264 | + The How-To is now also included in the tarball. 265 | * Perl modularization. 266 | * Add the date to the program name when compiling it from SVN. 267 | 268 | -- Brice Goglin Tue, 19 Sep 2006 21:34:00 -0400 269 | 270 | lltag (0.11) 271 | 272 | * Always read existing tags first, allowing to: 273 | + Rename using existing tags when the old filename is useless 274 | (requested by Fabien Wernli). In case of multiple occurence of a tag, 275 | the first one is used. 276 | + Suppport replacing, clearing, and appending well for all types: 277 | - Replacing is now the default for all types. 278 | - Appending is useless for mp3 since only one occurence (the first one) 279 | may be stored in the file. 280 | - Remove the CLEARING, REPLACING OR APPENDING manpage section. 281 | * Add --tag for additional tags, and warn when not storable in in mp3 files. 282 | * Now use %d, -d and DATE instead of %y, -y and YEAR since ogg/flac tags 283 | are dates. 284 | * Use %i instead of %d to ignore some text when matching. 285 | * Fix 'u' confirmation to try the current parser first for next files 286 | (was broken since 0.10). 287 | * 'basename' is now used instead of 'filename' for 'type' in the format 288 | file, 'filename' is still supported for now. 289 | * Do not rename if the new name already exists. 290 | * Fix reading/tagging the date of ogg/flac files. 291 | * Use all-capitals tag names to match ogg/flac. 292 | + default_ option is still supported in the config 293 | for now. 294 | * The indices in the format file may now be full field names instead of 295 | letters. 296 | * Print the default letter in <> instead of capital when confirming/editing. 297 | * Add EDITING TAGS in the manpage. 298 | * Add configuration options for the config file in the manpage. 299 | * Fix quoting of the command-line in --dry-run. 300 | * Large rework of the internal structures. 301 | * Major cleanup of the manpage. 302 | 303 | -- Brice Goglin Sat, 19 Mar 2006 23:56:00 -0500 304 | 305 | lltag (0.10) 306 | 307 | * Use Readline library to make tag editing easier if the installed 308 | readline library is smart, with inline edition of existing tags 309 | and history of last entered values. 310 | * Added -S ans --show-tags to only show file tags instead of tagging 311 | (requested by Stephane Gimenez in Gna! bug #4601). 312 | * Add --regexp and --rename-regexp for basic regexp-replacing in tags 313 | (requested by Jonathan Worth Washington, Gna! bug #4604). 314 | * When renaming with a undefined track number, initialize it to 0. 315 | * When renaming with a track number, make it at least 2 digits 316 | (reported by Emmanuel Jeandel). 317 | * Always write renaming configuration options with --gencfg, even 318 | if renaming was not enabled. 319 | * Cleanup core function names and several messages so that lltag speak 320 | about matching instead of tagging. It avoids getting messages about 321 | tagging while --no-tagging was passed (requested by Stephane Gimenez). 322 | * Use ' instead of # to show spaces between command line argument 323 | with --dry-run (requested by Emmanuel Jeandel). 324 | * Allow to pass ()[] characters in the matching format without having 325 | to escape them. 326 | * Allow all %x in the renaming format when x is not a field letter. 327 | * Fix where the 'd' field (dummy) is allowed. 328 | * Fix the error when no formats file is found. 329 | * Fix typos in the manpage (reported by Emmanuel Jeandel). 330 | 331 | -- Brice Goglin Sat, 19 Nov 2005 02:41:00 -0500 332 | 333 | lltag (0.9.1) 334 | 335 | * Large fix of internal database format files processing: 336 | + Fix formats file parsing (last format was omitted). 337 | + If $HOME/.lltag/formats exists, /etc/lltag/formats is ignored. 338 | + Fix documentation. 339 | * Add --config, --gencfg and /etc/lltag/config to configure lltag behavior 340 | (requested by Stephane Gimenez). 341 | * Do not warn when no file is passed. 342 | 343 | -- Brice Goglin Mon, 17 Oct 2005 20:51:00 +0200 344 | 345 | lltag (0.9) 346 | 347 | * Add rename support with --rename (requested by Stephane Gimenez): 348 | + Add --rename-min to lowcase tags before renaming. 349 | + Add --rename-sep to replace spaces before renaming. 350 | + Add --rename-ext to assume the extension in provided in the rename format. 351 | + Add --no-tagging to rename file without actually tagging them. 352 | * When compiling from SVN, add a +svn suffix to the version. 353 | * Reorder usage in a comprehensible way. 354 | * Do not be case-sensitive when looking at file extensions to guess their type. 355 | 356 | -- Brice Goglin Sun, 2 Oct 2005 19:06:00 +0200 357 | 358 | lltag (0.8) 359 | 360 | * Add FLAC support (requested by Stephane Gimenez). 361 | * Add --maj to upcase first letters in tags (requested by Stephane Gimenez). 362 | 363 | -- Brice Goglin Tue, 27 Sep 2005 23:03:00 +0200 364 | 365 | lltag (0.7.2) 366 | 367 | * Fix compilation and installation when DESTDIR is overridden. 368 | * Fix make uninstall. 369 | * Add a VERSION file. 370 | * Update all contact addresses and URLs since the project is 371 | now hosted by Gna!. 372 | 373 | -- Brice Goglin Thu, 22 Sep 2005 10:40:00 +0200 374 | 375 | lltag (0.7.1) 376 | 377 | * Add a warning (with reference to README) when system fails with ENOENT or EPERM. 378 | * Cleanup system usage. 379 | * Update contact address to gna.org and add it to README. 380 | 381 | -- Brice Goglin Wed, 7 Sep 2005 17:57:18 +0200 382 | 383 | lltag (0.7) 384 | 385 | * Add --clear to force emptying of mp3 tags (default for ogg). 386 | * Add --append to force appending of ogg tags 387 | (default is overwrite, append is impossible for mp3). 388 | * Add a section about clearing, replacing or appending in the manpage. 389 | * Add 'comment' tag support (with -c or %c). 390 | * Add missing options to the command line in the man page. 391 | * Cleanup the way current values are shown, 392 | show and . 393 | * Accept both CLEAR and when editing fields. 394 | * Cleanup system usage when tagging command, get the output 395 | and show it in case of error. 396 | * Don't add () after sub prototypes. 397 | 398 | -- Brice Goglin Tue, 16 Aug 2005 00:39:18 +0200 399 | 400 | lltag (0.6.2) 401 | 402 | * Fix Debian tarball generation. 403 | * Include COPYING and Changes in the tarball. 404 | * Add a README. 405 | * Document editing in the manpage (CLEAR and ). 406 | 407 | -- Brice Goglin Fri, 12 Aug 2005 22:26:56 +0200 408 | 409 | lltag (0.6.1) 410 | 411 | * Guess by default. 412 | * Fix wrong processing of return values when internal ou user parsing loops fail. 413 | * Add clean target to the Makefile. 414 | * A few typos. 415 | 416 | -- Brice Goglin Sat, 30 Jul 2005 23:24:02 +0200 417 | 418 | lltag (0.6) 419 | 420 | * Allow to edit fields with 'e' during confirmation. 421 | * Allow to see what would be done with 'v' during confirmation. 422 | * Fix a few missing capitalization. 423 | * Print "Nothing to do" when no field has to be tagged. 424 | 425 | -- Brice Goglin Sat, 30 Jul 2005 21:36:27 +0200 426 | 427 | lltag (0.5.5) 428 | 429 | * Allow to pass multiple |-separated chars or strings to --sep. 430 | * When a field appears multiple times in the format, lltag does not print all matched values. 431 | It checks that they are identical, prints a warning if not, and keeps the first one. 432 | * Do not tag when there's nothing to tag (fix bogus mp3info/vorbiscomment invocation). 433 | * Add comments at the beginning of formats. 434 | * Fix matching in man page. 435 | 436 | -- Brice Goglin Sat, 30 Jul 2005 13:26:54 +0200 437 | 438 | lltag (0.5.4) 439 | 440 | * Allow to only set default values (no user formats given, no guess). 441 | * Clean acceptable behavior, especially confirmation possibilities in various cases. 442 | 443 | -- Brice Goglin Mon, 23 May 2005 00:35:26 +0200 444 | 445 | lltag (0.5.3) 446 | 447 | * Fix short and long option processing to set default field values. 448 | * Add --list and -L to list internal formats. 449 | * Add --version and -V to show version. 450 | * Show version in usage. 451 | * Automatically set version in the binary during install. 452 | 453 | -- Brice Goglin Thu, 12 May 2005 21:27:15 +0200 454 | 455 | lltag (0.5.2) 456 | 457 | * "" means "yes" when confirming tagging. 458 | * Use lltag.in to replace /etc/ directories during install. 459 | * Add format file description to the manpage. 460 | * Generate Gentoo tarball in the Makefile (thanks to David Baelde for the ebuild). 461 | 462 | -- Brice Goglin Sun, 10 Apr 2005 00:41:08 +0200 463 | 464 | lltag (0.5.1) 465 | 466 | * Fix -G option for directory containing spaces. 467 | * Add missing '%a - %A' path format. 468 | 469 | -- Brice Goglin Wed, 30 Mar 2005 23:53:34 +0200 470 | 471 | lltag (0.5) 472 | 473 | * Add /etc/lltag/formats file to store formats. 474 | * Move all internal formats to /etc/lltag/formats. 475 | * Also read $HOME/.lltag/formats. 476 | * Support --spaces option for user-specified formats. 477 | * Add confirmation to manpage. 478 | * Cleanup a few messages. 479 | * Cleanup tag_file return values. 480 | * A few other cleanups. 481 | 482 | -- Brice Goglin Sun, 13 Mar 2005 20:24:22 +0100 483 | 484 | lltag (0.4.2) 485 | 486 | * Add -R|--recursive option. 487 | * Check that files are really files. 488 | * Fix path matching which could lead to parts of the path matched with 489 | the filename. 490 | 491 | -- Brice Goglin Mon, 7 Mar 2005 00:00:04 +0100 492 | 493 | lltag (0.4.1) 494 | 495 | * Add manpage. 496 | * Add --sep option to ask replacement of a character by a space in tags. 497 | * Do not accept multiple spaces by default. 498 | * Add --spaces to accept multiple spaces. 499 | * Accept spaces limiting path subpart. 500 | * Add message when no format was found. 501 | * Add the missing --format option equivalent to -F. 502 | * Use GetOpt to handle options properly. 503 | * Cleanup field showing. 504 | * Cleanup internal structure names. 505 | 506 | -- Brice Goglin Sun, 6 Mar 2005 22:15:45 +0100 507 | 508 | lltag (0.4) 509 | 510 | * Support guessing formats with -G. 511 | - Internal filename parsers are (with variable whitespaces) 512 | "%n - %a - %t", "%n - %t", "%n[.)] %t" 513 | "%a - %n - %t", "%a - %t", "%t" 514 | - Internal path parsers are (with variable whitespaces and 515 | forgotten first directories) 516 | "%a/%a - %A", "%a/%A", "%a", "%A" 517 | * Add --yes to force tagging without confirmation when guessing. 518 | * Add --ask to force confirmation when not guessing. 519 | * Add an equivalent long option for each short option. 520 | * Support mixing options and files on the command line. 521 | * Add verbose message when setting default values. 522 | * Show command line in verbose mode even if --dry-run wasn't passed. 523 | * Cleanup command line showing. 524 | * Cleanup wrong option detection. 525 | * Fix format string matching to ensure the whole filename will be used. 526 | * Change several ERROR messages into less noisy messages. 527 | * Show usage on stdout instead of stderr. 528 | * Fix usage. 529 | 530 | -- Brice Goglin Sun, 6 Mar 2005 15:33:42 +0100 531 | 532 | lltag (0.3.1) 533 | 534 | * Add author and homepage to usage. 535 | 536 | -- Brice Goglin Tue, 1 Mar 2005 02:14:44 +0100 537 | 538 | lltag (0.3) 539 | 540 | * Format must now be passed with -F. 541 | * Multiple formats are allowed, the first that matches will be used. 542 | * Extension is no-longer included in format. 543 | * Support both ogg and mp3 tagging. 544 | * --ogg and --mp3 options to force tagging instead of by-extension detection. 545 | * Detect wrong %x code in format. 546 | * Detect and warn about problematic regexps. 547 | * Cleanup of messages. 548 | * Large cleanup of the code. 549 | * Split Debian and upstream changelogs. 550 | * Add a Makefile. 551 | 552 | -- Brice Goglin Sun, 27 Feb 2005 11:47:16 +0100 553 | 554 | llmp3tag (0.2) 555 | 556 | * Rewrite parsing of the format to create indexed regexp to fix some issues. 557 | * %n now only matches numbers. 558 | * Some cleanup in the code. 559 | 560 | -- Brice Goglin Sun, 27 Feb 2005 02:14:50 +0100 561 | 562 | llmp3tag (0.1) 563 | 564 | * Initial release. 565 | 566 | -- Brice Goglin Wed, 15 Dec 2004 19:47:00 +0200 567 | -------------------------------------------------------------------------------- /lltag.1: -------------------------------------------------------------------------------- 1 | .\" Process this file with 2 | .\" groff -man -Tascii foo.1 3 | .\" 4 | .TH LLTAG 1 "NOVEMBER 2006" 5 | 6 | 7 | 8 | 9 | 10 | .SH NAME 11 | lltag - tag and rename mp3/ogg/flac music files automagically 12 | 13 | 14 | 15 | 16 | .SH SYNOPSIS 17 | .B lltag 18 | .RB [ -C ] 19 | .RB [ -E ] 20 | .RB [ "-F " ] 21 | .RB [ -G ] 22 | .RB [ -p ] 23 | .RB [ "-a " ] 24 | .RB [ "-t " ] 25 | .RB [ "-A <album>" ] 26 | .RB [ "-n <number>" ] 27 | .RB [ "-g <genre>" ] 28 | .RB [ "-d <date>" ] 29 | .RB [ "-c <comment>" ] 30 | .RB [ "--tag <TAG=value>" ] 31 | .RB [ --spaces ] 32 | .RB [ --maj ] 33 | .RB [ "--sep\ <s1|s2|...>" ] 34 | .RB [ "--regexp <regexp>" ] 35 | .RB [ --mp3/--ogg/--flac ] 36 | .RB [ "--type <type>" ] 37 | .RB [ --clear ] 38 | .RB [ --append ] 39 | .RB [ --no-tagging ] 40 | .RB [ --preserve-time ] 41 | .RB [ "--rename <format>" ] 42 | .RB [ --rename-ext ] 43 | .RB [ --rename-min ] 44 | .RB [ "--rename-sep <sep>" ] 45 | .RB [ "--rename-slash <char>" ] 46 | .RB [ "--rename-regexp <regexp>" ] 47 | .RB [ --dry-run ] 48 | .RB [ --yes ] 49 | .RB [ --ask ] 50 | .RB [ "--cddb-query <query>" ] 51 | .RB [ "--cddb-server <server[:port]>" ] 52 | .RB [ -R ] 53 | .RB [ -v ] 54 | .RB [ -q ] 55 | .RB [ "--config <file>" ] 56 | .RB [ "--gencfg <file>" ] 57 | .RB [ -S ] 58 | .RB [ "--show-tags <tags>" ] 59 | .RB [ -L ] 60 | .RB [ -V ] 61 | .RB [ -h ] 62 | .RB files... 63 | .\" 64 | 65 | 66 | 67 | 68 | .SH DESCRIPTION 69 | .B lltag 70 | is a command-line tool to automagically set tags of MP3, OGG or FLAC 71 | files. There are several ways to obtains the tags that will be set: 72 | 73 | .TP 74 | .B Parsing the filename 75 | .B lltag 76 | may either parse the filename using its own internal database 77 | of commonly-used formats (default behavior, or when 78 | .B -G 79 | is passed), or some user-provided formats (when 80 | .B -F 81 | is passed). 82 | 83 | .TP 84 | .B Requesting from CDDB 85 | .B lltag 86 | may access an online CDDB database to extract tags from a track of a CD (when 87 | .B -C 88 | is passed). 89 | 90 | .TP 91 | .B Explicitly setting values 92 | .B lltag 93 | provides a set of command-line option to manually set various tags. 94 | 95 | .TP 96 | .B Manually editing values 97 | .B lltag 98 | provides an interactive interface to edit existing values 99 | or any value provided by the above strategies. 100 | 101 | .P 102 | Each time, a new audio file is processed, 103 | .B lltag 104 | starts by trying to obtain new tags depending on the behavior options 105 | given by the user. 106 | 107 | First, if a preferred parser has been selected before, it is used to try 108 | to parse the new filename. 109 | Then, if editing is enabled 110 | .RB ( -E ), 111 | the user will be able to modify existing tag values. 112 | Then, if CDDB is enabled 113 | .RB ( -C ), 114 | the user will be asked to request tags from the 115 | online CDDB database. 116 | Then, if the user provided any parsing format 117 | .RB ( -F ), 118 | or if guessing is enabled 119 | .RB ( -G ), 120 | .B lltag 121 | will attempt to parse the filename. 122 | 123 | Note that if no behavior is chosen at all on command-line, including no 124 | renaming option, then parsing with the internal format database will be 125 | used by default (as if 126 | .B -G 127 | had been passed). 128 | 129 | As soon as one of the above strategies succeeds, 130 | .B lltag 131 | jumps to the main menu 132 | where the user may either accept new tags or select another behavior (see 133 | .B MAIN MENU 134 | in 135 | .B INTERACTIVE MENUS 136 | below for details). 137 | If 138 | .B --yes 139 | has been passed, or if automatic mode has been previously enabled in the menu, 140 | it will proceed with tagging (and renaming if requested) and go on with the 141 | next file. 142 | 143 | The new tags that the selected strategy returns will be appended with 144 | the explicit values given with 145 | .BR " -a ", " -t ", " -A ", " -g ", " -n ", " -d ", " -c " or " --tag . 146 | They will then either replace (default), clear and replace 147 | .RB ( --clear ) 148 | or append to 149 | .RB ( --append ) 150 | the existing tags in the target file. 151 | 152 | Once the tags are known, a backend program or library is used to apply 153 | them to the audio file (unless 154 | .B --no-tagging 155 | is passed). 156 | .RB "The " MP3::Tag " Perl module or " mp3info 157 | is used to tag 158 | .B MP3 159 | files while 160 | .B vorbiscomment 161 | is used for 162 | .B OGG 163 | files, and 164 | .B metaflac 165 | is used for 166 | .B FLAC 167 | files. 168 | 169 | In the end, when called with 170 | .BR --rename , 171 | the target file will also be renamed according to a user-provided format 172 | filled with the tag values. 173 | 174 | 175 | 176 | 177 | .SH OPTIONS 178 | 179 | .TP 180 | .BI "-A, --ALBUM" " <album>" 181 | Add a value for the \fIALBUM\fR tag. 182 | 183 | .TP 184 | .BI "-a, --ARTIST" " <artist>" 185 | Add a value for the \fIARTIST\fR tag. 186 | 187 | .TP 188 | .BI "--append" 189 | Force appending of ogg/flac tags 190 | (instead of replacing existing tags). 191 | The corresponding configuration file option is 192 | .IR append_tags . 193 | 194 | Since mp3 files may only get one tag of each type, appending 195 | does nothing, the first occurrence only is stored. 196 | 197 | .TP 198 | .B --ask 199 | Always ask confirmation to the user before using a user-specified 200 | parser. By default, all actions require confirmation, except when 201 | a matching user-specified format is found. 202 | The corresponding configuration file option is 203 | .IR ask . 204 | See 205 | .B PARSING MENU 206 | in 207 | .B INTERACTIVE MENUS 208 | below for details. 209 | 210 | .TP 211 | .B "-C, --cddb" 212 | Try to find tags in the CDDB online database before trying to parse filenames. 213 | The queries are sent using the HTTP interface, which means a HTTP proxy might 214 | be used when required. 215 | The corresponding configuration file option is 216 | .IR cddb . 217 | 218 | .TP 219 | .BI "--cddb-query" " <keywords>" 220 | .TP 221 | .BI "--cddb-query" " <cat>/<id>" 222 | Automatically search for CD matching <keywords> or matching category <cat> 223 | and id <id> 224 | as if the user passed 225 | .B --cddb 226 | and entered the query interactively in the module. 227 | 228 | .TP 229 | .BI "--cddb-server" " <server[:port]>" 230 | Change the CDDB server, and eventually its port. 231 | The default is 232 | .BR www.freedb.org:80 . 233 | The corresponding configuration file options are 234 | .IR cddb_server_name " and " cddb_server_port . 235 | If a HTTP proxy is required to access the internet, 236 | the environment variable 237 | .B HTTP_PROXY 238 | may be used (set to something like "http://my.proxy.com"). 239 | 240 | .TP 241 | .BI "-c, --COMMENT" " <comment>" 242 | Add a value for the \fICOMMENT\fR tag. 243 | 244 | .TP 245 | .B --clear 246 | Force clearing of all tags before tagging 247 | (instead of replacing existing tags). 248 | The corresponding configuration file option is 249 | .IR clear_tags . 250 | 251 | .TP 252 | .BI --config " <file>" 253 | Parse additional configuration file. 254 | See 255 | .B CONFIGURATION FILES 256 | below for details. 257 | 258 | .TP 259 | .BI "-d, --DATE" " <date>" 260 | Add a value for the \fIDATE\fR tag. 261 | Note that the ID3 date tag may only store 4 characters (for a year). 262 | 263 | .TP 264 | .B --dry-run 265 | Do not really tag files, just show what would have been done. 266 | The corresponding configuration file option is 267 | .IR dry_run . 268 | 269 | .TP 270 | .B -E, --edit 271 | Edit tags immediately. 272 | 273 | .TP 274 | .BI "-F, --format" " <format>" 275 | Add the specified format string to the list of user-supplied formats. 276 | The corresponding configuration file option is 277 | .IR format . 278 | Might be used several times to try different formats. 279 | See 280 | .B FORMAT 281 | below for details. 282 | 283 | .TP 284 | .B --flac 285 | Tag all files as FLAC files, using the FLAC backend (based on \fBmetaflac\fR). 286 | The corresponding configuration file option is 287 | .IR type . 288 | 289 | .TP 290 | .B "-G, --guess" 291 | Guess format using the internal database if no user-specified format 292 | worked (default behavior). 293 | The corresponding configuration file option is 294 | .IR guess . 295 | 296 | .TP 297 | .BI "-g, --GENRE" " <genre>" 298 | Add a value for the \fIGENRE\fR tag. 299 | While some file types accept any string as a genre, some others 300 | (especially ID3v1 tags in MP3 files) require the string to match 301 | within a list of specified genres. 302 | 303 | .TP 304 | .BI --gencfg " <file>" 305 | Generate configuration file. 306 | See 307 | .B CONFIGURATION FILES 308 | below for details. 309 | 310 | .TP 311 | .B "-h, --help" 312 | Print a usage message and exit. 313 | 314 | .TP 315 | .B "-L, --list" 316 | List internal formats. 317 | 318 | .TP 319 | .B --maj 320 | Upcase the first letter of each word in tags. 321 | The corresponding configuration file option is 322 | .IR maj . 323 | 324 | .TP 325 | .B --mp3 326 | Tag all files as MP3 files, using the MP3 backend 327 | (based on either \fBmp3info\fR or \fBMP3::Tag\fR). 328 | The corresponding configuration file option is 329 | .IR type . 330 | 331 | .TP 332 | .B --mp3v2, --id3v2 333 | Enable the experimental MP3 ID3v2-aware backend (based on \fRMP3::Tag\fR) 334 | instead of the old ID3v1-only backend. 335 | 336 | .TP 337 | .B --mp3read=[1][2] 338 | Configure how the MP3v2 backend reads and merges ID3v1 and v2 tags. 339 | By default, v1 are appended to v2 (\fB21\fR). 340 | If set to \fB1\fR, only v1 are read. 341 | If set to \fB2\fR, only v2 are read. 342 | If set to \fB12\fR, v2 are appended to v1. 343 | Note that merging/appending takes care of removing duplicates. 344 | 345 | .TP 346 | .BI "-n, --NUMBER" " <number>" 347 | Add a value for the \fINUMBER\fR tag. 348 | 349 | .TP 350 | .B --no-tagging 351 | Do not actually tag files. This might be used to rename files 352 | without tagging. 353 | The corresponding configuration file option is 354 | .IR no_tagging . 355 | 356 | .TP 357 | .B -T, --preserve-time 358 | Preserve file modification time during tagging. 359 | The corresponding configuration file option is 360 | .IR preserve_time . 361 | 362 | .TP 363 | .B --ogg 364 | Tag all files as OGG files, using the OGG backend (based on \fBvorbiscomment\fR). 365 | The corresponding configuration file option is 366 | .IR type . 367 | 368 | .TP 369 | .B "-p, --no-path" 370 | Do not consider the path of files when matching. 371 | The corresponding configuration file option is 372 | .IR no_path . 373 | 374 | .TP 375 | .B "-q, --quiet" 376 | Decrease message verbosity. 377 | The corresponding configuration file option is 378 | .I verbose 379 | which indicates the verbose level. 380 | See 381 | .BR -v 382 | for details about the existing verbosity levels. 383 | 384 | .TP 385 | .B "-R, --recursive" 386 | Recursively search for files in subdirectories that are given on 387 | the command line. 388 | The corresponding configuration file option is 389 | .IR recursive . 390 | 391 | .TP 392 | .BI --regexp " <[tag,tag:]s/from/to/>" 393 | Replace \fIfrom\fR with \fIto\fR in tags before tagging. 394 | The corresponding configuration file option is 395 | .IR regexp . 396 | If several tags (comma-separated) prefix the regexp, replacement is 397 | only applied to the corresponding fields. 398 | This option might be used multiple times to specify multiple replacing. 399 | 400 | .TP 401 | .BI --rename " <format>" 402 | After tagging, rename the file according to the format. 403 | The corresponding configuration file option is 404 | .IR rename_format . 405 | The format is filled using the first occurrence of each tag that was 406 | used to tag the file right before. 407 | It means that an old existing tag may be used if no new one replaced 408 | it and 409 | .B --clear 410 | was not passed. 411 | 412 | By default, confirmation is asked before tagging. 413 | See 414 | .B RENAMING MENU 415 | in 416 | .B INTERACTIVE MENUS 417 | below for details. 418 | 419 | .TP 420 | .B --rename-ext 421 | Assume that the file extension is provided by the rename format 422 | instead of automatically adding the extension corresponding to 423 | the file type. 424 | The corresponding configuration file option is 425 | .IR rename_ext . 426 | 427 | .TP 428 | .B --rename-min 429 | Lowcase all tags before renaming. 430 | The corresponding configuration file option is 431 | .IR rename_min . 432 | 433 | .TP 434 | .BI --rename-regexp " <[tag,tag:]s/from/to/>" 435 | Replace \fIfrom\fR with \fIto\fR in tags before renaming. 436 | If several tags (comma-separated) prefix the regexp, replacement is 437 | only applied to the corresponding fields. 438 | This option might be used multiple times to specify multiple replacing. 439 | The corresponding configuration file option is 440 | .IR rename_regexp . 441 | 442 | .TP 443 | .BI --rename-sep " <sep>" 444 | Replace spaces with sep when renaming. 445 | The corresponding configuration file option is 446 | .IR rename_sep . 447 | See 448 | .B --rename-regexp 449 | for a more general replace feature. 450 | 451 | .TP 452 | .BI --rename-slash " <char>" 453 | Replace slashes with char when renaming. 454 | The corresponding configuration file option is 455 | .IR rename_slash . 456 | See 457 | .B --rename-regexp 458 | for a more general replace feature. 459 | 460 | .TP 461 | .B -S 462 | Instead of tagging, lltag shows the tags that are currently set in 463 | files. 464 | See 465 | .B --show-tags 466 | to show only some tags. 467 | 468 | .TP 469 | .BI --sep " <string|string>" 470 | Replace the specified characters or strings with space in tags. 471 | The corresponding configuration file option is 472 | .IR sep . 473 | They have to be |-separated. 474 | See 475 | .B --regexp 476 | for a more general replace feature. 477 | 478 | .TP 479 | .BI --show-tags " <tag1,tag2,...>" 480 | Instead of tagging, lltag shows tags that are currently set in files. 481 | The argument is a comma separated list of tag types 482 | .RI ( artist ", " title ", " album ", " number ", " 483 | .IR genre ", " date ", " comment " or " all ). 484 | See also 485 | .B -S 486 | to show all tags. 487 | 488 | .TP 489 | .B --spaces 490 | Allow multiple or no space instead of only one when matching. 491 | Also allow spaces limiting path elements. 492 | The corresponding configuration file option is 493 | .IR spaces . 494 | See also 495 | .B INTERNAL FORMATS 496 | to get the detailled impact of this option. 497 | 498 | .TP 499 | .BI "-t, --TITLE" " <title>" 500 | Add a value for the \fITITLE\fR tag. 501 | 502 | .TP 503 | .BI "--tag" " <TAG=value>" 504 | Add an explicit tag value. 505 | The corresponding configuration file option is 506 | .IR tag . 507 | Might be used several times, even for the same tag. 508 | When setting a common tag, it is similar to using 509 | .BR -a ", " -A ", " -t ", " -n ", " -g ", " -d " or " -c . 510 | Note that mp3 tags do not support whatever 511 | .IR TAG . 512 | 513 | .TP 514 | .BI "--type" " <type>" 515 | Tag all files as 516 | .B <type> 517 | files. 518 | The corresponding configuration file option is 519 | .IR type . 520 | 521 | .TP 522 | .B "-v, --verbose" 523 | Increase message verbosity. 524 | The corresponding configuration file option is 525 | .I verbose 526 | which indicates the verbose level. 527 | 528 | The default verbosity level is 0 to show only important messages. 529 | Other possible values are 1 to show usage information when a menu 530 | is displayed for the first time, and 2 to always show usage 531 | information before a menu appears. 532 | 533 | See also 534 | .BR -q . 535 | 536 | .TP 537 | .B "-V, --version" 538 | Show the version. 539 | 540 | .TP 541 | .B --yes 542 | Always accept tagging without asking the user. 543 | The corresponding configuration file option is 544 | .IR yes . 545 | By default user-specified format matching is accepted 546 | while guess format matching is asked for confirmation. 547 | 548 | Also always accept renaming without asking the user. 549 | 550 | 551 | 552 | 553 | .SH INTERACTIVE MENUS 554 | When not running with 555 | .BI --yes , 556 | the user has to tell lltag what to do. 557 | Files are processed one after the other, with the following steps: 558 | .TP 559 | .B * 560 | If the 561 | .B preferred 562 | parser exists, try to apply it. 563 | .TP 564 | .B * 565 | If failed, if 566 | .B --cddb 567 | was passed, trying a CDDB query. 568 | .TP 569 | .B * 570 | If failed, try the user-provided formats, if any. 571 | .TP 572 | .B * 573 | If failed, if no user-format were passed, or if 574 | .B -G 575 | was passed, try the internal formats. 576 | .TP 577 | .B * 578 | Then we have a list of tags to apply, we may apply them, edit them, 579 | or go back to a CDDB query or trying to parse the filename again. 580 | .TP 581 | .B * 582 | Then, if 583 | .B --rename 584 | was passed, the file is renamed. 585 | 586 | When hitting 587 | .B Ctrl-d 588 | at the beginning of an empty line 589 | .RB ( EOF ), 590 | the general behavior is to cancel the current operation 591 | without leaving. 592 | 593 | We now describe all interactive menus in detail. 594 | 595 | 596 | 597 | 598 | .SS MAIN MENU 599 | Once some tags have been obtained by either CDDB, parsing or the explicit 600 | values given on the command line, the main menu opens to either change the tags 601 | or apply them: 602 | .TP 603 | .B y 604 | Yes, use these tags (default) 605 | .TP 606 | .B a 607 | Always yes, stop asking for a confirmation 608 | .TP 609 | .B P 610 | Try to parse the file, see 611 | .B PARSING MENU 612 | .TP 613 | .B C 614 | Query CDDB, see 615 | .B CDDB MENUS 616 | .TP 617 | .B E 618 | Edit values, see 619 | .B EDITING MENU 620 | .TP 621 | .B D 622 | Only use explicit values, forget about CDDB or parsed tags 623 | .TP 624 | .B Z 625 | Reset to no tag values at all 626 | .TP 627 | .B R 628 | Revert to existing tag values from the current file 629 | .TP 630 | .B O 631 | Display existing tag values in the current file 632 | .TP 633 | .B n 634 | Do not tag this file, jump to renaming (or to the next file if renaming is disabled) 635 | .TP 636 | .BR q " (or " EOF ) 637 | Skip this file 638 | .TP 639 | .B Q 640 | Quit without tagging anything anymore 641 | 642 | 643 | 644 | .SS CDDB MENUS 645 | When the CDDB opens for the first time, the user must enter a query 646 | to choose a CD in the online database. 647 | .TP 648 | .B <space-separated keywords> 649 | CDDB query for CD matching the keywords. 650 | Search in all CD categories within fields artist OR album. 651 | 652 | .\" freedb.org specific manual, not used anymore 653 | .\"Search in all CD categories within fields artist and title by default. 654 | .\"If 655 | .\".B cats=foo+bar 656 | .\"is added, search in CD categories foo and bar only. 657 | .\"If 658 | .\".B fields=all 659 | .\"is added, search keywords in all fields. 660 | .\"If 661 | .\".B fields=foo+bar 662 | .\"is added, search keywords in fields foo and bar. 663 | .\".TP 664 | .\".B <category>/<hexadecinal id> 665 | .\"CDDB query for CD matching category and id 666 | 667 | .TP 668 | .BR q " (or " EOF ) 669 | Quit CDDB query, see 670 | .B MAIN MENU 671 | 672 | .P 673 | Once keywords have been passed as a query to CDDB, a list of matching 674 | CD titles will be displayed. The user then needs to choose one: 675 | .TP 676 | .B <index> 677 | Choose a CD in the current keywords query results list 678 | .TP 679 | .B V 680 | View the list of CD matching the keywords 681 | .TP 682 | .B k 683 | Start again CDDB query with different keywords 684 | .TP 685 | .BR q " (or " EOF ) 686 | Quit CDDB query, see 687 | .B MAIN MENU 688 | 689 | .P 690 | Once a CD have been chosen, the user needs to choose a track 691 | .TP 692 | .B <index> 693 | Choose a track of the current CD 694 | .TP 695 | .B <index> a 696 | Choose a track and do not ask for confirmation anymore 697 | .TP 698 | .B a 699 | Use default track and do not ask for confirmation anymore 700 | .TP 701 | .B E 702 | Edit current CD common tags, see 703 | .B EDITING MENU 704 | .TP 705 | .B v 706 | View the list of CD matching the keywords 707 | .TP 708 | .B c 709 | Change the CD chosen in keywords query results list 710 | .TP 711 | .B k 712 | Start again CDDB query with different keywords 713 | .TP 714 | .BR q " (or " EOF ) 715 | Quit CDDB query, see 716 | .B MAIN MENU 717 | 718 | .P 719 | Note that entering the CDDB menus again will go back to the previous 720 | CD instead of asking the user to query again, so that an entire CD 721 | may be tagged easily. 722 | 723 | 724 | 725 | .SS PARSING MENU 726 | When 727 | .B --ask 728 | is passed or when guessing, each matching will lead to 729 | a confirmation message before tagging. 730 | Available behaviors are: 731 | .TP 732 | .B y 733 | Tag current file with current format. This is the default. 734 | .TP 735 | .B u 736 | Tag current file with current format. 737 | Then use current format for all remaining matching files. 738 | When a non-matching file is reached, stop using this 739 | preferred format. 740 | .TP 741 | .B a 742 | Tag current file with current format. 743 | Then, never asking for a confirmation anymore. 744 | .TP 745 | .B n 746 | Don't tag current file with this format. 747 | Try the next matching format on the current file. 748 | .TP 749 | .B p 750 | When matching is done through combination of a path parser 751 | and a basename parser, keep the basename parser and try the 752 | next path parser on the current file. 753 | .TP 754 | .BR q " (or " EOF ) 755 | Stop trying to parse this file. 756 | 757 | 758 | 759 | .SS EDITING MENU 760 | It is possible to edit tags, either before tagging or file, or before 761 | choosing a track in a CD obtained by CDDB. 762 | The current value of all regular fields is shown and may be modified 763 | by entering another value, deleted by entering 764 | .BR <DELETE> , 765 | or cleared. 766 | 767 | The behavior depends on the installed readline library. 768 | If it is smart, the current value may be edited inline and an 769 | history is available. 770 | If not, pressing 771 | .I <enter> 772 | will keep the current value while 773 | .I CLEAR 774 | will empty it. 775 | .I EOF 776 | while cancel the editing of this single value. 777 | 778 | Each field may be selected for edition by pressing its corresponding 779 | letter in the format (see \fBFORMAT\fR). 780 | Since there might be some non-standard tag names, it is also possible 781 | to enter \fItag FOO\fR to modify tag \fIFOO\fR. 782 | 783 | Editing ends by tagging (if \fIE\fR is pressed) 784 | or canceling and return to confirmation menu (if \fIC\fR is pressed). 785 | 786 | The other options are: 787 | .TP 788 | .B V 789 | View the current values of tags 790 | .TP 791 | .B y 792 | End edition, save changes, and return to previous menu 793 | .TP 794 | .BR q " (or " EOF ) 795 | Cancel edition, forget about changes, and return to previous menu 796 | 797 | 798 | 799 | .SS RENAMING MENU 800 | By default, before renaming, a confirmation is asked to the user. 801 | You may bypass it by passing 802 | .B --yes 803 | on the command line. 804 | 805 | If the rename format uses a field that is not defined, 806 | a warning is shown and confirmation is always asked. 807 | 808 | Available behaviors when renaming are: 809 | .TP 810 | .B y 811 | Rename current file with current new filename. 812 | This is the default. 813 | .TP 814 | .B a 815 | Rename current file with current new filename. 816 | Then, never asking for a renaming confirmation anymore. 817 | .TP 818 | .B e 819 | Edit current new filename before renaming. 820 | The behavior depends on the installed readline library. 821 | If it is smart, the current value may be edited inline 822 | and an history is available. 823 | .TP 824 | .BR q " (or " EOF ) 825 | Don't rename current file. 826 | .TP 827 | .B h 828 | Show help about confirmation. 829 | 830 | 831 | 832 | 833 | .SH FORMAT 834 | User-specified formats must be a string composed of any characters 835 | and the following special codes: 836 | .RS 837 | 838 | .I "%a" 839 | to match the author. 840 | 841 | .I "%A" 842 | to match the album. 843 | 844 | .I "%g" 845 | to match the genre. 846 | 847 | .I "%n" 848 | to match the track number. 849 | 850 | .I "%t" 851 | to match the title. 852 | 853 | .I "%d" 854 | to match the date. 855 | 856 | .I "%c" 857 | to match the comment. 858 | 859 | .I "%i" 860 | to match anything and ignore it. 861 | 862 | .I "%%" 863 | to match %. 864 | 865 | .RE 866 | Additionally, while renaming, the following codes are available: 867 | .RS 868 | 869 | .I "%F" 870 | is replaced by the original basename of the file. 871 | 872 | .I "%E" 873 | is replaced by the original extension of the file. 874 | 875 | .I "%P" 876 | is replaced by the original path of the file. 877 | 878 | 879 | 880 | .SH INTERNAL FORMATS 881 | The internal format database is usually stored in 882 | .IR /etc/lltag/formats . 883 | The user may override this file by defining a 884 | .IR $HOME/.lltag/formats . 885 | If this file exists, the system-wide one is ignored. 886 | 887 | See the manpage of 888 | .I lltag_formats 889 | or 890 | .I /etc/lltag/formats 891 | for details. 892 | 893 | 894 | 895 | 896 | .SH CONFIGURATION FILES 897 | lltag reads some configuration files before parsing command line options. 898 | The system-wide configuration file is defined in 899 | .I /etc/lltag/config 900 | if it exists. 901 | 902 | It also reads 903 | .I $HOME/.lltag/config 904 | if it exists. 905 | 906 | The user may also add another configurable file with 907 | .BR --config . 908 | 909 | lltag may also generate a configuration with 910 | .BR --gencfg . 911 | 912 | See the manpage of 913 | .I lltag_config 914 | or the example of 915 | .I config 916 | file provided in the documentation for details. 917 | 918 | 919 | 920 | .SH FILES 921 | .RE 922 | .I /etc/lltag/formats 923 | .RS 924 | System-wide internal format database. 925 | See 926 | .B INTERNAL FORMATS 927 | for details. 928 | .RE 929 | .I $HOME/.lltag/formats 930 | .RS 931 | User internal format database. If it exists, the system-wide one is ignored. 932 | .RE 933 | .I $HOME/.lltag/edit_history 934 | .RS 935 | History of last entered values in the edition mode if the 936 | .B Readline 937 | library supports this feature. 938 | .RE 939 | .I /etc/lltag/config 940 | .RS 941 | System-wide configuration file, if it exists. 942 | See 943 | .B CONFIGURATION FILES 944 | for details. 945 | .RE 946 | .I $HOME/.lltag/config 947 | .RS 948 | User configuration file. 949 | 950 | 951 | 952 | .SH EXAMPLES 953 | .RE 954 | Show all tags for each OGG files in the current directory: 955 | .RS 956 | lltag \-S *.ogg 957 | .RE 958 | Show only a selected list of tags for all files in all subdirectories: 959 | .RS 960 | lltag \-\-show-tags artist,album,title,number \-R . 961 | .RE 962 | Set an arbitrary tag in a file (only works with OGG vorbis or FLAC files): 963 | .RS 964 | lltag \-\-tag foo=nil foo.ogg 965 | .RE 966 | Delete the foo tag from a file: 967 | .RS 968 | lltag \-\-tag foo= bar.ogg 969 | .RE 970 | Set the ALBUM, ARTIST and GENRE tag values of the MP3 files in the current directory: 971 | .RS 972 | lltag \-\-ARTIST "Queen" \-\-ALBUM "Innunendo" \-\-GENRE "rock" \-\-COMMENT="very cool" *.mp3 973 | .RE 974 | Rename a file by assembling its current NUMBER, ARTIST and TITLE tag values: 975 | .RS 976 | lltag \-\-no\-tagging \-\-rename "%n - %a - %t" foobar.ogg 977 | .RE 978 | Clear all tags in all FLAC files: 979 | .RS 980 | lltag \-\-clear *.flac 981 | 982 | 983 | 984 | 985 | .SH SEE ALSO 986 | .PP 987 | .BR lltag_config "(5), " lltag_formats (5) 988 | 989 | The 990 | .I howto.html 991 | file provided within the documentation. 992 | 993 | 994 | 995 | 996 | .SH AUTHOR 997 | Brice Goglin 998 | --------------------------------------------------------------------------------