Arquivos
pipe-viewer/lib/WWW/PipeViewer/Utils.pm
T
2020-10-30 18:25:50 +02:00

864 linhas
20 KiB
Perl

package WWW::PipeViewer::Utils;
use utf8;
use 5.014;
use warnings;
=head1 NAME
WWW::PipeViewer::Utils - Various utils.
=head1 SYNOPSIS
use WWW::PipeViewer::Utils;
my $yv_utils = WWW::PipeViewer::Utils->new(%opts);
print $yv_utils->format_time(3600);
=head1 SUBROUTINES/METHODS
=head2 new(%opts)
Options:
=over 4
=item thousand_separator => ""
Character used as thousand separator.
=item months => []
Month names for I<format_date()>
=item youtube_url_format => ""
A youtube URL format for sprintf(format, videoID).
=back
=cut
sub new {
my ($class, %opts) = @_;
my $self = bless {
thousand_separator => q{,},
youtube_url_format => 'https://www.youtube.com/watch?v=%s',
}, $class;
$self->{months} = [
qw(
Jan Feb Mar
Apr May Jun
Jul Aug Sep
Oct Nov Dec
)
];
foreach my $key (keys %{$self}) {
$self->{$key} = delete $opts{$key}
if exists $opts{$key};
}
foreach my $invalid_key (keys %opts) {
warn "Invalid key: '${invalid_key}'";
}
return $self;
}
=head2 extension($type)
Returns the extension format from a given type.
From a string like 'video/webm;+codecs="vp9"', it returns 'webm'.
=cut
sub extension {
my ($self, $type) = @_;
$type =~ /\bflv\b/i ? q{flv}
: $type =~ /\bopus\b/i ? q{opus}
: $type =~ /\b3gpp?\b/i ? q{3gp}
: $type =~ m{^video/(\w+)} ? $1
: $type =~ m{^audio/(\w+)} ? $1
: $type =~ /\bwebm\b/i ? q{webm}
: q{mp4};
}
=head2 format_time($sec)
Returns time from seconds.
=cut
sub format_time {
my ($self, $sec) = @_;
$sec >= 3600
? join q{:}, map { sprintf '%02d', $_ } $sec / 3600 % 24, $sec / 60 % 60, $sec % 60
: join q{:}, map { sprintf '%02d', $_ } $sec / 60 % 60, $sec % 60;
}
=head2 format_duration($duration)
Return seconds from duration (PT1H20M10S).
=cut
# PT5M3S -> 05:03
# PT1H20M10S -> 01:20:10
# PT16S -> 00:16
sub format_duration {
my ($self, $duration) = @_;
$duration // return 0;
my ($hour, $min, $sec) = (0, 0, 0);
$hour = $1 if ($duration =~ /(\d+)H/);
$min = $1 if ($duration =~ /(\d+)M/);
$sec = $1 if ($duration =~ /(\d+)S/);
$hour * 60 * 60 + $min * 60 + $sec;
}
=head2 format_date($date)
Return string "04 May 2010" from "2010-05-04T00:25:55.000Z"
=cut
sub format_date {
my ($self, $date) = @_;
# 2010-05-04T00:25:55.000Z
# to: 04 May 2010
$date =~ s{^
(?<year>\d{4})
-
(?<month>\d{2})
-
(?<day>\d{2})
.*
}
{$+{day} $self->{months}[$+{month} - 1] $+{year}}x;
return $date;
}
=head2 date_to_age($date)
Return the (approximated) age for a given date of the form "2010-05-04T00:25:55.000Z".
=cut
sub date_to_age {
my ($self, $date) = @_;
$date =~ m{^
(?<year>\d{4})
-
(?<month>\d{2})
-
(?<day>\d{2})
[a-zA-Z]
(?<hour>\d{2})
:
(?<min>\d{2})
:
(?<sec>\d{2})
}x || return undef;
my ($sec, $min, $hour, $day, $month, $year) = gmtime(time);
$year += 1900;
$month += 1;
my $lambda = sub {
if ($year == $+{year}) {
if ($month == $+{month}) {
if ($day == $+{day}) {
if ($hour == $+{hour}) {
if ($min == $+{min}) {
return join(' ', $sec - $+{sec}, 'seconds');
}
return join(' ', $min - $+{min}, 'minutes');
}
return join(' ', $hour - $+{hour}, 'hours');
}
return join(' ', $day - $+{day}, 'days');
}
return join(' ', $month - $+{month}, 'months');
}
if ($year - $+{year} == 1) {
my $month_diff = $+{month} - $month;
if ($month_diff > 0) {
return join(' ', 12 - $month_diff, 'months');
}
}
return join(' ', $year - $+{year}, 'years');
};
my $age = $lambda->();
if ($age =~ /^1\s/) { # singular mode
$age =~ s/s\z//;
}
return $age;
}
=head2 has_entries($result)
Returns true if a given result has entries.
=cut
sub has_entries {
my ($self, $result) = @_;
if (ref($result->{results}) eq 'HASH') {
foreach my $type(qw(comments videos playlists)) {
if (exists $result->{results}{$type}) {
return scalar @{$result->{results}{$type}} > 0;
}
}
my $type = $result->{results}{type} // '';
if ($type eq 'playlist') {
return $result->{results}{videoCount} > 0;
}
}
if (ref($result->{results}) eq 'ARRAY') {
return scalar(@{$result->{results}}) > 0;
}
if (ref($result->{results}) eq 'HASH' and not keys %{$result->{results}}) {
return 0;
}
return 1; # maybe?
#ref($result) eq 'HASH' and ($result->{results}{pageInfo}{totalResults} > 0);
}
=head2 normalize_video_title($title, $fat32safe)
Replace file-unsafe characters and trim spaces.
=cut
sub normalize_video_title {
my ($self, $title, $fat32safe) = @_;
if ($fat32safe) {
$title =~ s/: / - /g;
$title =~ tr{:"*/?\\|}{;'+%!%%}; # "
$title =~ tr/<>//d;
}
else {
$title =~ tr{/}{%};
}
join(q{ }, split(q{ }, $title));
}
=head2 format_text(%opt)
Formats a text with information from streaming and video info.
The structure of C<%opt> is:
(
streaming => HASH,
info => HASH,
text => STRING,
escape => BOOL,
fat32safe => BOOL,
)
=cut
sub format_text {
my ($self, %opt) = @_;
my $streaming = $opt{streaming};
my $info = $opt{info};
my $text = $opt{text};
my $escape = $opt{escape};
my $fat32safe = $opt{fat32safe};
my %special_tokens = (
ID => sub { $self->get_video_id($info) },
AUTHOR => sub { $self->get_channel_title($info) },
CHANNELID => sub { $self->get_channel_id($info) },
DEFINITION => sub { $self->get_definition($info) },
DIMENSION => sub { $self->get_dimension($info) },
VIEWS => sub { $self->get_views($info) },
VIEWS_SHORT => sub { $self->get_views_approx($info) },
LIKES => sub { $self->get_likes($info) },
DISLIKES => sub { $self->get_dislikes($info) },
COMMENTS => sub { $self->get_comments($info) },
DURATION => sub { $self->get_duration($info) },
TIME => sub { $self->get_time($info) },
TITLE => sub { $self->get_title($info) },
FTITLE => sub { $self->normalize_video_title($self->get_title($info), $fat32safe) },
CAPTION => sub { $self->get_caption($info) },
PUBLISHED => sub { $self->get_publication_date($info) },
AGE => sub { $self->get_publication_age($info) },
AGE_SHORT => sub { $self->get_publication_age_approx($info) },
DESCRIPTION => sub { $self->get_description($info) },
RATING => sub {
my $likes = $self->get_likes($info) // 0;
my $dislikes = $self->get_dislikes($info) // 0;
my $rating = 0;
if ($likes + $dislikes > 0) {
$rating = $likes / ($likes + $dislikes) * 5;
}
sprintf('%.2f', $rating);
},
(
defined($streaming)
? (
RESOLUTION => sub {
$streaming->{resolution} =~ /^\d+\z/
? $streaming->{resolution} . 'p'
: $streaming->{resolution};
},
ITAG => sub { $streaming->{streaming}{itag} },
SUB => sub { $streaming->{srt_file} },
VIDEO => sub { $streaming->{streaming}{url} },
FORMAT => sub { $self->extension($streaming->{streaming}{type}) },
AUDIO => sub {
ref($streaming->{streaming}{__AUDIO__}) eq 'HASH'
? $streaming->{streaming}{__AUDIO__}{url}
: q{};
},
AOV => sub {
ref($streaming->{streaming}{__AUDIO__}) eq 'HASH'
? $streaming->{streaming}{__AUDIO__}{url}
: $streaming->{streaming}{url};
},
)
: ()
),
URL => sub { sprintf($self->{youtube_url_format}, $self->get_video_id($info)) },
);
my $tokens_re = do {
local $" = '|';
qr/\*(@{[keys %special_tokens]})\*/;
};
my %special_escapes = (
a => "\a",
b => "\b",
e => "\e",
f => "\f",
n => "\n",
r => "\r",
t => "\t",
);
my $escapes_re = do {
local $" = q{};
qr/\\([@{[keys %special_escapes]}])/;
};
$text =~ s/$escapes_re/$special_escapes{$1}/g;
$escape
? $text =~ s/$tokens_re/\Q${\$special_tokens{$1}()}\E/gr
: $text =~ s/$tokens_re/${\$special_tokens{$1}()}/gr;
}
=head2 set_thousands($num)
Return the number with thousand separators.
=cut
sub set_thousands { # ugly, but fast
my ($self, $n) = @_;
return 0 unless $n;
length($n) > 3 or return $n;
my $l = length($n) - 3;
my $i = ($l - 1) % 3 + 1;
my $x = substr($n, 0, $i) . $self->{thousand_separator};
while ($i < $l) {
$x .= substr($n, $i, 3) . $self->{thousand_separator};
$i += 3;
}
return $x . substr($n, $i);
}
=head2 get_video_id($info)
Get videoID.
=cut
sub get_video_id {
my ($self, $info) = @_;
$info->{videoId};
#~ ref($info->{id}) eq 'HASH' ? $info->{id}{videoId}
#~ : exists($info->{snippet}{resourceId}{videoId}) ? $info->{snippet}{resourceId}{videoId}
#~ : exists($info->{contentDetails}{videoId}) ? $info->{contentDetails}{videoId}
#~ : exists($info->{contentDetails}{playlistItem}{resourceId}{videoId})
#~ ? $info->{contentDetails}{playlistItem}{resourceId}{videoId}
#~ : exists($info->{contentDetails}{upload}{videoId}) ? $info->{contentDetails}{upload}{videoId}
#~ : do {
#~ my $id = $info->{id} // return undef;
#~ if (length($id) != 11) {
#~ return undef;
#~ }
#~ $id;
#~ };
}
sub get_playlist_id {
my ($self, $info) = @_;
$info->{playlistId};
}
sub get_playlist_video_count {
my ($self, $info) = @_;
$info->{videoCount};
}
=head2 get_description($info)
Get description.
=cut
sub get_description {
my ($self, $info) = @_;
my $desc = $info->{descriptionHtml} // $info->{description} // '';
require URI::Escape;
require HTML::Entities;
# Decode external links
$desc =~ s{<a href="/redirect\?(.*?)".*?>.*?</a>}{
my $url = $1;
if ($url =~ /(?:^|;)q=([^&]+)/) {
URI::Escape::uri_unescape($1);
}
else {
$url;
}
}segi;
# Decode hashtags
$desc =~ s{<a href="/results\?search_query=.*?".*?>(.*?)</a>}{$1}sgi;
# Decode internal links to videos / playlists
$desc =~ s{<a href="/(watch\?.*?)".*?>(https://www\.youtube\.com)/watch\?.*?</a>}{
my $url = $2;
my $params = URI::Escape::uri_unescape($1);
"$url/$params";
}segi;
# Decode internal youtu.be links
$desc =~ s{<a href="/watch\?v=(.*?)".*?>(https://youtu\.be)/.*?</a>}{
my $url = $2;
my $params = URI::Escape::uri_unescape($1);
"$url/$params";
}segi;
# Decode other internal links
$desc =~ s{<a href="/(.*?)".*?>.*?</a>}{https://youtube.com/$1}sgi;
$desc =~ s{<br/?>}{\n}gi;
$desc =~ s{<a href="(.*?)".*?>.*?</a>}{$1}sgi;
$desc =~ s/<.*?>//gs;
$desc = HTML::Entities::decode_entities($desc);
$desc =~ s/^\s+//;
if (not $desc =~ /\S/) {
$desc = $info->{description} // '';
}
($desc =~ /\S/) ? $desc : 'No description available...';
}
=head2 get_title($info)
Get title.
=cut
sub get_title {
my ($self, $info) = @_;
$info->{title};
}
=head2 get_thumbnail_url($info;$type='default')
Get thumbnail URL.
=cut
sub get_thumbnail_url {
my ($self, $info, $type) = @_;
if (exists $info->{videoId}) {
$info->{type} = 'video';
}
if ($info->{type} eq 'playlist') {
return $info->{playlistThumbnail};
}
if ($info->{type} eq 'channel') {
ref($info->{authorThumbnails}) eq 'ARRAY' or return '';
return $info->{authorThumbnails}[0]{url};
}
ref($info->{videoThumbnails}) eq 'ARRAY' or return '';
my @thumbs = @{$info->{videoThumbnails}};
my @wanted = grep{$_->{quality} eq $type} @thumbs;
my $url;
if (@wanted) {
$url = $wanted[0]{url};
}
else {
warn "[!] Couldn't find thumbnail of type <<$type>>...";
$url = $thumbs[0]{url};
}
# Clean URL of trackers and other junk
$url =~ s/\.(?:jpg|png|webp)\K\?.*//;
return $url;
}
sub get_channel_title {
my ($self, $info) = @_;
#$info->{snippet}{channelTitle} || $self->get_channel_id($info);
$info->{author};
}
sub get_author {
my ($self, $info) = @_;
$info->{author};
}
sub get_comment_id {
my ($self, $info) = @_;
$info->{commentId};
}
sub get_comment_content {
my ($self, $info) = @_;
$info->{content};
}
sub get_id {
my ($self, $info) = @_;
#$info->{id};
$info->{videoId};
}
sub get_channel_id {
my ($self, $info) = @_;
#$info->{snippet}{resourceId}{channelId} // $info->{snippet}{channelId};
$info->{authorId};
}
sub get_category_id {
my ($self, $info) = @_;
#$info->{snippet}{resourceId}{categoryId} // $info->{snippet}{categoryId};
#"unknown";
$info->{genre} // 'Unknown';
}
sub get_category_name {
my ($self, $info) = @_;
state $categories = {
1 => 'Film & Animation',
2 => 'Autos & Vehicles',
10 => 'Music',
15 => 'Pets & Animals',
17 => 'Sports',
19 => 'Travel & Events',
20 => 'Gaming',
22 => 'People & Blogs',
23 => 'Comedy',
24 => 'Entertainment',
25 => 'News & Politics',
26 => 'Howto & Style',
27 => 'Education',
28 => 'Science & Technology',
29 => 'Nonprofits & Activism',
};
#$categories->{$self->get_category_id($info) // ''} // 'Unknown';
$info->{genre} // 'Unknown';
}
sub get_publication_date {
my ($self, $info) = @_;
#$self->format_date($info->{snippet}{publishedAt});
#$self->format_date
require Time::Piece;
my $time = Time::Piece->new($info->{published});
$time->strftime("%d %B %Y");
}
sub get_publication_age {
my ($self, $info) = @_;
($info->{publishedText} // '') =~ s/\sago\z//r;;
}
sub get_publication_age_approx {
my ($self, $info) = @_;
my $age = $self->get_publication_age($info) // '';
if ($age =~ /hour|min|sec/) {
return "0d";
}
if ($age =~ /^(\d+) day/) {
return "$1d";
}
if ($age =~ /^(\d+) week/) {
return "$1w";
}
if ($age =~ /^(\d+) month/) {
return "$1m";
}
if ($age =~ /^(\d+) year/) {
return "$1y";
}
return $age;
}
sub get_duration {
my ($self, $info) = @_;
#$self->format_duration($info->{contentDetails}{duration});
#$self->format_duration($info->{lengthSeconds});
$info->{lengthSeconds};
}
sub get_time {
my ($self, $info) = @_;
if ($info->{liveNow}) {
return 'LIVE';
}
$self->format_time($self->get_duration($info));
#$self->format_time($self->get_duration($info));
}
sub get_definition {
my ($self, $info) = @_;
#uc($info->{contentDetails}{definition} // '-');
#...;
"unknown";
}
sub get_dimension {
my ($self, $info) = @_;
#uc($info->{contentDetails}{dimension});
#...;
"unknown";
}
sub get_caption {
my ($self, $info) = @_;
#$info->{contentDetails}{caption};
#...;
"unknown";
}
sub get_views {
my ($self, $info) = @_;
$info->{viewCount} // 0;
}
sub get_views_approx {
my ($self, $info) = @_;
my $views = $self->get_views($info);
if ($views < 1000) {
return $views;
}
if ($views >= 10 * 1e9) { # ten billions
return sprintf("%dB", int($views / 1e9));
}
if ($views >= 1e9) { # billions
return sprintf("%.2gB", $views / 1e9);
}
if ($views >= 10 * 1e6) { # ten millions
return sprintf("%dM", int($views / 1e6));
}
if ($views >= 1e6) { # millions
return sprintf("%.2gM", $views / 1e6);
}
if ($views >= 10 * 1e3) { # ten thousands
return sprintf("%dK", int($views / 1e3));
}
if ($views >= 1e3) { # thousands
return sprintf("%.2gK", $views / 1e3);
}
return $views;
}
sub get_likes {
my ($self, $info) = @_;
$info->{likeCount} // 0;
}
sub get_dislikes {
my ($self, $info) = @_;
$info->{dislikeCount} // 0;
}
sub get_comments {
my ($self, $info) = @_;
#$info->{statistics}{commentCount};
1;
}
{
no strict 'refs';
foreach my $pair (
[playlist => {'playlist' => 1}],
[channel => {'channel' => 1}],
[video => {'video' => 1, 'playlistItem' => 1}],
[subscription => {'subscription' => 1}],
[activity => {'activity' => 1}],
) {
*{__PACKAGE__ . '::' . 'is_' . $pair->[0]} = sub {
my ($self, $item) = @_;
if ($pair->[0] eq 'video') {
return 1 if exists $item->{videoId};
}
exists $pair->[1]{$item->{type} // ''};
#~ if (ref($item->{id}) eq 'HASH') {
#~ if (exists $pair->[1]{$item->{id}{kind}}) {
#~ return 1;
#~ }
#~ }
#~ elsif (exists $item->{kind}) {
#~ if (exists $pair->[1]{$item->{kind}}) {
#~ return 1;
#~ }
#~ }
#~ return;
};
}
}
sub is_channelID {
my ($self, $id) = @_;
$id || return;
$id eq 'mine' or $id =~ /^UC[-a-zA-Z0-9_]{22}\z/;
}
sub is_videoID {
my ($self, $id) = @_;
$id || return;
$id =~ /^[-a-zA-Z0-9_]{11}\z/;
}
sub period_to_date {
my ($self, $amount, $period) = @_;
state $day = 60 * 60 * 24;
state $week = $day * 7;
state $month = $day * 30.4368;
state $year = $day * 365.242;
my $time = $amount * (
$period =~ /^d/i ? $day
: $period =~ /^w/i ? $week
: $period =~ /^m/i ? $month
: $period =~ /^y/i ? $year
: 0
);
my $now = time;
my @time = gmtime($now - $time);
join('-', $time[5] + 1900, sprintf('%02d', $time[4] + 1), sprintf('%02d', $time[3])) . 'T'
. join(':', sprintf('%02d', $time[2]), sprintf('%02d', $time[1]), sprintf('%02d', $time[0])) . 'Z';
}
=head1 AUTHOR
Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >>
=head1 SUPPORT
You can find documentation for this module with the perldoc command.
perldoc WWW::PipeViewer::Utils
=head1 LICENSE AND COPYRIGHT
Copyright 2012-2020 Trizen.
This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.
See L<http://dev.perl.org/licenses/> for more information.
=cut
1; # End of WWW::PipeViewer::Utils