MP3-M3U-Parser
view release on metacpan or search on metacpan
lib/MP3/M3U/Parser.pm view on Meta::CPAN
sub new {
# -parse_path -seconds -search -overwrite
my($class, @args) = @_;
my %o = @args % 2 ? () : @args; # options
my $self = {
_M3U_ => [], # for parse()
TOTAL_FILES => 0, # Counter
TOTAL_TIME => 0, # In seconds
TOTAL_SONGS => 0, # Counter
AVERAGE_TIME => 0, # Counter
ACOUNTER => 0, # Counter
ANON => 0, # Counter for SCALAR & GLOB M3U
INDEX => 0, # index counter for _M3U_
EXPORTF => 0, # Export file name counter for anonymous exports
seconds => $o{'-seconds'} || EMPTY_STRING, # format or get seconds.
search_string => $o{'-search'} || EMPTY_STRING, # search_string
parse_path => $o{'-parse_path'} || EMPTY_STRING, # mixed list?
overwrite => $o{'-overwrite'} || 0, # overwrite export file if exists?
encoding => $o{'-encoding'} || EMPTY_STRING, # leave it to export() if no param
expformat => $o{'-expformat'} || EMPTY_STRING, # leave it to export() if no param
expdrives => $o{'-expdrives'} || EMPTY_STRING, # leave it to export() if no param
};
my $s = $self->{search_string};
if ( $s && length $s < MINIMUM_SEARCH_LENGTH ) {
croak 'A search string must be at least three characters long';
}
bless $self, $class;
return $self;
}
sub parse {
my($self, @files) = @_;
foreach my $file ( @files ) {
$self->_parse_file(
ref $file ? $file
: do {
my $new = $self->_locate_file( $file );
croak "$new does not exist" if ! -e $new;
$new;
}
);
}
# Average time of all the parsed songs:
my($ac, $tt) = ( $self->{ACOUNTER}, $self->{TOTAL_TIME} );
$self->{AVERAGE_TIME} = ($ac && $tt) ? $self->_seconds( $tt / $ac ) : 0;
return defined wantarray ? $self : undef;
}
sub _check_parse_file_params {
my($self, $file) = @_;
my $ref = ref $file;
if ( $ref && $ref ne 'GLOB' && $ref ne 'SCALAR' ) {
croak "Unknown parameter of type '$ref' passed to parse()";
}
my $cd;
if ( ! $ref ) {
my @tmp = split m{[\\/]}xms, $file;
($cd = pop @tmp) =~ s{ [.] m3u }{}xmsi;
}
my $this_file = $ref ? 'ANON'.$self->{ANON}++ : $self->_locate_file($file);
$self->{'_M3U_'}[ $self->{INDEX} ] = {
file => $this_file,
list => $ref ? $this_file : ($cd || EMPTY_STRING),
drive => DEFAULT_DRIVE,
data => [],
total => 0,
};
$self->{TOTAL_FILES} += 1; # Total lists counter
my($fh, @fh);
if ( $ref eq 'GLOB' ) {
$fh = $file;
}
elsif ( $ref eq 'SCALAR' ) {
@fh = split m{\n}xms, ${$file};
}
else {
# Open the file to parse:
require IO::File;
$fh = IO::File->new;
$fh->open( $file, '<' ) or croak "I could't open '$file': $!";
}
return $ref, $fh, @fh;
}
sub _validate_m3u {
my($self, $next, $ref, $file) = @_;
PREPROCESS: while ( my $m3u = $next->() ) {
# First line is just a comment. But we need it to validate
# the file as a m3u playlist file.
chomp $m3u;
last PREPROCESS if $m3u =~ RE_M3U_HEADER;
croak $ref ? "The '$ref' parameter does not contain valid m3u data"
: "'$file' is not a valid m3u file";
}
return;
}
sub _iterator {
my($self, $ref, $fh, @fh) = @_;
return $ref eq 'SCALAR' ? sub { return shift @fh } : sub { return <$fh> };
}
sub _extract_path {
my($self, $i, $m3u, $device_ref, $counter_ref) = @_;
if ( $m3u =~ RE_DRIVE_PATH ||
$m3u =~ RE_NORMAL_PATH ||
$m3u =~ RE_PARTIAL_PATH
) {
# Get the drive and path info.
my $path = $1;
$i->[PATH] = $self->{parse_path} eq 'asis' ? $m3u : $path;
if ( ${$device_ref} eq DEFAULT_DRIVE && $m3u =~ m{ \A (\w:) }xms ) {
${$device_ref} = $1;
}
${ $counter_ref }++;
}
return;
}
sub _extract_artist_song {
my($self, $i) = @_;
# Try to extract artist and song info
# and remove leading and trailing spaces
# Some artist names can also have a "-" in it.
# For this reason; require that the data has " - " in it.
# ... but the spaces can be one or more.
# So, things like "artist-song" does not work...
my($artist, @xsong) = split m{\s{1,}-\s{1,}}xms, $i->[ID3] || $i->[PATH];
if ( $artist ) {
$artist = $self->_trim( $artist );
$artist =~ s{.*[\\/]}{}xms; # remove path junk
$i->[ARTIST] = $artist;
}
if ( @xsong ) {
my $song = join q{-}, @xsong;
$song = $self->_trim( $song );
$song =~ s{ [.] [a-zA-Z0-9]+ \z }{}xms; # remove extension if exists
$i->[SONG] = $song;
}
return;
}
sub _initialize {
my($self, $i);
foreach my $CHECK ( 0..MAXDATA ) {
$i->[$CHECK] = EMPTY_STRING if ! defined $i->[$CHECK];
}
return;
}
sub _parse_file {
# supports disk files, scalar variables and filehandles (typeglobs)
my($self, $file) = @_;
my($ref, $fh, @fh) = $self->_check_parse_file_params( $file );
my $next = $self->_iterator( $ref, $fh, @fh );
$self->_validate_m3u( $next, $ref, $file );
my $dkey = $self->{_M3U_}[ $self->{INDEX} ]{data}; # data key
my $device = \$self->{_M3U_}[ $self->{INDEX} ]{drive}; # device letter
# These three variables are used when there is a '-search' parameter.
# long: total_time, total_songs, total_average_time
my($ttime,$tsong,$taver) = (0,0,0);
my $index = 0; # index number of the list array
my $temp_sec; # must be defined outside
RECORD: while ( my $m3u = $next->() ) {
chomp $m3u;
next if ! $m3u; # Record may be blank if it is not a disk file.
$#{$dkey->[$index]} = MAXDATA; # For the absence of EXTINF line.
# If the extra information exists, parse it:
if ( $m3u =~ RE_INF_HEADER ) {
my($j, $sec, @song);
($j ,@song) = split m{\,}xms, $m3u;
($j ,$sec) = split m{:}xms, $j;
$temp_sec = $sec;
$ttime += $sec;
$dkey->[$index][ID3] = join q{,}, @song;
$dkey->[$index][LEN] = $self->_seconds($sec || 0);
$taver++;
next RECORD; # jump to path info
}
my $i = $dkey->[$index];
$self->_extract_path( $i, $m3u, $device, \$tsong );
$self->_extract_artist_song( $i );
$self->_initialize( $i );
# If we are searching something:
if ( $self->{search_string} ) {
my $matched = $self->_search( $i->[PATH], $i->[ID3] );
if ( $matched ) {
$index++; # if we got a match, increase the index
}
else {
# if we didnt match anything, resize these counters ...
$tsong--;
$taver--;
$ttime -= $temp_sec;
delete $dkey->[$index]; # ... and delete the empty index
}
}
else {
$index++; # If we are not searching, just increase the index
}
}
$fh->close if ! $ref;
return $self->_set_parse_file_counters( $ttime, $tsong, $taver );
}
sub _set_parse_file_counters {
my($self, $ttime, $tsong, $taver) = @_;
# Calculate the total songs in the list:
my $k = $self->{_M3U_}[ $self->{INDEX} ];
$k->{total} = @{ $k->{data} };
# Adjust the global counters:
$self->{TOTAL_FILES}-- if $self->{search_string} && $k->{total} == 0;
$self->{TOTAL_TIME} += $ttime;
$self->{TOTAL_SONGS} += $tsong;
$self->{ACOUNTER} += $taver;
$self->{INDEX}++;
return $self;
}
sub reset { ## no critic (ProhibitBuiltinHomonyms)
# reset the object
my $self = shift;
my @zeroes = qw(
TOTAL_FILES
TOTAL_TIME
TOTAL_SONGS
( run in 2.873 seconds using v1.01-cache-2.11-cpan-71847e10f99 )