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 )