diff options
author | Jesús <heckyel@hyperbola.info> | 2020-03-02 08:18:54 -0500 |
---|---|---|
committer | Jesús <heckyel@hyperbola.info> | 2020-03-02 08:18:54 -0500 |
commit | c67158fa409f1b1b4f98a8621a69bb2013b76451 (patch) | |
tree | bb4ca80f29cda70734a868d372e00c85e172e7d3 /lib/WWW/FairViewer/Utils.pm | |
parent | ad7ec1785fc28799e10d10e7a679dc5bb4891ee3 (diff) | |
download | fair-viewer-c67158fa409f1b1b4f98a8621a69bb2013b76451.tar.lz fair-viewer-c67158fa409f1b1b4f98a8621a69bb2013b76451.tar.xz fair-viewer-c67158fa409f1b1b4f98a8621a69bb2013b76451.zip |
rebrand app
Diffstat (limited to 'lib/WWW/FairViewer/Utils.pm')
-rw-r--r-- | lib/WWW/FairViewer/Utils.pm | 801 |
1 files changed, 801 insertions, 0 deletions
diff --git a/lib/WWW/FairViewer/Utils.pm b/lib/WWW/FairViewer/Utils.pm new file mode 100644 index 0000000..5a9296a --- /dev/null +++ b/lib/WWW/FairViewer/Utils.pm @@ -0,0 +1,801 @@ +package WWW::FairViewer::Utils; + +use utf8; +use 5.014; +use warnings; + +=head1 NAME + +WWW::FairViewer::Utils - Various utils. + +=head1 SYNOPSIS + + use WWW::FairViewer::Utils; + + my $yv_utils = WWW::FairViewer::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 =~ /\bwebm\b/i ? q{webm} + : $type =~ /\b3gpp?\b/i ? q{3gp} + : $type =~ m{^video/(\w+)} ? $1 + : $type =~ m{^audio/(\w+)} ? $1 + : 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') { + + if (exists $result->{results}{comments}) { + return scalar @{$result->{results}{comments}} > 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; + } + + 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->{description}; + (defined($desc) and $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; + + if (@wanted) { + return $wanted[0]{url}; + } + + warn "[!] Couldn't find thumbnail of type <<$type>>..."; + $thumbs[0]{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::FairViewer::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::FairViewer::Utils |