App-DrivePlayer

 view release on metacpan or  search on metacpan

lib/App/DrivePlayer/Scanner.pm  view on Meta::CPAN

    $self->on_track_found->(\%track) if $self->has_on_track_found;
}

# Parse filename into (title, artist, album, track_number, year).
# Handles: "NN - Artist - Title", "Artist - Title", "NN - Title", "Title"
sub _parse_filename {
    my ($filename) = @_;
    (my $base = $filename) =~ s/\.[^.]+$//;

    my ($title, $artist, $album, $track_num, $year);

    # "YYYY-..." prefix: capture the year and strip it before running the
    # track-number patterns below (otherwise 2001 would match \d+ as track#).
    if ($base =~ s{
        ^
        ( (?: 19 | 20) \d{2} )   # 4-digit year, 19xx or 20xx
        -
    }{}x) {
        $year = $1;
    }

    if ($base =~ /^(\d+)[\s.\-]+(.+?)\s+[-–]\s+(.+)$/) {
        ($track_num, $artist, $title) = ($1 + 0, $2, $3);
    } elsif ($base =~ /^(\d+)[\s.\-]+(.+)$/) {
        ($track_num, $title) = ($1 + 0, $2);
    } elsif ($base =~ /^(.+?)\s+[-–]\s+(.+)$/) {
        ($artist, $title) = ($1, $2);
    } else {
        $title = $base;
    }

    if (!$year && $title && $title =~ s{
        \s*
        [ \( \[ ]                # opening paren or bracket
        ( (?: 19 | 20) \d{2} )   # 4-digit year
        [ \) \] ]                # closing paren or bracket
        \s*
        $
    }{}x) {
        $year = $1;
    }

    return ($title // $base, $artist, $album, $track_num, $year);
}

1;

__END__

=head1 NAME

App::DrivePlayer::Scanner - Recursively scan a Google Drive folder and store tracks

=head1 SYNOPSIS

  use App::DrivePlayer::Scanner;

  my $scanner = App::DrivePlayer::Scanner->new(
      drive          => $drive_api,       # Google::RestApi::DriveApi3
      db             => $db,              # App::DrivePlayer::DB
      on_progress    => sub { say $_[0] },
      on_track_found => sub { my $track = shift; ... },
  );

  $scanner->scan_folder($root_folder_id, 'My Music');

  # From within an on_progress callback:
  $scanner->stop;

=head1 DESCRIPTION

Walks a Google Drive folder hierarchy depth-first, recording every audio
file it finds into the DrivePlayer database.  Non-audio files and Google
Docs are silently ignored.

Metadata (title, artist, album, track number, year) is extracted from the
filename using common naming conventions, and supplemented by inferring
artist and album from the folder path when the filename alone is ambiguous.

Supported filename patterns:

  NN - Artist - Title.ext
  Artist - Title.ext          (en-dash also accepted)
  NN - Title.ext
  Title.ext

Year is extracted from a trailing C<(YYYY)> or C<[YYYY]> suffix.

A rescan of an existing folder replaces all previous data for that folder.

=head1 ATTRIBUTES

=head2 drive

  is: ro, required: 1

A L<Google::RestApi::DriveApi3> instance (or any object with a C<list>
method matching that interface).

=head2 db

  is: ro, required: 1

A L<App::DrivePlayer::DB> instance used to persist scan results.

=head2 on_progress

  is: ro, isa: CodeRef, optional

Called with a single string message as each folder is entered or when a
Drive API error occurs.  Calling L</stop> from within this callback will
prevent the current folder's Drive listing from being fetched.

=head2 on_track_found

  is: ro, isa: CodeRef, optional

Called with a track hashref each time an audio file is successfully stored.

=head1 METHODS



( run in 1.387 second using v1.01-cache-2.11-cpan-d7a12ab2c7f )