File Coverage

blib/lib/Music/Tag.pm
Criterion Covered Total %
statement 49 51 96.0
branch n/a
condition n/a
subroutine 17 17 100.0
pod n/a
total 66 68 97.0


line stmt bran cond sub pod time code
1             package Music::Tag;
2 5     5   197798 use strict; use warnings; use utf8;
  5     5   12  
  5     5   205  
  5         29  
  5         10  
  5         160  
  5         4851  
  5         51  
  5         26  
3             our $VERSION = '0.4103';
4              
5             # Copyright © 2007,2008,2009,2010 Edward Allen III. Some rights reserved.
6             #
7             # You may distribute under the terms of either the GNU General Public
8             # License or the Artistic License, as specified in the README file.
9              
10              
11 5     5   283 use Carp;
  5         8  
  5         1804  
12 5     5   10302 use Locale::Country;
  5         291280  
  5         541  
13 5     5   49 use File::Spec;
  5         11  
  5         129  
14 5     5   7777 use Encode;
  5         89787  
  5         691  
15 5     5   4859 use Config::Options;
  5         82166  
  5         303  
16 5     5   5413 use Digest::SHA1;
  5         5404  
  5         305  
17 5     5   5396 use Time::Local;
  5         10845  
  5         563  
18 5     5   5393 use IO::File;
  5         78174  
  5         1034  
19 5     5   5233 use IO::Dir;
  5         101650  
  5         309  
20 5     5   51 use File::stat;
  5         11  
  5         23  
21 5     5   7118 use File::Slurp;
  5         131500  
  5         732  
22 5     5   6719 use Readonly;
  5         27526  
  5         371  
23 5     5   3569 use Music::Tag::Generic;
  5         18  
  5         219  
24 5     5   4125 use DateTimeX::Easy;
  0            
  0            
25              
26             use utf8;
27              
28             #use vars qw(%DataMethods);
29             my %DataMethods;
30             my $DefaultOptions;
31             my @PLUGINS;
32             my $PBP_METHODS = 1;
33             my $TRADITIONAL_METHODS = 1;
34             my %METHODS = ();
35             my ( $SHA1_SIZE, $SLURP_SIZE, $TENPRINT_SIZE );
36             Readonly::Scalar $SHA1_SIZE => 4 * 4096;
37             Readonly::Scalar $SLURP_SIZE => 1024;
38             Readonly::Scalar $TENPRINT_SIZE => 12;
39             sub default_options {
40             my $self = shift;
41             return $DefaultOptions;
42             }
43              
44             sub LoadOptions {
45             my $self = shift;
46             my $optfile = shift;
47             if ( ref $self ) {
48             return $self->options->fromfile_perl($optfile);
49             }
50             elsif ($self) {
51             return $DefaultOptions->fromfile_perl($optfile);
52             }
53             }
54              
55             sub new {
56             my $class = shift;
57             my $filename = shift;
58             my $options = shift || {};
59             my $plugin = shift || 'Auto';
60             my $data = shift || {};
61              
62             my $self = {};
63             $self->{data} = $data;
64             if ( ref $class ) {
65             my $clone = { %{$class} };
66             bless $clone, ref $class;
67             return $clone;
68             }
69             else {
70             bless $self, $class;
71             $self->{_plugins} = [];
72             $self->options($options);
73             $self->filename($filename);
74             $self->{changed} = 0;
75             }
76              
77             $self->_test_modules();
78              
79             $self->add_plugin( $plugin, $options );
80             return $self;
81              
82             }
83              
84             sub _test_modules {
85             my $self = shift;
86             my %module_map = (
87             'ANSIColor' => 'Term::ANSIColor',
88             'LevenshteinXS' => 'Text::LevenshteinXS',
89             'Levenshtein' => 'Text::Levenshtein',
90             'Unaccent' => 'Text::Unaccent::PurePerl',
91             'Inflect' => 'Lingua::EN::Inflect',
92             );
93             while ( my ( $k, $v ) = each %module_map ) {
94             if ( ( $self->options->{$k} )
95             && ( $self->_has_module($v) ) ) {
96             $self->options->{$k} = 1;
97             }
98             else {
99             $self->options->{$k} = 0;
100             }
101             }
102             return;
103             }
104              
105             sub _has_module {
106             my $self = shift;
107             my $module = shift;
108             my $modfile = $module . '.pm';
109             $modfile =~ s/\:\:/\//g;
110             if ( eval { require $modfile; 1 } ) {
111             return 1;
112             }
113             else {
114             $self->status( 1, "Not loading $module: " . $@ );
115             return 0;
116             }
117             }
118              
119             sub add_plugin {
120             my $self = shift;
121             my $object = shift;
122             my $opts = shift || {};
123             my $options = $self->options->clone;
124             $options->merge($opts);
125             my $type = shift || 0;
126             my $ref;
127             if ( ref $object ) {
128             $ref = $object;
129             $ref->info($self);
130             $ref->options($options);
131             }
132             else {
133             my ( $plugin, $popts ) = split( /:/, $object );
134             if ( $self->available_plugins($plugin) ) {
135             if ($popts) {
136             my @opts = split( /[;]/, $popts );
137             foreach (@opts) {
138             my ( $k, $v ) = split( /=/, $_ );
139             $options->options( $k, $v );
140             }
141             }
142             if (!eval {
143             if ( not $plugin =~ /::/ ) {
144             $plugin = 'Music::Tag::' . $plugin;
145             }
146             if ( $self->_has_module($plugin) ) {
147             $ref = $plugin->new( $self, $options );
148             }
149             return 1;
150             }
151             ) {
152             croak "Error loading plugin ${plugin}: $@" if $@;
153             }
154             }
155             else {
156             croak "Error loading plugin ${plugin}: Not Found";
157             }
158             }
159             if ($ref) {
160             push @{ $self->{_plugins} }, $ref;
161             }
162             return $ref;
163             }
164              
165             sub plugin {
166             my $self = shift;
167             my $plugin = shift;
168             if ( defined $plugin ) {
169             foreach ( @{ $self->{_plugins} } ) {
170             if ( ref($_) =~ /$plugin$/ ) {
171             return $_;
172             }
173             }
174             }
175             return;
176             }
177              
178             sub get_tag {
179             my $self = shift;
180             $self->_foreach_plugin( sub { $_[0]->get_tag } );
181             return $self;
182             }
183              
184             sub _foreach_plugin {
185             my $self = shift;
186             my $callback = shift;
187             foreach my $plugin ( @{ $self->{_plugins} } ) {
188             if ( ref $plugin ) {
189             &{$callback}($plugin);
190             }
191             elsif ($plugin) {
192             $self->error("Invalid Plugin in list: '$plugin'");
193             }
194             }
195             return $self;
196             }
197              
198             sub set_tag {
199             my $self = shift;
200             $self->_foreach_plugin( sub { $_[0]->set_tag } );
201             return $self;
202             }
203              
204             sub strip_tag {
205             my $self = shift;
206             $self->_foreach_plugin( sub { $_[0]->strip_tag } );
207             return $self;
208             }
209              
210              
211             # In retrospect, this was misnamed. Too late now!
212             sub close { ## no critic (ProhibitBuiltinHomonyms, ProhibitAmbiguousNames)
213             my $self = shift;
214             return $self->_foreach_plugin(
215             sub {
216             $_[0]->close();
217              
218             #$_->{info} = undef;
219             #$_ = undef;
220             }
221             );
222             }
223              
224             sub changed {
225             my $self = shift;
226             my $new = shift;
227             if ( defined $new ) {
228             $self->{changed}++;
229             }
230             return $self->{changed};
231             }
232              
233             sub data {
234             my $self = shift;
235             my $new = shift;
236             if ( defined $new ) {
237             $self->{data} = $new;
238             }
239             return $self->{data};
240             }
241              
242             sub options { ## no critic (Subroutines::RequireArgUnpacking)
243             my $self = shift;
244             if ( not exists $self->{_options} ) {
245             $self->{_options} = Config::Options->new( $self->default_options );
246             }
247             return $self->{_options}->options(@_);
248             }
249              
250             sub setfileinfo {
251             my $self = shift;
252             if ( $self->filename ) {
253             my $st = stat $self->filename;
254             $self->mepoch( $st->mtime );
255             $self->bytes( $st->size );
256             return $st;
257             }
258             return;
259             }
260              
261             sub sha1 {
262             my $self = shift;
263             if ( not( ( $self->filename ) && ( -e $self->filename ) ) ) {
264             return undef; ## no critic (Subroutines::ProhibitExplicitReturnUndef)
265             }
266             my $maxsize = $SHA1_SIZE;
267             my $in = IO::File->new();
268             $in->open( $self->filename, '<' ) or die "Bad file: $self->filename\n";
269             my $st = stat $self->filename;
270             my $sha1 = Digest::SHA1->new();
271             $sha1->add( pack( 'V', $st->size ) );
272             my $d;
273              
274             if ( $in->read( $d, $maxsize ) ) {
275             $sha1->add($d);
276             }
277             $in->close();
278             return $sha1->hexdigest;
279             }
280              
281             sub picture {
282             my $self = shift;
283             unless ( exists $self->{data}->{PICTURE} ) {
284             $self->{data}->{PICTURE} = {};
285             }
286             $self->{data}->{PICTURE} = shift if @_;
287              
288             if ( ( exists $self->{data}->{PICTURE}->{filename} )
289             && ( $self->{data}->{PICTURE}->{filename} ) ) {
290             my $root = File::Spec->rootdir();
291             if ( $self->filename ) {
292             $root = $self->filedir;
293             }
294             my $picfile =
295             File::Spec->rel2abs( $self->{data}->{PICTURE}->{filename},
296             $root );
297             if ( -f $picfile ) {
298             if ( $self->{data}->{PICTURE}->{_Data} ) {
299             delete $self->{data}->{PICTURE}->{_Data};
300             }
301             my %ret = %{ $self->{data}->{PICTURE} }; # Copy ref
302             $ret{_Data} = read_file( $picfile, 'binmode' => ':raw' );
303             return \%ret;
304             }
305             }
306             elsif (( exists $self->{data}->{PICTURE}->{_Data} )
307             && ( length $self->{data}->{PICTURE}->{_Data} ) ) {
308             return $self->{data}->{PICTURE};
309             }
310             return {};
311             }
312              
313             sub get_picture {
314             my $self = shift;
315             return $self->picture;
316             }
317              
318             sub set_picture {
319             my $self = shift;
320             my $value = shift;
321             return $self->picture($value);
322             }
323              
324             sub picture_filename {
325             my $self = shift;
326             my $new = shift;
327             if ($new) {
328             if ( not exists $self->{data}->{PICTURE} ) {
329             $self->{data}->{PICTURE} = {};
330             }
331             $self->{data}->{PICTURE}->{filename} = $new;
332             }
333             if ( ( exists $self->{data}->{PICTURE} )
334             && ( $self->{data}->{PICTURE}->{filename} ) ) {
335             return $self->{data}->{PICTURE}->{filename};
336             }
337             elsif (( exists $self->{data}->{PICTURE} )
338             && ( $self->{data}->{PICTURE}->{_Data} )
339             && ( length( $self->{data}->{PICTURE}->{_Data} ) ) ) {
340             return 0;
341             }
342              
343             # Value is undefined, so return undef.
344             return undef; ## no critic (Subroutines::ProhibitExplicitReturnUndef)
345             }
346              
347             sub picture_exists { goto &has_picture; }
348             sub has_picture {
349             my $self = shift;
350             if ( ( exists $self->{data}->{PICTURE}->{filename} )
351             && ( $self->{data}->{PICTURE}->{filename} ) ) {
352             my $root = File::Spec->rootdir();
353             if ( $self->filename ) {
354             $root = $self->filedir;
355             }
356             my $picfile =
357             File::Spec->rel2abs( $self->{data}->{PICTURE}->{filename},
358             $root );
359             if ( -f $picfile ) {
360             return 1;
361             }
362             else {
363             $self->status( 0, 'Picture: ', $picfile, ' does not exists' );
364             }
365             }
366             elsif (( exists $self->{data}->{PICTURE}->{_Data} )
367             && ( length $self->{data}->{PICTURE}->{_Data} ) ) {
368             return 1;
369             }
370             return 0;
371             }
372              
373             sub available_plugins {
374             my $self = shift;
375             my $check = shift;
376             if ($check) {
377             foreach (@PLUGINS) {
378             if ( $check eq $_ ) {
379             return 1;
380             }
381             }
382             return 0;
383             }
384             return @PLUGINS;
385             }
386              
387              
388             sub datamethods {
389             my $package = shift;
390             if (ref $package) { $package = ref $package; }
391             my $add = shift;
392             if ($add) {
393             my $new = lc($add);
394             $DataMethods{$new} = 1;
395             if ( !defined &{ 'get_' . $new } ) {
396             $package->_make_accessor( $new => {} );
397             }
398             }
399             return [ keys %DataMethods ];
400             }
401              
402             sub used_datamethods {
403             my $self = shift;
404             my @ret = ();
405             foreach my $m ( @{ $self->datamethods } ) {
406             if ($self->has_data($m)) {
407             push @ret, $m;
408             }
409             }
410             return \@ret;
411             }
412              
413             sub wav_out {
414             my $self = shift;
415             my $fh = shift;
416             my $out;
417             $self->_foreach_plugin(
418             sub {
419             $out = $_->wav_out($fh);
420             return $out if ( defined $out );
421             }
422             );
423             return $out;
424             }
425              
426             # This method is far from perfect. It can't be perfect.
427             # It won't mangle valid UTF-8, however.
428             # Just be sure to always return perl utf8 in plugins when possible.
429              
430             sub _isutf8 {
431             my $self = shift;
432             my $in = shift;
433              
434             # If it is a proper utf8, with tag, just return it.
435             if ( Encode::is_utf8( $in, 1 ) ) {
436             return $in;
437             }
438              
439             my $has7f = 0;
440             foreach ( split( //, $in ) ) {
441             if ( ord($_) >= 0x7f ) { ## no critic (ProhibitMagicNumbers)
442             $has7f++;
443             }
444             }
445              
446             # No char >7F it is prob. valid ASCII, just return it.
447             if ( !$has7f ) {
448             utf8::upgrade($in);
449             return $in;
450             }
451              
452             # See if it is a valid UTF-16 encoding.
453             #my $out;
454             #eval {
455             # $out = decode('UTF-16', $in, 1);
456             #};
457             #return $out unless $@;
458              
459             # See if it is a valid UTF-16LE encoding.
460             #my $out;
461             #eval {
462             # $out = decode('UTF-16LE', $in, 1);
463             #};
464             #return $out unless $@;
465              
466             # See if it is a valid UTF-8 encoding.
467             my $out;
468             if ( eval { $out = decode( 'UTF-8', $in, 1 ); return 1 } ) {
469             utf8::upgrade($out);
470             return $out;
471             }
472              
473             # Finally just give up and return it.
474              
475             utf8::upgrade($in);
476             return $in;
477             }
478              
479             sub _add_to_namespace {
480             my ( $package, $attrname, $reader, $writer, $predicate ) = @_;
481             $METHODS{$attrname} = {reader => $reader};
482             if ($predicate) {
483             $METHODS{$attrname}->{predicate} = $predicate;
484             }
485             {
486             ## no critic (ProhibitProlongedStrictureOverride,ProhibitNoStrict)
487             no strict 'refs';
488              
489             if ($TRADITIONAL_METHODS) {
490             my $readwriter;
491             if ($writer) {
492             $readwriter = _generate_readwriter( $package, $reader, $writer );
493             $METHODS{$attrname}->{writer} = $writer;
494             } elsif ($reader) {
495             $readwriter = $reader;
496             }
497             $METHODS{$attrname}->{readwriter} = $readwriter;
498             if ($readwriter) { *{ $package . '::' . $attrname } = $readwriter; }
499             }
500             if ($PBP_METHODS) {
501             if ($writer) { *{ $package . '::set_' . $attrname } = $writer; }
502             if ($reader) { *{ $package . '::get_' . $attrname } = $reader; }
503             }
504             if ($TRADITIONAL_METHODS || $PBP_METHODS) {
505             if ($predicate) { *{ $package . '::has_' . $attrname } = $predicate; }
506             }
507             ## use critic
508             }
509             }
510              
511             sub _get_method {
512             my $self = shift;
513             my $method = shift;
514             my $attr = shift;
515             if ((exists $METHODS{$attr}) && (ref $METHODS{$attr})) {
516             return $METHODS{$attr}->{$method};
517             }
518             else {
519             return sub {};
520             }
521             }
522              
523             sub _get_reader {
524             my $self = shift;
525             my $attr = shift;
526             $self->_get_method('reader',$attr);
527             }
528              
529             sub _get_writer {
530             my $self = shift;
531             my $attr = shift;
532             $self->_get_method('writer',$attr);
533             }
534              
535             sub _get_predicate {
536             my $self = shift;
537             my $attr = shift;
538             $self->_get_method('predicate',$attr);
539             }
540              
541             sub _do_method {
542             my $self = shift;
543             my $method = shift;
544             my $attr = shift;
545             my @p = @_;
546             &{$self->_get_method($method,$attr)}($self,@p);
547             }
548              
549             sub get_data {
550             my $self = shift;
551             my @opts = @_;
552             $self->_do_method('reader',@opts);
553             }
554              
555             sub set_data {
556             my $self = shift;
557             my @opts = @_;
558             $self->_do_method('writer',@opts);
559             }
560              
561             sub has_data {
562             my $self = shift;
563             my @opts = @_;
564             $self->_do_method('predicate',@opts);
565             }
566              
567             sub _generate_reader {
568             my ( $package, $attr, $options ) = @_;
569             my $default = $options->{default} || undef;
570             my $trigger = $options->{readtrigger} || undef;
571             my $outfilter = $options->{outfilter} || undef;
572             my $builder = $options->{builder} || undef;
573             return sub {
574             my $self = shift;
575             if ( ( not exists $self->{data}->{$attr} )
576             or ( not defined $self->{data}->{$attr} ) ) {
577             if ($builder) {
578             $self->{data}->{$attr} = &{$builder}($self);
579             }
580             else {
581             return $default;
582             }
583             }
584             if ($trigger) { &{$trigger}( $self, $self->{data}->{$attr} ); }
585             return $outfilter
586             ? &{$outfilter}( $self, $self->{data}->{$attr} )
587             : $self->{data}->{$attr};
588             }
589             }
590              
591             sub _generate_writer {
592             my ( $package, $attr, $options ) = @_;
593             my $trigger = $options->{trigger} || undef;
594             my $filter = $options->{filter} || undef;
595             my $validator = $options->{validator} || undef;
596              
597             return sub {
598             my ( $self, $value ) = @_;
599             my $setvalue = $filter ? &{$filter}( $self, $value ) : $value;
600             if ( ($validator) && ( !&{$validator}( $self, $value ) ) ) {
601             $self->status(
602             0,
603             "Invalid value for $attr: ",
604             ( defined $setvalue ) ? $setvalue : 'UNDEFINED'
605             );
606             return;
607             }
608             if ( $self->options('verbose') ) {
609             $self->status(
610             1,
611             "Setting $attr to ",
612             ( defined $setvalue ) ? $setvalue : 'UNDEFINED'
613             );
614             }
615             $self->{data}->{$attr} = $setvalue;
616             if ($trigger) { &{$trigger}( $self, $setvalue ); }
617             return $self->{data}->{$attr};
618             }
619             }
620              
621             sub _generate_readwriter {
622             my ( $package, $reader, $writer ) = @_;
623             return sub {
624             my ( $self, $value ) = @_;
625             if ( defined $value ) {
626             return &{$writer}( $self, $value );
627             }
628             else {
629             return &{$reader}($self);
630             }
631             };
632             }
633              
634             sub _generate_predicate {
635             my ( $package, $attr, $options ) = @_;
636             return sub {
637             my $self = shift;
638             return ( ( exists $self->{data}->{$attr} )
639             && ( defined $self->{data}->{$attr} ) );
640             };
641             }
642              
643             sub _make_accessor {
644             my ( $package, $attrname, $options ) = @_;
645             my $attr = $options->{attr} || uc($attrname);
646             my $reader = _generate_reader( $package, $attr, $options );
647             my $writer;
648             if ( !( ( exists $options->{readonly} ) && ( $options->{readonly} ) ) ) {
649             $writer = _generate_writer( $package, $attr, $options );
650             }
651             my $predicate = _generate_predicate( $package, $attr, $options );
652             _add_to_namespace( $package, $attrname, $reader, $writer, $predicate );
653             return;
654             }
655              
656             sub _make_datetime_accessor {
657             my ( $package, $attrname, $options ) = @_;
658             my $attr = $options->{attr} || uc($attrname);
659             my $filter = sub {
660             my ( $self, $value ) = @_;
661             if ( defined $value ) {
662             if ( $value =~ /^\-?\d+$/ ) {
663             return DateTime->from_epoch( epoch => $value );
664             }
665             else {
666             return DateTimeX::Easy->new($value);
667             }
668             $self->status( 0, "Invalid date set for ${attr}: ${value}" );
669             }
670             return;
671             };
672             $options->{filter} = $filter;
673              
674             my $predicate = _generate_predicate( $package, $attr, $options );
675             my $writer = _generate_writer( $package, $attr, $options );
676             my $dt_reader = _generate_reader( $package, $attr, $options );
677             _add_to_namespace( $package,
678             ( $options->{dtname} ? $options->{dtname} : $attrname . 'dt' ),
679             $dt_reader, $writer, $predicate );
680              
681             $options->{outfilter} = sub { my ( $self, $val ) = @_; return $val->ymd };
682             my $date_reader = _generate_reader( $package, $attr, $options );
683             _add_to_namespace(
684             $package,
685             ( $options->{datename} ? $options->{datename} : $attrname . 'date' ),
686             $date_reader,
687             $writer,
688             $predicate
689             );
690              
691             $options->{outfilter} =
692             sub { my ( $self, $val ) = @_; return $val->ymd . ' ' . $val->hms };
693             my $time_reader = _generate_reader( $package, $attr, $options );
694             _add_to_namespace(
695             $package,
696             ( $options->{timename} ? $options->{timename} : $attrname . 'time' ),
697             $time_reader,
698             $writer,
699             $predicate
700             );
701              
702             $options->{outfilter} =
703             sub { my ( $self, $val ) = @_; return $val->epoch };
704             my $epoch_reader = _generate_reader( $package, $attr, $options );
705             _add_to_namespace(
706             $package,
707             ( $options->{epochname}
708             ? $options->{epochname}
709             : $attrname . 'epoch'
710             ),
711             $epoch_reader,
712             $writer,
713             $predicate
714             );
715             return;
716             }
717              
718             sub _make_ordinal_accessor {
719             my ( $package, $attrname, $options ) = @_;
720             my $attr = uc($attrname);
721             my $pos = $options->{pos_attr};
722             if ( !$pos ) { croak("pos_attr required\n"); return }
723             my $total = $options->{total_attr};
724             if ( !$total ) { croak("total_attr required\n"); return }
725             my $writer = sub {
726             my ( $self, $new ) = @_;
727             my ( $t, $tt ) = split( m{/}, $new );
728             if ($t) {
729             &{$self->_get_writer($pos)}($self,$t);
730             }
731             if ($tt) {
732             &{$self->_get_writer($total)}($self,$tt);
733             }
734             return $new;
735             };
736             my $reader = sub {
737             my $self = shift;
738             my $m = '_get_' . $pos;
739             my $t = &{$self->_get_reader($pos)}($self);
740             my $tt = &{$self->_get_reader($total)}($self);
741             my $r = '';
742             if ($t) {
743             $r .= $t;
744             }
745             if ($tt) {
746             $r .= '/' . $tt;
747             }
748             return $r;
749             };
750             my $predicate = sub {
751             my $self = shift;
752             my ( $pp, $pt ) = ( 'has_' . $pos, 'has_' . $total );
753             if ( $self->$pp || $self->$pt ) {
754             return 1;
755             }
756             return;
757             };
758             _add_to_namespace( $package, $attrname, $reader, $writer, $predicate );
759             return;
760             }
761              
762             sub _make_list_accessor {
763             my ( $package, $attrname, $options ) = @_;
764             $options->{filter} = sub {
765             my ( $self, $value ) = @_;
766             my @ret = ();
767             if ( ref $value ) {
768             push @ret, @{$value};
769             }
770             else {
771             push @ret, split( /\s*,\s*/, $value );
772             }
773             return \@ret;
774             };
775             _make_accessor( $package, $attrname, $options );
776             }
777              
778             sub status { ## no critic (Subroutines::RequireArgUnpacking)
779             my $self = shift;
780             if ( not $self->options('quiet') ) {
781             my $name = ref($self);
782             if ( $_[0] =~ /\:\:/ ) {
783             $name = shift;
784             }
785             my $level = 0;
786             if ( $_[0] =~ /^\d+$/ ) {
787             $level = shift;
788             }
789             my $verbose = $self->options('verbose') || 0;
790             if ( $level <= $verbose ) {
791             $name =~ s/^Music::Tag:://g;
792             print $self->_tenprint( $name, 'bold white', $TENPRINT_SIZE ), @_,
793             "\n";
794             }
795             }
796             return;
797             }
798              
799             sub _tenprint {
800             my $self = shift;
801             my $text = shift;
802             my $_color = shift || 'bold yellow';
803             my $size = shift || $TENPRINT_SIZE;
804             return
805             $self->_color($_color)
806             . sprintf( '%' . $size . 's: ', substr( $text, 0, $size ) )
807             . $self->_color('reset');
808             }
809              
810             sub _color { ## no critic (Subroutines::RequireArgUnpacking)
811             my $self = shift;
812             if ( $self->options->{ANSIColor} ) {
813             return Term::ANSIColor::color(@_);
814             }
815             else {
816             return '';
817             }
818             }
819              
820             sub error { ## no critic (Subroutines::RequireArgUnpacking)
821             my $self = shift;
822              
823             # unless ( $self->options('quiet') ) {
824             carp( ref($self), ' ', @_ );
825              
826             # }
827             return;
828             }
829              
830             sub _create_attributes {
831             my $package = shift;
832             my $params = shift;
833              
834             if ($params->{pbp}) {
835             $PBP_METHODS = 1;
836             $TRADITIONAL_METHODS = 0;
837             }
838             if ($params->{traditional}) {
839             $TRADITIONAL_METHODS = 1;
840             }
841            
842             if (ref $package) { $package = ref $package; }
843             my @datamethods = qw(
844             album album_type albumartist albumartist_sortname albumid appleid
845             artist artist_end artist_start artist_start_time artist_start_epoch
846             artist_end_time artist_end_epoch artist_type artistid asin bitrate
847             booklet bytes codec comment compilation composer copyright country
848             countrycode disc discnum disctitle duration encoded_by encoder filename
849             frames framesize frequency gaplessdata genre ipod ipod_dbid
850             ipod_location ipod_trackid label lastplayedtime lastplayeddate
851             lastplayedepoch lyrics mb_albumid mb_artistid mb_trackid mip_puid
852             mtime mdate mepoch originalartist performer path picture playcount
853             postgap pregap rating albumrating recorddate recordtime releasedate
854             releasetime recordepoch releaseepoch samplecount secs songid sortname
855             stereo tempo title totaldiscs totaltracks track tracknum url user vbr
856             year upc ean jan filetype mip_fingerprint artisttags albumtags
857             tracktags);
858              
859             %DataMethods = map { $_ => { readwrite => 1 } } @datamethods;
860              
861             ## no critic (ProtectPrivateSubs)
862              
863             $package->_make_accessor(
864             'albumartist' => {
865             builder => sub { my $self = shift; return $self->artist() }
866             }
867             );
868             $package->_make_accessor(
869             'albumartist_sortname' => {
870             builder => sub { my $self = shift; return $self->sortname() }
871             }
872             );
873             $package->_make_list_accessor( 'albumtags' => {} );
874             $package->_make_list_accessor( 'artisttags' => {} );
875             $package->_make_accessor(
876             'country' => {
877             attr => 'COUNTRYCODE',
878             filter => sub {
879             my ( $self, $new ) = @_;
880             return country2code($new);
881             },
882             outfilter => sub {
883             my ( $self, $value ) = @_;
884             return code2country($value);
885             }
886             }
887             );
888             $package->_make_ordinal_accessor(
889             'discnum',
890             { pos_attr => 'disc',
891             total_attr => 'totaldiscs',
892             }
893             );
894             $package->_make_accessor(
895             'secs',
896             { attr => 'DURATION',
897             filter => sub {
898             my ( $self, $new ) = @_;
899             return $new * 1000;
900             },
901             outfilter => sub {
902             my ( $self, $value ) = @_;
903             return int( $value / 1000 );
904             }
905             }
906             );
907             $package->_make_accessor(
908             'ean',
909             { validator => sub {
910             my ( $self, $value ) = @_;
911             return $value =~ /^\d{13}$/;
912             },
913             alias => [qw(jan)],
914             }
915             );
916             $package->_make_accessor(
917             'filename',
918             { filter => sub {
919             my ( $self, $new ) = @_;
920             return File::Spec->rel2abs($new);
921             }
922             }
923             );
924             $package->_make_accessor(
925             'filedir',
926             { attr => 'FILENAME',
927             outfilter => sub {
928             my ( $self, $value ) = @_;
929             my ( $vol, $path, $file ) = File::Spec->splitpath($value);
930             return File::Spec->catpath( $vol, $path, '' );
931             },
932             readonly => 1,
933             }
934             );
935             $package->_make_accessor( 'artist', { alias => [qw(performer)] } );
936              
937             $package->_make_list_accessor( 'tracktags' => {} );
938              
939             $package->_make_ordinal_accessor(
940             'tracknum',
941             { pos_attr => 'track',
942             total_attr => 'totaltracks',
943             }
944             );
945              
946             $package->_make_accessor(
947             'upc',
948             { attr => 'EAN',
949             validator => sub {
950             my ( $self, $value ) = @_;
951             return $value =~ /^\d{13}$/;
952             },
953             filter => sub {
954             my ( $self, $value ) = @_;
955             return ( '0' . $value );
956             },
957             outfilter => sub {
958             my ( $self, $value ) = @_;
959             $value =~ s/^0//;
960             return $value;
961             }
962             }
963             );
964              
965             $package->_make_datetime_accessor(
966             'record' => {
967             trigger => sub {
968             my ( $self, $value ) = @_;
969             if ( $value->isa('DateTime') ) {
970             $self->set_year( $value->year );
971             }
972             }
973             }
974             );
975             $package->_make_datetime_accessor( 'release' => {} );
976             $package->_make_datetime_accessor( 'm' => {} );
977             $package->_make_datetime_accessor('lastplayed');
978             $package->_make_datetime_accessor(
979             'artist_start' => {
980             timename => 'artist_start_time',
981             datename => 'artist_start',
982             epochname => 'artist_start_epoch',
983             dtname => 'artist_start_dt',
984             }
985             );
986             $package->_make_datetime_accessor(
987             'artist_end' => {
988             timename => 'artist_end_time',
989             datename => 'artist_end',
990             epochname => 'artist_end_epoch',
991             dtname => 'artist_end_dt',
992             }
993             );
994              
995             $package->_make_accessor(
996             'year' => {
997             builder => sub {
998             my $self = shift;
999             if ( $self->has_releasedt ) {
1000             return $self->releasedt->year;
1001             }
1002             }
1003             }
1004             );
1005              
1006             $METHODS{'picture'} = {
1007             reader => \&get_picture,
1008             writer => \&set_picture,
1009             predicate => \&has_picture,
1010             };
1011              
1012              
1013             foreach my $m (@datamethods) {
1014             if ( ! exists $METHODS{$m}) {
1015             $package->_make_accessor( $m => {} );
1016             }
1017             }
1018             ## use critic
1019             }
1020              
1021             sub _find_plugins {
1022             my $package = shift;
1023             if (ref $package) { $package = ref $package; }
1024             my $me = $package;
1025             $me =~ s{::}{/}g;
1026             @PLUGINS = ();
1027             foreach my $d (@INC) {
1028             chomp $d;
1029             if ( -d "$d/$me/" ) {
1030             my $fdir = IO::Dir->new("$d/$me");
1031             if ( defined $fdir ) {
1032             while ( my $m = $fdir->read() ) {
1033             next if $m eq 'Test.pm';
1034             if ( $m =~ /^(.*)\.pm$/ ) {
1035             my $mod = $1;
1036             push @PLUGINS, $mod;
1037             }
1038             }
1039             }
1040             $fdir->close();
1041             }
1042             }
1043              
1044             }
1045              
1046             sub import {
1047             my $package = shift;
1048             my $params = {};
1049             if ( ref $_[0] ) { $params = $_[0]; }
1050             elsif ( !scalar @_ % 2 ) { $params = {@_}; }
1051             $package->_create_attributes($params);
1052             $package->_find_plugins($params);
1053             return 1;
1054             }
1055              
1056             BEGIN {
1057             $DefaultOptions = Config::Options->new(
1058             { verbose => 0,
1059             quiet => 0,
1060             ANSIColor => 0,
1061             LevenshteinXS => 1,
1062             Levenshtein => 1,
1063             Unaccent => 1,
1064             Inflect => 0,
1065             optionfile =>
1066             [ '/etc/musictag.conf', $ENV{HOME} . '/.musictag.conf' ],
1067             }
1068             );
1069             }
1070              
1071             sub DESTROY {
1072             my $self = shift;
1073             $self->_foreach_plugin( sub { $_[0]->{info} = undef } );
1074             return;
1075             }
1076              
1077             1;
1078             __END__