App-DrivePlayer

 view release on metacpan or  search on metacpan

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

    $meta{comment}      = $tags{comment}     if $tags{comment};
    if ($tags{tracknumber} && $tags{tracknumber} =~ /^(\d+)/) {
        $meta{track_number} = $1 + 0;
    }
    if ($tags{date} && $tags{date} =~ /^(\d{4})/) {
        $meta{year} = $1 + 0;
    }

    # Duration from FLAC stream info block
    my $info = $flac->info();
    if ($info && $info->{TOTALSAMPLES} && $info->{SAMPLERATE}) {
        $meta{duration_ms} = int($info->{TOTALSAMPLES} / $info->{SAMPLERATE} * 1000);
    } elsif ($flac->{trackTotalLengthSeconds}) {
        $meta{duration_ms} = int($flac->{trackTotalLengthSeconds} * 1000);
    }

    return %meta ? \%meta : undef;
}

# Probe a Drive file for duration (milliseconds) using ffprobe.
# Returns undef if ffprobe is unavailable or the probe fails.
sub probe_duration_ms {
    my ($class, $drive_id, $token) = @_;
    return unless ffprobe_available();

    my $ffprobe = -x '/usr/bin/ffprobe' ? '/usr/bin/ffprobe' : 'ffprobe';
    my $url     = sprintf($DRIVE_URL, $drive_id);
    my $header  = "Authorization: $token\r\n";

    my $out = q{};
    my $pid = open(my $fh, '-|', $ffprobe,
        '-v',            'error',
        '-headers',      $header,
        '-show_entries', 'format=duration',
        '-of',           'default=noprint_wrappers=1:nokey=1',
        $url,
    );
    if ($pid) {
        $out = do { local $/; <$fh> };
        close $fh;
    }
    return unless $out =~ /^(\d+(?:\.\d+)?)/m;
    return int($1 * 1000);
}

# Keep private alias for internal calls
sub _fpcalc_available { fpcalc_available() }

sub _download_partial {
    my ($self, $drive_id, $max_bytes) = @_;
    my $token = $self->token_fn->();
    unless ($token) {
        $log->warn("Fingerprint: no bearer token available") if $log;
        return;
    }

    my $url     = sprintf $DRIVE_URL, uri_escape_utf8($drive_id);
    my $max     = ($max_bytes // ($DOWNLOAD_MB * 1024 * 1024)) - 1;
    my $ua      = HTTP::Tiny->new(agent => $USER_AGENT, timeout => 30);
    my ($fh, $tmpfile) = tempfile(SUFFIX => '.audio', UNLINK => 0);
    binmode $fh;

    my $res = $ua->request('GET', $url, {
        headers       => {
            Authorization => $token,
            Range         => "bytes=0-$max",
        },
        data_callback => sub { print {$fh} $_[0] },
    });
    close $fh;

    if ($res->{success} || $res->{status} == 206) {
        $log->debug("Fingerprint: downloaded " . (-s $tmpfile) . " bytes for drive_id=$drive_id") if $log;
        return $tmpfile;
    }

    $log->warn("Fingerprint: HTTP $res->{status} downloading drive_id=$drive_id: $res->{reason}") if $log;
    unlink $tmpfile;
    return;
}

sub _fingerprint {
    my ($tmpfile) = @_;
    my $fpcalc = -x '/usr/bin/fpcalc' ? '/usr/bin/fpcalc' : 'fpcalc';
    my $json   = qx($fpcalc -json -length 120 \Q$tmpfile\E 2>/dev/null);
    return unless $json;
    my $data = eval { decode_json($json) } or return;
    return unless $data->{fingerprint} && $data->{duration};
    return { fingerprint => $data->{fingerprint}, duration => int($data->{duration}) };
}

sub _query_acoustid {
    my ($self, $fp) = @_;
    my $url = $AID_BASE
            . '?client='      . uri_escape_utf8($self->acoustid_key)
            . '&meta=recordings+compress'
            . '&duration='    . $fp->{duration}
            . '&fingerprint=' . uri_escape_utf8($fp->{fingerprint});

    my $data    = $self->_get_plain($url) or return;
    my $results = $data->{results}        or return;
    return unless @$results;

    # Pick the result with the highest score
    my ($best) = sort { $b->{score} <=> $a->{score} } @$results;
    return unless $best->{score} && $best->{score} > 0.5;

    my $recordings = $best->{recordings} or return;
    return unless @$recordings;
    return $recordings->[0]{id};
}

# ------------------------------------------------------------------
# HTTP helpers
# ------------------------------------------------------------------

sub _get_plain {
    my ($self, $url) = @_;
    my $ua  = HTTP::Tiny->new(agent => $USER_AGENT, timeout => 5);
    my $res = $ua->get($url);
    return unless $res->{success};



( run in 1.030 second using v1.01-cache-2.11-cpan-cdf2f3d4e48 )