diff options
author | trizen <trizen@protonmail.com> | 2020-02-15 00:05:24 +0200 |
---|---|---|
committer | trizen <trizen@protonmail.com> | 2020-02-15 00:05:24 +0200 |
commit | 8e4ba906343718a4a1c2fbd939a9dbe0a75287f8 (patch) | |
tree | 340f4a502ddefbdfc0a5eec997751b5e19206aa8 /lib/WWW/StrawViewer | |
parent | 3e92a9d96d2ff6d2718a5a1e0d69dac766f6a141 (diff) | |
download | fair-viewer-8e4ba906343718a4a1c2fbd939a9dbe0a75287f8.tar.lz fair-viewer-8e4ba906343718a4a1c2fbd939a9dbe0a75287f8.tar.xz fair-viewer-8e4ba906343718a4a1c2fbd939a9dbe0a75287f8.zip |
Import files.
Diffstat (limited to 'lib/WWW/StrawViewer')
-rw-r--r-- | lib/WWW/StrawViewer/Activities.pm | 93 | ||||
-rw-r--r-- | lib/WWW/StrawViewer/Authentication.pm | 216 | ||||
-rw-r--r-- | lib/WWW/StrawViewer/Channels.pm | 190 | ||||
-rw-r--r-- | lib/WWW/StrawViewer/CommentThreads.pm | 103 | ||||
-rw-r--r-- | lib/WWW/StrawViewer/GetCaption.pm | 280 | ||||
-rw-r--r-- | lib/WWW/StrawViewer/GuideCategories.pm | 85 | ||||
-rw-r--r-- | lib/WWW/StrawViewer/Itags.pm | 319 | ||||
-rw-r--r-- | lib/WWW/StrawViewer/ParseJSON.pm | 76 | ||||
-rw-r--r-- | lib/WWW/StrawViewer/ParseXML.pm | 311 | ||||
-rw-r--r-- | lib/WWW/StrawViewer/PlaylistItems.pm | 167 | ||||
-rw-r--r-- | lib/WWW/StrawViewer/Playlists.pm | 124 | ||||
-rw-r--r-- | lib/WWW/StrawViewer/RegularExpressions.pm | 89 | ||||
-rw-r--r-- | lib/WWW/StrawViewer/Search.pm | 175 | ||||
-rw-r--r-- | lib/WWW/StrawViewer/Subscriptions.pm | 272 | ||||
-rw-r--r-- | lib/WWW/StrawViewer/Utils.pm | 735 | ||||
-rw-r--r-- | lib/WWW/StrawViewer/VideoCategories.pm | 97 | ||||
-rw-r--r-- | lib/WWW/StrawViewer/Videos.pm | 229 |
17 files changed, 3561 insertions, 0 deletions
diff --git a/lib/WWW/StrawViewer/Activities.pm b/lib/WWW/StrawViewer/Activities.pm new file mode 100644 index 0000000..39a6a3d --- /dev/null +++ b/lib/WWW/StrawViewer/Activities.pm @@ -0,0 +1,93 @@ +package WWW::StrawViewer::Activities; + +use utf8; +use 5.014; +use warnings; + +=head1 NAME + +WWW::StrawViewer::Activities - list of channel activity events that match the request criteria. + +=head1 SYNOPSIS + + use WWW::StrawViewer; + my $obj = WWW::StrawViewer->new(%opts); + my $activities = $obj->activities($channel_id); + +=head1 SUBROUTINES/METHODS + +=cut + +sub _make_activities_url { + my ($self, %opts) = @_; + $self->_make_feed_url('activities', part => 'snippet,contentDetails', %opts); +} + +=head2 activities($channel_id) + +Get activities for channel ID. + +=cut + +sub activities { + my ($self, $channel_id) = @_; + + if ($channel_id eq 'mine') { + return $self->my_activities; + } + + if ($channel_id !~ /^UC/) { + $channel_id = $self->channel_id_from_username($channel_id) // $channel_id; + } + + $self->_get_results($self->_make_activities_url(channelId => $channel_id)); +} + +=head2 activities_from_username($username) + +Get activities for username. + +=cut + +sub activities_from_username { + my ($self, $username) = @_; + return $self->activities($username); +} + +=head2 my_activities() + +Get authenticated user's activities. + +=cut + +sub my_activities { + my ($self) = @_; + $self->get_access_token() // return; + $self->_get_results($self->_make_activities_url(mine => 'true')); +} + +=head1 AUTHOR + +Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >> + + +=head1 SUPPORT + +You can find documentation for this module with the perldoc command. + + perldoc WWW::StrawViewer::Activities + + +=head1 LICENSE AND COPYRIGHT + +Copyright 2013-2015 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::StrawViewer::Activities diff --git a/lib/WWW/StrawViewer/Authentication.pm b/lib/WWW/StrawViewer/Authentication.pm new file mode 100644 index 0000000..1fa2368 --- /dev/null +++ b/lib/WWW/StrawViewer/Authentication.pm @@ -0,0 +1,216 @@ +package WWW::StrawViewer::Authentication; + +use utf8; +use 5.014; +use warnings; + +=head1 NAME + +WWW::StrawViewer::Authentication - OAuth login support. + +=head1 SYNOPSIS + + use WWW::StrawViewer; + my $hash_ref = WWW::StrawViewer->oauth_login($code); + +=head1 SUBROUTINES/METHODS + +=cut + +sub _get_token_oauth_url { + my ($self) = @_; + return $self->get_oauth_url() . 'token'; +} + +=head2 oauth_refresh_token() + +Refresh the access_token using the refresh_token. Returns a HASH ref with the `access_token` or undef. + +=cut + +sub oauth_refresh_token { + my ($self) = @_; + + my $json_data = $self->lwp_post( + $self->_get_token_oauth_url(), + [Content => $self->get_www_content_type, + client_id => $self->get_client_id() // return, + client_secret => $self->get_client_secret() // return, + refresh_token => $self->get_refresh_token() // return, + grant_type => 'refresh_token', + ] + ); + + return $self->parse_json_string($json_data); +} + +=head2 get_accounts_oauth_url() + +Creates an OAuth URL with the 'code' response type. (Google's authorization server) + +=cut + +sub get_accounts_oauth_url { + my ($self) = @_; + + my $url = $self->_append_url_args( + ($self->get_oauth_url() . 'auth'), + response_type => 'code', + client_id => $self->get_client_id() // return, + redirect_uri => $self->get_redirect_uri() // return, + scope => 'https://www.googleapis.com/auth/youtube.force-ssl', + access_type => 'offline', + ); + return $url; +} + +=head2 oauth_login($code) + +Returns a HASH ref with the access_token, refresh_token and some other info. + +The $code can be obtained by going to the URL returned by the C<get_accounts_oauth_url()> method. + +=cut + +sub oauth_login { + my ($self, $code) = @_; + + length($code) < 20 and return; + + my $json_data = $self->lwp_post( + $self->_get_token_oauth_url(), + [Content => $self->get_www_content_type, + client_id => $self->get_client_id() // return, + client_secret => $self->get_client_secret() // return, + redirect_uri => $self->get_redirect_uri() // return, + grant_type => 'authorization_code', + code => $code, + ] + ); + + return $self->parse_json_string($json_data); +} + +sub __AUTH_EOL__() { "\0\0\0" } + +=head2 load_authentication_tokens() + +Will try to load the access and refresh tokens from I<authentication_file>. + +=cut + +sub load_authentication_tokens { + my ($self) = @_; + + if (defined $self->get_access_token and defined $self->get_refresh_token) { + return 1; + } + + my $file = $self->get_authentication_file() // return; + my $key = $self->get_key() // return; + + if (-f $file) { + local $/ = __AUTH_EOL__; + open my $fh, '<:raw', $file or return; + + my @tokens; + foreach my $i (0 .. 1) { + chomp(my $token = <$fh>); + $token =~ /\S/ || last; + push @tokens, $self->decode_token($token); + } + + $self->set_access_token($tokens[0]) // return; + $self->set_refresh_token($tokens[1]) // return; + + close $fh; + return 1; + } + + return; +} + +=head2 encode_token($token) + +Encode the token with the I<key> and return it. + +=cut + +sub encode_token { + my ($self, $token) = @_; + + if (defined(my $key = $self->get_key)) { + require MIME::Base64; + return MIME::Base64::encode_base64($token ^ substr($key, -length($token))); + } + + return; +} + +=head2 decode_token($token) + +Decode the token with the I<key> and return it. + +=cut + +sub decode_token { + my ($self, $token) = @_; + + if (defined(my $key = $self->get_key)) { + require MIME::Base64; + my $bin = MIME::Base64::decode_base64($token); + return $bin ^ substr($key, -length($bin)); + } + + return; +} + +=head2 save_authentication_tokens() + +Encode and save the access and refresh into the I<authentication_file>. + +=cut + +sub save_authentication_tokens { + my ($self) = @_; + + my $file = $self->get_authentication_file() // return; + my $access_token = $self->get_access_token() // return; + my $refresh_token = $self->get_refresh_token() // return; + + if (open my $fh, '>:raw', $file) { + foreach my $token ($access_token, $refresh_token) { + print {$fh} $self->encode_token($token) . __AUTH_EOL__; + } + close $fh; + return 1; + } + + return; +} + +=head1 AUTHOR + +Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >> + + +=head1 SUPPORT + +You can find documentation for this module with the perldoc command. + + perldoc WWW::StrawViewer::Authentication + + +=head1 LICENSE AND COPYRIGHT + +Copyright 2013-2015 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::StrawViewer::Authentication diff --git a/lib/WWW/StrawViewer/Channels.pm b/lib/WWW/StrawViewer/Channels.pm new file mode 100644 index 0000000..d48c744 --- /dev/null +++ b/lib/WWW/StrawViewer/Channels.pm @@ -0,0 +1,190 @@ +package WWW::StrawViewer::Channels; + +use utf8; +use 5.014; +use warnings; + +=head1 NAME + +WWW::StrawViewer::Channels - Channels interface. + +=head1 SYNOPSIS + + use WWW::StrawViewer; + my $obj = WWW::StrawViewer->new(%opts); + my $videos = $obj->channels_from_categoryID($category_id); + +=head1 SUBROUTINES/METHODS + +=cut + +sub _make_channels_url { + my ($self, %opts) = @_; + return $self->_make_feed_url('channels', %opts); +} + +=head2 channels_from_categoryID($category_id) + +Return the YouTube channels associated with the specified category. + +=head2 channels_info($channel_id) + +Return information for the comma-separated list of the YouTube channel ID(s). + +=head1 Channel details + +For all functions, C<$channels->{results}{items}> contains: + +=cut + +{ + no strict 'refs'; + + foreach my $method ( + { + key => 'categoryId', + name => 'channels_from_guide_category', + }, + { + key => 'id', + name => 'channels_info', + }, + { + key => 'forUsername', + name => 'channels_from_username', + }, + ) { + *{__PACKAGE__ . '::' . $method->{name}} = sub { + my ($self, $channel_id) = @_; + return $self->_get_results($self->_make_channels_url($method->{key} => $channel_id)); + }; + } + + foreach my $part (qw(id contentDetails statistics topicDetails)) { + *{__PACKAGE__ . '::' . 'channels_' . $part} = sub { + my ($self, $id) = @_; + return $self->_get_results($self->_make_channels_url(id => $id, part => $part)); + }; + } +} + +=head2 my_channel() + +Returns info about the channel of the current authenticated user. + +=cut + +sub my_channel { + my ($self) = @_; + $self->get_access_token() // return; + return $self->_get_results($self->_make_channels_url(part => 'snippet', mine => 'true')); +} + +=head2 my_channel_id() + +Returns the channel ID of the current authenticated user. + +=cut + +sub my_channel_id { + my ($self) = @_; + + state $cache = {}; + + if (exists $cache->{id}) { + return $cache->{id}; + } + + $cache->{id} = undef; + my $channel = $self->my_channel() // return; + $cache->{id} = $channel->{results}{items}[0]{id} // return; +} + +=head2 channels_my_subscribers() + +Retrieve a list of channels that subscribed to the authenticated user's channel. + +=cut + +sub channels_my_subscribers { + my ($self) = @_; + $self->get_access_token() // return; + return $self->_get_results($self->_make_channels_url(mySubscribers => 'true')); +} + +=head2 channel_id_from_username($username) + +Return the channel ID for an username. + +=cut + +sub channel_id_from_username { + my ($self, $username) = @_; + + state $username_lookup = {}; + + if (exists $username_lookup->{$username}) { + return $username_lookup->{$username}; + } + + $username_lookup->{$username} = undef; + my $channel = $self->channels_from_username($username) // return; + $username_lookup->{$username} = $channel->{results}{items}[0]{id} // return; +} + +=head2 channel_title_from_id($channel_id) + +Return the channel title for a given channel ID. + +=cut + +sub channel_title_from_id { + my ($self, $channel_id) = @_; + + if ($channel_id eq 'mine') { + $channel_id = $self->my_channel_id(); + } + + my $info = $self->channels_info($channel_id // return) // return; + + ( ref($info) eq 'HASH' + and ref($info->{results}) eq 'HASH' + and ref($info->{results}{items}) eq 'ARRAY' + and ref($info->{results}{items}[0]) eq 'HASH') + ? $info->{results}{items}[0]{snippet}{title} + : (); +} + +=head2 channels_contentDetails($channelID) + +=head2 channels_statistics($channelID); + +=head2 channels_topicDetails($channelID) + +=cut + +=head1 AUTHOR + +Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >> + + +=head1 SUPPORT + +You can find documentation for this module with the perldoc command. + + perldoc WWW::StrawViewer::Channels + + +=head1 LICENSE AND COPYRIGHT + +Copyright 2013-2015 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::StrawViewer::Channels diff --git a/lib/WWW/StrawViewer/CommentThreads.pm b/lib/WWW/StrawViewer/CommentThreads.pm new file mode 100644 index 0000000..499d930 --- /dev/null +++ b/lib/WWW/StrawViewer/CommentThreads.pm @@ -0,0 +1,103 @@ +package WWW::StrawViewer::CommentThreads; + +use utf8; +use 5.014; +use warnings; + +=head1 NAME + +WWW::StrawViewer::CommentThreads - Retrieve comments threads. + +=head1 SYNOPSIS + + use WWW::StrawViewer; + my $obj = WWW::StrawViewer->new(%opts); + my $videos = $obj->comments_from_video_id($video_id); + +=head1 SUBROUTINES/METHODS + +=cut + +sub _make_commentThreads_url { + my ($self, %opts) = @_; + return + $self->_make_feed_url( + 'commentThreads', + pageToken => $self->page_token, + %opts + ); +} + +=head2 comments_from_videoID($videoID) + +Retrieve comments from a video ID. + +=cut + +sub comments_from_video_id { + my ($self, $video_id) = @_; + return + $self->_get_results( + $self->_make_commentThreads_url( + videoId => $video_id, + textFormat => 'plainText', + order => $self->get_comments_order, + part => 'snippet,replies' + ), + simple => 1, + ); +} + +=head2 comment_to_video_id($comment, $videoID) + +Send a comment to a video ID. + +=cut + +sub comment_to_video_id { + my ($self, $comment, $video_id) = @_; + + my $url = $self->_simple_feeds_url('commentThreads', part => 'snippet'); + + my $hash = { + "snippet" => { + + "topLevelComment" => { + "snippet" => { + "textOriginal" => $comment, + } + }, + "videoId" => $video_id, + + #"channelId" => $channel_id, + }, + }; + + $self->post_as_json($url, $hash); +} + +=head1 AUTHOR + +Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >> + + +=head1 SUPPORT + +You can find documentation for this module with the perldoc command. + + perldoc WWW::StrawViewer::CommentThreads + + +=head1 LICENSE AND COPYRIGHT + +Copyright 2015-2016 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::StrawViewer::CommentThreads diff --git a/lib/WWW/StrawViewer/GetCaption.pm b/lib/WWW/StrawViewer/GetCaption.pm new file mode 100644 index 0000000..81af4e2 --- /dev/null +++ b/lib/WWW/StrawViewer/GetCaption.pm @@ -0,0 +1,280 @@ +package WWW::StrawViewer::GetCaption; + +use utf8; +use 5.014; +use warnings; + +=head1 NAME + +WWW::StrawViewer::GetCaption - Save the YouTube closed captions as .srt files for a videoID. + +=head1 SYNOPSIS + + use WWW::StrawViewer::GetCaption; + + my $yv_cap = WWW::StrawViewer::GetCaption->new(%opts); + + print $yv_cap->get_caption($videoID); + +=head1 SUBROUTINES/METHODS + +=head2 new(%opts) + +Options: + +=over 4 + +=item captions => [] + +The captions data. + +=item captions_dir => "." + +Where to save the closed captions. + +=item languages => [qw(en es ro jp)] + +Preferred languages. First found is saved and returned. + +=back + +=cut + +sub new { + my ($class, %opts) = @_; + + my $self = bless {}, $class; + $self->{captions_dir} = undef; + $self->{captions} = []; + $self->{auto_captions} = 0; + $self->{languages} = [qw(en es)]; + + 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 find_caption_data() + +Find a caption data, based on the preferred languages. + +=cut + +sub find_caption_data { + my ($self) = @_; + + my @found; + foreach my $caption (@{$self->{captions}}) { + if (defined $caption->{languageCode}) { + foreach my $i (0 .. $#{$self->{languages}}) { + my $lang = $self->{languages}[$i]; + if ($caption->{languageCode} =~ /^\Q$lang\E(?:\z|[_-])/i) { + + # Automatic Speech Recognition + my $auto = defined($caption->{kind}) && lc($caption->{kind}) eq 'asr'; + + # Check against auto-generated captions + if ($auto and not $self->{auto_captions}) { + next; + } + + # Fuzzy match or auto-generated caption + if (lc($caption->{languageCode}) ne lc($lang) or $auto) { + $found[$i + (($auto ? 2 : 1) * scalar(@{$self->{languages}}))] = $caption; + } + + # Perfect match + else { + $i == 0 and return $caption; + $found[$i] = $caption; + } + } + } + } + } + + foreach my $caption (@found) { + return $caption if defined($caption); + } + + return; +} + +=head2 sec2time(@seconds) + +Convert a list of seconds to .srt times. + +=cut + +sub sec2time { + my $self = shift; + + my @out; + foreach my $sec (map { sprintf '%.3f', $_ } @_) { + push @out, + sprintf('%02d:%02d:%02d,%03d', ($sec / 3600 % 24, $sec / 60 % 60, $sec % 60, substr($sec, index($sec, '.') + 1))); + } + + return @out; +} + +=head2 xml2srt($xml_string) + +Convert the XML data to SubRip format. + +=cut + +sub xml2srt { + my ($self, $xml) = @_; + + require WWW::StrawViewer::ParseXML; + my $hash = eval { WWW::StrawViewer::ParseXML::xml2hash($xml) } // return; + + my $sections; + if ( exists $hash->{transcript} + and ref($hash->{transcript}) eq 'ARRAY' + and ref($hash->{transcript}[0]) eq 'HASH' + and exists $hash->{transcript}[0]{text}) { + $sections = $hash->{transcript}[0]{text}; + } + else { + return; + } + + require HTML::Entities; + + my @text; + foreach my $i (0 .. $#{$sections}) { + my $line = $sections->[$i]; + + if (not defined($line->{'-dur'})) { + if (exists $sections->[$i + 1]) { + $line->{'-dur'} = $sections->[$i + 1]{'-start'} - $line->{'-start'}; + } + else { + $line->{'-dur'} = 10; + } + } + + my $start = $line->{'-start'}; + my $end = $start + $line->{'-dur'}; + + push @text, + join("\n", + $i + 1, + join(' --> ', $self->sec2time($start, $end)), + HTML::Entities::decode_entities($line->{'#text'} // '')); + } + + return join("\n\n", @text); +} + +=head2 get_xml_data($caption_data) + +Get the XML content for a given caption data. + +=cut + +sub get_xml_data { + my ($self, $url) = @_; + + state $lwp = do { + + require LWP::UserAgent; + + my $agent = LWP::UserAgent->new( + timeout => 30, + env_proxy => 1, + agent => + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36', + ); + + require LWP::ConnCache; + state $cache = LWP::ConnCache->new; + $cache->total_capacity(undef); # no limit + + state $accepted_encodings = do { + require HTTP::Message; + HTTP::Message::decodable(); + }; + + $agent->ssl_opts(Timeout => 30); + $agent->default_header('Accept-Encoding' => $accepted_encodings); + $agent->conn_cache($cache); + + $agent; + }; + + my $req = $lwp->get($url); + + if ($req->is_success) { + return $req->decoded_content; + } + + return; +} + +=head2 save_caption($video_ID) + +Save the caption in a .srt file and return its file path. + +=cut + +sub save_caption { + my ($self, $video_id) = @_; + + # Find one of the preferred languages + my $info = $self->find_caption_data() // return; + + require File::Spec; + my $filename = "${video_id}_$info->{languageCode}.srt"; + my $srt_file = File::Spec->catfile($self->{captions_dir} // File::Spec->tmpdir, $filename); + + # Return the srt file if it already exists + return $srt_file if (-e $srt_file); + + # Get XML data, then transform it to SubRip data + my $xml = $self->get_xml_data($info->{baseUrl} // return) // return; + my $srt = $self->xml2srt($xml) // return; + + # Write the SubRib data to the $srt_file + open(my $fh, '>:utf8', $srt_file) or return; + print {$fh} $srt, "\n"; + close $fh; + + # Return the .srt file path + return $srt_file; +} + +=head1 AUTHOR + +Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >> + + +=head1 SUPPORT + +You can find documentation for this module with the perldoc command. + + perldoc WWW::StrawViewer::GetCaption + + +=head1 LICENSE AND COPYRIGHT + +Copyright 2012-2015 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::StrawViewer::GetCaption diff --git a/lib/WWW/StrawViewer/GuideCategories.pm b/lib/WWW/StrawViewer/GuideCategories.pm new file mode 100644 index 0000000..1f164b1 --- /dev/null +++ b/lib/WWW/StrawViewer/GuideCategories.pm @@ -0,0 +1,85 @@ +package WWW::StrawViewer::GuideCategories; + +use utf8; +use 5.014; +use warnings; + +=head1 NAME + +WWW::StrawViewer::GuideCategories - Categories interface. + +=head1 SYNOPSIS + + use WWW::StrawViewer; + my $obj = WWW::StrawViewer->new(%opts); + my $videos = $obj->youtube_categories('US'); + +=head1 SUBROUTINES/METHODS + +=cut + +sub _make_guideCategories_url { + my ($self, %opts) = @_; + + if (not exists $opts{id}) { + $opts{regionCode} //= $self->get_regionCode; + } + + $self->_make_feed_url('guideCategories', hl => $self->get_hl, %opts); +} + +=head2 guide_categories(;$region_id) + +Return guide categories for a specific region ID. + +=head2 guide_categories_info($category_id) + +Return info for a list of comma-separated category IDs. + +=cut + +{ + no strict 'refs'; + + foreach my $method ( + { + key => 'id', + name => 'guide_categories_info', + }, + { + key => 'regionCode', + name => 'guide_categories', + }, + ) { + *{__PACKAGE__ . '::' . $method->{name}} = sub { + my ($self, $id) = @_; + return $self->_get_results($self->_make_guideCategories_url($method->{key} => $id // return)); + }; + } +} + +=head1 AUTHOR + +Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >> + + +=head1 SUPPORT + +You can find documentation for this module with the perldoc command. + + perldoc WWW::StrawViewer::GuideCategories + + +=head1 LICENSE AND COPYRIGHT + +Copyright 2013-2015 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::StrawViewer::GuideCategories diff --git a/lib/WWW/StrawViewer/Itags.pm b/lib/WWW/StrawViewer/Itags.pm new file mode 100644 index 0000000..95ed483 --- /dev/null +++ b/lib/WWW/StrawViewer/Itags.pm @@ -0,0 +1,319 @@ +package WWW::StrawViewer::Itags; + +use utf8; +use 5.014; +use warnings; + +=head1 NAME + +WWW::StrawViewer::Itags - Get the YouTube itags. + +=head1 SYNOPSIS + + use WWW::StrawViewer::Itags; + + my $yv_itags = WWW::StrawViewer::Itags->new(); + + my $itags = $yv_itags->get_itags(); + my $res = $yv_itags->get_resolutions(); + +=head1 SUBROUTINES/METHODS + +=head2 new() + +Return the blessed object. + +=cut + +sub new { + my ($class) = @_; + bless {}, $class; +} + +=head2 get_itags() + +Get a HASH ref with the YouTube itags. {resolution => [itags]}. + +Reference: http://en.wikipedia.org/wiki/YouTube#Quality_and_formats + +=cut + +sub get_itags { +#<<< + scalar { + 'best' => [ + 38, # mp4 (3072p) (v-a) + [138, # mp4 (2160p-4320p) (v) + 266, # mp4 (2160p-2304p) (v) + ], + ], + + '2160' => [ + [ + 315, # webm HFR (v) + 272, # webm (v) + 313, # webm (v) + 401, # av1 (v) + ], + ], + + '1440' => [ + [ + 308, # webm HFR (v) + 271, # webm (v) + 264, # mp4 (v) + 400, # av1 (v) + ], + ], + + '1080' => [ + [303, # webm HFR (v) + 299, # mp4 HFR (v) + ], + 46, # webm (v-a) + 37, # mp4 (v-a) + [248, # webm (v) + 137, # mp4 (v) + 399, # av1 (v) + ], + 301, # mp4 (live) (v-a) + 96, # ts (live) (v-a) + ], + + '720' => [ + [302, # webm HFR (v) + 298, # mp4 HFR (v) + ], + 45, # webm (v-a) + 22, # mp4 (v-a) + [247, # webm (v) + 136, # mp4 (v) + 398, # av1 (v) + ], + 300, # mp4 (live) (v-a) + 120, # flv (live) (v-a) + 95, # ts (live) (v-a) + ], + + '480' => [ + 44, # webm (v-a) + 35, # flv (v-a) + [244, # webm (v) + 135, # mp4 (v) + 397, # av1 (v) + ], + 94, # mp4 (live) (v-a) + ], + + '360' => [ + 43, # webm (v-a) + 34, # flv (v-a) + [243, # webm (v) + 134, # mp4 (v) + 396, # av1 (v) + ], + 93, # mp4 (live) (v-a) + 18, # mp4 (v-a) + ], + + '240' => [ + 6, # flv (270p) (v-a) + 5, # flv (v-a) + 36, # 3gp (v-a) + 13, # 3gp (v-a) + [242, # webm (v) + 133, # mp4 (v) + 395, # av1 (v) + ], + 92, # mp4 (live) (v-a) + 132, # ts (live) (v-a) + ], + + '144' => [ + 17, # 3gp (v-a) + [278, # webm (v) + 160, # mp4 (v) + 394, # av1 (v) + ], + 91, # mp4 (live) (v-a) + 151, # ts (live) (v-a) + ], + + 'audio' => [172, # webm (192 kbps) + 251, # webm opus (128-160 kbps) + 171, # webm vorbis (92-128 kbps) + 140, # mp4a (128 kbps) + 141, # mp4a (256 kbps) + 250, # webm opus (64 kbps) + 249, # webm opus (48 kbps) + 139, # mp4a (48 kbps) + ], + }; +#>>> +} + +=head2 get_resolutions() + +Get an ARRAY ref with the supported resolutions ordered from highest to lowest. + +=cut + +sub get_resolutions { + my ($self) = @_; + + state $itags = $self->get_itags(); + return [ + grep { exists $itags->{$_} } + qw( + best + 2160 + 1440 + 1080 + 720 + 480 + 360 + 240 + 144 + audio + ) + ]; +} + +sub _find_streaming_url { + my ($self, %args) = @_; + + my $stream = $args{stream} // return; + my $resolution = $args{resolution} // return; + + foreach my $itag (@{$args{itags}->{$resolution}}) { + + if (ref($itag) eq 'ARRAY') { + + $args{dash} || next; + + foreach my $i (@{$itag}) { + + next if not exists $stream->{$i}; + + my $video_info = $stream->{$i}; + my $audio_info = $self->_find_streaming_url(%args, resolution => 'audio', dash => 0); + + if (defined $audio_info) { + $video_info->{__AUDIO__} = $audio_info; + return $video_info; + } + } + + next; + } + + if (exists $stream->{$itag}) { + if ($resolution eq 'audio' and not $args{dash_mp4_audio}) { + if ($itag == 140 or $itag == 141 or $itag == 139) { + next; # skip mp4 audio URLs + } + } + + my $entry = $stream->{$itag}; + + # Ignore segmented DASH URLs (they load pretty slow in mpv) + if (not $args{dash_segmented}) { + next if ($entry->{url} =~ m{^https://manifest\.googlevideo\.com/api/manifest/dash/}); + } + + return $entry; + } + } + + return; +} + +=head2 find_streaming_url(%options) + +Return the streaming URL which corresponds with the specified resolution. + + ( + urls => \@streaming_urls, + resolution => 'resolution_name', # from $obj->get_resolutions(), + dash => 1/0, # include or exclude DASH itags + dash_mp4_audio => 1/0, # include or exclude DASH videos with MP4 audio + dash_segmented => 1/0, # include or exclude segmented DASH videos + ) + +=cut + +sub find_streaming_url { + my ($self, %args) = @_; + + my $urls_array = $args{urls}; + my $resolution = $args{resolution}; + + state $itags = $self->get_itags(); + + if (defined($resolution) and $resolution =~ /^([0-9]+)/) { + $resolution = $1; + } + + my %stream; + foreach my $info_ref (@{$urls_array}) { + if (exists $info_ref->{itag} and exists $info_ref->{url}) { + $stream{$info_ref->{itag}} = $info_ref; + } + } + + $args{stream} = \%stream; + $args{itags} = $itags; + $args{resolution} = $resolution; + + my ($streaming, $found_resolution); + + # Try to find the wanted resolution + if (defined($resolution) and exists $itags->{$resolution}) { + $streaming = $self->_find_streaming_url(%args); + $found_resolution = $resolution; + } + + # Otherwise, find the best resolution available + if (not defined $streaming) { + + state $resolutions = $self->get_resolutions(); + + foreach my $res (@{$resolutions}) { + + $streaming = $self->_find_streaming_url(%args, resolution => $res); + + if (defined($streaming)) { + $found_resolution = $res; + last; + } + } + } + + wantarray ? ($streaming, $found_resolution) : $streaming; +} + +=head1 AUTHOR + +Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >> + + +=head1 SUPPORT + +You can find documentation for this module with the perldoc command. + + perldoc WWW::StrawViewer::Itags + + +=head1 LICENSE AND COPYRIGHT + +Copyright 2012-2015 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::StrawViewer::Itags diff --git a/lib/WWW/StrawViewer/ParseJSON.pm b/lib/WWW/StrawViewer/ParseJSON.pm new file mode 100644 index 0000000..e2e7d65 --- /dev/null +++ b/lib/WWW/StrawViewer/ParseJSON.pm @@ -0,0 +1,76 @@ +package WWW::StrawViewer::ParseJSON; + +use utf8; +use 5.014; +use warnings; + +=head1 NAME + +WWW::StrawViewer::ParseJSON - Parse JSON content. + +=head1 SYNOPSIS + + use WWW::StrawViewer::ParseJSON; + my $obj = WWW::StrawViewer::ParseJSON->new(%opts); + +=head1 SUBROUTINES/METHODS + +=cut + +=head2 parse_json_string($json_string) + +Parse a JSON string and return a HASH ref. + +=cut + +sub parse_json_string { + my ($self, $json) = @_; + + if (not defined($json) or $json eq '') { + return {}; + } + + require JSON; + my $hash = eval { JSON::decode_json($json) }; + return $@ ? do { warn "[JSON]: $@\n"; {} } : $hash; +} + +=head2 make_json_string($ref) + +Create a JSON string from a HASH or ARRAY ref. + +=cut + +sub make_json_string { + my ($self, $ref) = @_; + + require JSON; + my $str = eval { JSON::encode_json($ref) }; + return $@ ? do { warn "[JSON]: $@\n"; '' } : $str; +} + +=head1 AUTHOR + +Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >> + + +=head1 SUPPORT + +You can find documentation for this module with the perldoc command. + + perldoc WWW::StrawViewer::ParseJSON + + +=head1 LICENSE AND COPYRIGHT + +Copyright 2013-2015 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::StrawViewer::ParseJSON diff --git a/lib/WWW/StrawViewer/ParseXML.pm b/lib/WWW/StrawViewer/ParseXML.pm new file mode 100644 index 0000000..b6039b1 --- /dev/null +++ b/lib/WWW/StrawViewer/ParseXML.pm @@ -0,0 +1,311 @@ +package WWW::StrawViewer::ParseXML; + +use utf8; +use 5.014; +use warnings; + +=encoding utf8 + +=head1 NAME + +WWW::StrawViewer::ParseXML - Convert XML to a HASH ref structure. + +=head1 SYNOPSIS + +Parse XML content and return an HASH ref structure. + +Usage: + + use WWW::StrawViewer::ParseXML; + my $hash_ref = WWW::StrawViewer::ParseXML::xml2hash($xml_string); + +=head1 SUBROUTINES/METHODS + +=head2 xml2hash($xml_string) + +Parse XML and return an HASH ref. + +=cut + +sub xml2hash { + my $xml = shift() // return; + + $xml = "$xml"; # copy the string + + my $xml_ref = {}; + + my %args = ( + attr => '-', + text => '#text', + empty => q{}, + @_ + ); + + my %ctags; + my $ref = $xml_ref; + + state $inv_chars = q{!"#$@%&'()*+,/;\\<=>?\]\[^`{|}~}; + state $valid_tag = qr{[^\-.\s0-9$inv_chars][^$inv_chars\s]*}; + + { + if ( + $xml =~ m{\G< \s* + ($valid_tag) \s* + ((?>$valid_tag\s*=\s*(?>".*?"|'.*?')|\s+)+)? \s* + (/)?\s*> \s* + }gcsxo + ) { + + my ($tag, $attrs, $closed) = ($1, $2, $3); + + if (defined $attrs) { + push @{$ctags{$tag}}, $ref; + + $ref = + ref $ref eq 'HASH' + ? ref $ref->{$tag} + ? $ref->{$tag} + : ( + defined $ref->{$tag} + ? ($ref->{$tag} = [$ref->{$tag}]) + : ($ref->{$tag} //= []) + ) + : ref $ref eq 'ARRAY' ? ref $ref->[-1]{$tag} + ? $ref->[-1]{$tag} + : ( + defined $ref->[-1]{$tag} + ? ($ref->[-1]{$tag} = [$ref->[-1]{$tag}]) + : ($ref->[-1]{$tag} //= []) + ) + : []; + + ++$#{$ref} if ref $ref eq 'ARRAY'; + + while ( + $attrs =~ m{\G + ($valid_tag) \s*=\s* + (?> + "(.*?)" + | + '(.*?)' + ) \s* + }gsxo + ) { + my ($key, $value) = ($1, $+); + $key = join(q{}, $args{attr}, $key); + if (ref $ref eq 'ARRAY') { + $ref->[-1]{$key} = _decode_entities($value); + } + elsif (ref $ref eq 'HASH') { + $ref->{$key} = $value; + } + } + + if (defined $closed) { + $ref = pop @{$ctags{$tag}}; + } + + if ($xml =~ m{\G<\s*/\s*\Q$tag\E\s*>\s*}gc) { + $ref = pop @{$ctags{$tag}}; + } + elsif ($xml =~ m{\G([^<]+)(?=<)}gsc) { + if (ref $ref eq 'ARRAY') { + $ref->[-1]{$args{text}} .= _decode_entities($1); + $ref = pop @{$ctags{$tag}}; + } + elsif (ref $ref eq 'HASH') { + $ref->{$args{text}} .= $1; + $ref = pop @{$ctags{$tag}}; + } + } + } + elsif (defined $closed) { + if (ref $ref eq 'ARRAY') { + if (exists $ref->[-1]{$tag}) { + if (ref $ref->[-1]{$tag} ne 'ARRAY') { + $ref->[-1]{$tag} = [$ref->[-1]{$tag}]; + } + push @{$ref->[-1]{$tag}}, $args{empty}; + } + else { + $ref->[-1]{$tag} = $args{empty}; + } + } + } + else { + if ($xml =~ /\G(?=<(?!!))/) { + push @{$ctags{$tag}}, $ref; + + $ref = + ref $ref eq 'HASH' + ? ref $ref->{$tag} + ? $ref->{$tag} + : ( + defined $ref->{$tag} + ? ($ref->{$tag} = [$ref->{$tag}]) + : ($ref->{$tag} //= []) + ) + : ref $ref eq 'ARRAY' ? ref $ref->[-1]{$tag} + ? $ref->[-1]{$tag} + : ( + defined $ref->[-1]{$tag} + ? ($ref->[-1]{$tag} = [$ref->[-1]{$tag}]) + : ($ref->[-1]{$tag} //= []) + ) + : []; + + ++$#{$ref} if ref $ref eq 'ARRAY'; + redo; + } + elsif ($xml =~ /\G<!\[CDATA\[(.*?)\]\]>\s*/gcs or $xml =~ /\G([^<]+)(?=<)/gsc) { + my ($text) = $1; + + if ($xml =~ m{\G<\s*/\s*\Q$tag\E\s*>\s*}gc) { + if (ref $ref eq 'ARRAY') { + if (exists $ref->[-1]{$tag}) { + if (ref $ref->[-1]{$tag} ne 'ARRAY') { + $ref->[-1]{$tag} = [$ref->[-1]{$tag}]; + } + push @{$ref->[-1]{$tag}}, $text; + } + else { + $ref->[-1]{$tag} .= _decode_entities($text); + } + } + elsif (ref $ref eq 'HASH') { + $ref->{$tag} .= $text; + } + } + else { + push @{$ctags{$tag}}, $ref; + + $ref = + ref $ref eq 'HASH' + ? ref $ref->{$tag} + ? $ref->{$tag} + : ( + defined $ref->{$tag} + ? ($ref->{$tag} = [$ref->{$tag}]) + : ($ref->{$tag} //= []) + ) + : ref $ref eq 'ARRAY' ? ref $ref->[-1]{$tag} + ? $ref->[-1]{$tag} + : ( + defined $ref->[-1]{$tag} + ? ($ref->[-1]{$tag} = [$ref->[-1]{$tag}]) + : ($ref->[-1]{$tag} //= []) + ) + : []; + + ++$#{$ref} if ref $ref eq 'ARRAY'; + + if (ref $ref eq 'ARRAY') { + if (exists $ref->[-1]{$tag}) { + if (ref $ref->[-1]{$tag} ne 'ARRAY') { + $ref->[-1] = [$ref->[-1]{$tag}]; + } + push @{$ref->[-1]}, {$args{text} => $text}; + } + else { + $ref->[-1]{$args{text}} .= $text; + } + } + elsif (ref $ref eq 'HASH') { + $ref->{$tag} .= $text; + } + } + } + } + + if ($xml =~ m{\G<\s*/\s*\Q$tag\E\s*>\s*}gc) { + ## tag closed - ok + } + + redo; + } + elsif ($xml =~ m{\G<\s*/\s*($valid_tag)\s*>\s*}gco) { + if (exists $ctags{$1} and @{$ctags{$1}}) { + $ref = pop @{$ctags{$1}}; + } + redo; + } + elsif ($xml =~ /\G<!\[CDATA\[(.*?)\]\]>\s*/gcs or $xml =~ m{\G([^<]+)(?=<)}gsc) { + if (ref $ref eq 'ARRAY') { + $ref->[-1]{$args{text}} .= $1; + } + elsif (ref $ref eq 'HASH') { + $ref->{$args{text}} .= $1; + } + redo; + } + elsif ($xml =~ /\G<\?/gc) { + $xml =~ /\G.*?\?>\s*/gcs or die "Invalid XML!"; + redo; + } + elsif ($xml =~ /\G<!--/gc) { + $xml =~ /\G.*?-->\s*/gcs or die "Comment not closed!"; + redo; + } + elsif ($xml =~ /\G<!DOCTYPE\s+/gc) { + $xml =~ /\G(?>$valid_tag|\s+|".*?"|'.*?')*\[.*?\]>\s*/sgco + or $xml =~ /\G.*?>\s*/sgc + or die "DOCTYPE not closed!"; + redo; + } + elsif ($xml =~ /\G\z/gc) { + ## ok + } + elsif ($xml =~ /\G\s+/gc) { + redo; + } + else { + die "Syntax error near: --> ", [split(/\n/, substr($xml, pos(), 2**6))]->[0], " <--\n"; + } + } + + return $xml_ref; +} + +{ + my %entities = ( + 'amp' => '&', + 'quot' => '"', + 'apos' => "'", + 'gt' => '>', + 'lt' => '<', + ); + + state $ent_re = do { + local $" = '|'; + qr/&(@{[keys %entities]});/; + }; + + sub _decode_entities { + $_[0] =~ s/$ent_re/$entities{$1}/gor; + } +} + +=head1 AUTHOR + +Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >> + +=head1 SUPPORT + +You can find documentation for this module with the perldoc command. + + perldoc WWW::StrawViewer::ParseXML + + +=head1 LICENSE AND COPYRIGHT + +Copyright 2012-2015 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::StrawViewer::ParseXML diff --git a/lib/WWW/StrawViewer/PlaylistItems.pm b/lib/WWW/StrawViewer/PlaylistItems.pm new file mode 100644 index 0000000..046e065 --- /dev/null +++ b/lib/WWW/StrawViewer/PlaylistItems.pm @@ -0,0 +1,167 @@ +package WWW::StrawViewer::PlaylistItems; + +use utf8; +use 5.014; +use warnings; + +=head1 NAME + +WWW::StrawViewer::PlaylistItems - Manage playlist entries. + +=head1 SYNOPSIS + + use WWW::StrawViewer; + my $obj = WWW::StrawViewer->new(%opts); + my $videos = $obj->videos_from_playlistID($playlist_id); + +=head1 SUBROUTINES/METHODS + +=cut + +sub _make_playlistItems_url { + my ($self, %opts) = @_; + return + $self->_make_feed_url( + 'playlistItems', + pageToken => $self->page_token, + %opts + ); +} + +=head2 add_video_to_playlist($playlistID, $videoID; $position=1) + +Add a video to given playlist ID, at position 1 (by default) + +=cut + +sub add_video_to_playlist { + my ($self, $playlist_id, $video_id, $position) = @_; + + $self->get_access_token() // return; + + $playlist_id // return; + $video_id // return; + $position //= 0; + + my $hash = { + "snippet" => { + "playlistId" => $playlist_id, + "resourceId" => { + "videoId" => $video_id, + "kind" => "youtube#video" + }, + "position" => $position, + } + }; + + my $url = $self->_make_playlistItems_url(pageToken => undef); + $self->post_as_json($url, $hash); +} + +=head2 favorite_video($videoID) + +Favorite a video. Returns true on success. + +=cut + +sub favorite_video { + my ($self, $video_id) = @_; + $video_id // return; + $self->get_access_token() // return; + my $playlist_id = $self->get_playlist_id('favorites', mine => 'true') // return; + $self->add_video_to_playlist($playlist_id, $video_id); +} + +=head2 videos_from_playlist_id($playlist_id) + +Get videos from a specific playlistID. + +=cut + +sub videos_from_playlist_id { + my ($self, $id) = @_; + return $self->_get_results($self->_make_playlistItems_url(playlistId => $id, part => 'contentDetails,snippet')); +} + +=head2 videos_from_id($playlist_id) + +Get videos from a specific playlistID. + +=cut + +sub playlists_from_id { + my ($self, $id) = @_; + return $self->_get_results($self->_make_playlistItems_url(id => $id)); +} + +=head2 favorites($channel_id) + +=head2 uploads($channel_id) + +=head2 likes($channel_id) + +Get the favorites, uploads and likes for a given channel ID. + +=cut + +=head2 favorites_from_username($username) + +=head2 uploads_from_username($username) + +=head2 likes_from_username($username) + +Get the favorites, uploads and likes for a given YouTube username. + +=cut + +{ + no strict 'refs'; + foreach my $name (qw(favorites uploads likes)) { + + *{__PACKAGE__ . '::' . $name . '_from_username'} = sub { + my ($self, $username) = @_; + my $playlist_id = $self->get_playlist_id( + $name, $username + ? (forUsername => $username) + : do { $self->get_access_token() // return; (mine => 'true') } + ) // return; + $self->videos_from_playlist_id($playlist_id); + }; + + *{__PACKAGE__ . '::' . $name} = sub { + my ($self, $channel_id) = @_; + my $playlist_id = $self->get_playlist_id( + $name, ($channel_id and $channel_id ne 'mine') + ? (id => $channel_id) + : do { $self->get_access_token() // return; (mine => 'true') } + ) // return; + $self->videos_from_playlist_id($playlist_id); + }; + } +} + +=head1 AUTHOR + +Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >> + + +=head1 SUPPORT + +You can find documentation for this module with the perldoc command. + + perldoc WWW::StrawViewer::PlaylistItems + + +=head1 LICENSE AND COPYRIGHT + +Copyright 2013-2015 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::StrawViewer::PlaylistItems diff --git a/lib/WWW/StrawViewer/Playlists.pm b/lib/WWW/StrawViewer/Playlists.pm new file mode 100644 index 0000000..5d4e07d --- /dev/null +++ b/lib/WWW/StrawViewer/Playlists.pm @@ -0,0 +1,124 @@ +package WWW::StrawViewer::Playlists; + +use utf8; +use 5.014; +use warnings; + +=head1 NAME + +WWW::StrawViewer::Playlists - Straw playlists handle. + +=head1 SYNOPSIS + + use WWW::StrawViewer; + my $obj = WWW::StrawViewer->new(%opts); + my $info = $obj->playlist_from_id($playlist_id); + +=head1 SUBROUTINES/METHODS + +=cut + +sub _make_playlists_url { + my ($self, %opts) = @_; + + if (not exists $opts{'part'}) { + $opts{'part'} = 'snippet,contentDetails'; + } + + $self->_make_feed_url( + 'playlists', + pageToken => $self->page_token, + %opts, + ); +} + +sub get_playlist_id { + my ($self, $playlist_name, %fields) = @_; + + my $url = $self->_simple_feeds_url('channels', qw(part contentDetails), %fields); + my $res = $self->_get_results($url); + + ref($res->{results}{items}) eq 'ARRAY' || return; + @{$res->{results}{items}} || return; + + return $res->{results}{items}[0]{contentDetails}{relatedPlaylists}{$playlist_name}; +} + +=head2 playlist_from_id($playlist_id) + +Return info for one or more playlists. +PlaylistIDs can be separated by commas. + +=cut + +sub playlist_from_id { + my ($self, $id, $part) = @_; + $self->_get_results($self->_make_playlists_url(id => $id, part => ($part // 'snippet'))); +} + +=head2 playlists($channel_id) + +Get and return playlists from a channel ID. + +=cut + +sub playlists { + my ($self, $channel_id) = @_; + $self->_get_results( + $self->_make_playlists_url( + ($channel_id and $channel_id ne 'mine') + ? (channelId => $channel_id) + : do { $self->get_access_token() // return; (mine => 'true') } + ) + ); +} + +=head2 playlists_from_username($username) + +Get and return the playlists created for a given username. + +=cut + +sub playlists_from_username { + my ($self, $username) = @_; + my $channel_id = $self->channel_id_from_username($username) // $username; + $self->playlists($channel_id); +} + +=head2 my_playlists() + +Get and return your playlists. + +=cut + +sub my_playlists { + my ($self) = @_; + $self->get_access_token() // return; + $self->_get_results($self->_make_playlists_url(mine => 'true')); +} + +=head1 AUTHOR + +Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >> + + +=head1 SUPPORT + +You can find documentation for this module with the perldoc command. + + perldoc WWW::StrawViewer::Playlists + + +=head1 LICENSE AND COPYRIGHT + +Copyright 2013-2015 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::StrawViewer::Playlists diff --git a/lib/WWW/StrawViewer/RegularExpressions.pm b/lib/WWW/StrawViewer/RegularExpressions.pm new file mode 100644 index 0000000..edf9dd5 --- /dev/null +++ b/lib/WWW/StrawViewer/RegularExpressions.pm @@ -0,0 +1,89 @@ +package WWW::StrawViewer::RegularExpressions; + +use utf8; +use 5.014; +use warnings; + +require Exporter; +our @ISA = qw(Exporter); + +=head1 NAME + +WWW::StrawViewer::RegularExpressions - Various utils. + +=head1 SYNOPSIS + + use WWW::StrawViewer::RegularExpressions; + use WWW::StrawViewer::RegularExpressions ($get_video_id_re); + +=cut + +my $opt_begin_chars = q{:;=}; # stdin option valid begin chars + +# Options +our $range_num_re = qr{^([0-9]{1,2}+)(?>-|\.\.)([0-9]{1,2}+)?\z}; +our $digit_or_equal_re = qr/(?(?=[1-9])|=)/; +our $non_digit_or_opt_re = qr{^(?!$range_num_re)(?>[0-9]{1,2}[^0-9]|[0-9]{3}|[^0-9$opt_begin_chars])}; + +# Generic name +my $generic_name_re = qr/[a-zA-Z0-9_.\-]{11,34}/; +our $valid_channel_id_re = qr{^(?:.*/channel/)?(?<channel_id>(?:\w+(?:[-.]++\w++)*|$generic_name_re))(?:/.*)?\z}; + +our $get_channel_videos_id_re = qr{^.*/channel/(?<channel_id>(?:\w+(?:[-.]++\w++)*|$generic_name_re))}; +our $get_channel_playlists_id_re = qr{$get_channel_videos_id_re/playlists}; + +our $get_username_videos_re = qr{^.*/user/(?<username>[-.\w]+)}; +our $get_username_playlists_re = qr{$get_username_videos_re/playlists}; + +# Video ID +my $video_id_re = qr/[0-9A-Za-z_\-]{11}/; +our $valid_video_id_re = qr{^$video_id_re\z}; +our $get_video_id_re = qr{(?:%3F|\b)(?>v|embed|youtu[.]be)(?>[=/]|%3D)(?<video_id>$video_id_re)}; + +# Playlist ID +our $valid_playlist_id_re = qr{^$generic_name_re\z}; +our $get_playlist_id_re = qr{(?:(?:(?>playlist\?list|view_play_list\?p|list)=)|\w#p/c/)(?<playlist_id>$generic_name_re)\b}; + +our $valid_opt_re = qr{^[$opt_begin_chars]([A-Za-z]++(?:-[A-Za-z]++)?(?>${digit_or_equal_re}.*)?)$}; + +our @EXPORT = qw( + $range_num_re + $digit_or_equal_re + $non_digit_or_opt_re + $valid_channel_id_re + $valid_video_id_re + $get_video_id_re + $valid_playlist_id_re + $get_playlist_id_re + $valid_opt_re + $get_channel_videos_id_re + $get_channel_playlists_id_re + $get_username_videos_re + $get_username_playlists_re + ); + +=head1 AUTHOR + +Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >> + + +=head1 SUPPORT + +You can find documentation for this module with the perldoc command. + + perldoc WWW::StrawViewer::RegularExpressions + + +=head1 LICENSE AND COPYRIGHT + +Copyright 2012-2013 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::StrawViewer::RegularExpressions diff --git a/lib/WWW/StrawViewer/Search.pm b/lib/WWW/StrawViewer/Search.pm new file mode 100644 index 0000000..67b8982 --- /dev/null +++ b/lib/WWW/StrawViewer/Search.pm @@ -0,0 +1,175 @@ +package WWW::StrawViewer::Search; + +use utf8; +use 5.014; +use warnings; + +=head1 NAME + +WWW::StrawViewer::Search - Search functions for Straw API v3 + +=head1 SYNOPSIS + + use WWW::StrawViewer; + my $obj = WWW::StrawViewer->new(%opts); + $obj->search_videos(@keywords); + +=head1 SUBROUTINES/METHODS + +=cut + +sub _make_search_url { + my ($self, %opts) = @_; + + return $self->_make_feed_url( + 'search', + + topicId => $self->get_topicId, + regionCode => $self->get_regionCode, + + maxResults => $self->get_maxResults, + order => $self->get_order, + publishedAfter => $self->get_publishedAfter, + publishedBefore => $self->get_publishedBefore, + regionCode => $self->get_regionCode, + relevanceLanguage => $self->get_relevanceLanguage, + safeSearch => $self->get_safeSearch, + channelId => $self->get_channelId, + channelType => $self->get_channelType, + pageToken => $self->page_token, + + ( + $opts{type} eq 'video' + ? ( + videoCaption => $self->get_videoCaption, + videoCategoryId => $self->get_videoCategoryId, + videoDefinition => $self->get_videoDefinition, + videoDimension => $self->get_videoDimension, + videoDuration => $self->get_videoDuration, + videoEmbeddable => $self->get_videoEmbeddable, + videoLicense => $self->get_videoLicense, + videoSyndicated => $self->get_videoSyndicated, + videoType => $self->get_videoType, + eventType => $self->get_eventType, + ) + : () + ), + + %opts, + ); + +} + +=head2 search_for($types,$keywords;\%args) + +Search for a list of types (comma-separated). + +=cut + +sub search_for { + my ($self, $type, $keywords, $args) = @_; + + $keywords //= []; + if (ref $keywords ne 'ARRAY') { + $keywords = [split ' ', $keywords]; + } + + my $url = $self->_make_search_url( + type => $type, + q => $self->escape_string(join(' ', @{$keywords})), + (ref $args eq 'HASH' ? %{$args} : (part => 'snippet')), + ); + + return $self->_get_results($url); +} + +{ + no strict 'refs'; + + foreach my $pair ( + { + name => 'videos', + type => 'video', + }, + { + name => 'playlists', + type => 'playlist', + }, + { + name => 'channels', + type => 'channel', + }, + { + name => 'all', + type => 'video,channel,playlist', + } + ) { + *{__PACKAGE__ . '::' . "search_$pair->{name}"} = sub { + my $self = shift; + $self->search_for($pair->{type}, @_); + }; + } +} + +=head2 search_videos($keywords;\%args) + +Search and return the found video results. + +=cut + +=head2 search_playlists($keywords;\%args) + +Search and return the found playlists. + +=cut + +=head2 search_channels($keywords;\%args) + +Search and return the found channels. + +=cut + +=head2 search_all($keywords;\%args) + +Search and return the results. + +=cut + +=head2 related_to_videoID($id) + +Retrieves a list of videos that are related to the video +that the parameter value identifies. The parameter value must +be set to a YouTube video ID. + +=cut + +sub related_to_videoID { + my ($self, $id) = @_; + return $self->search_for('video', [], {relatedToVideoId => $id}); +} + +=head1 AUTHOR + +Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >> + + +=head1 SUPPORT + +You can find documentation for this module with the perldoc command. + + perldoc WWW::StrawViewer::Search + + +=head1 LICENSE AND COPYRIGHT + +Copyright 2013-2015 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::StrawViewer::Search diff --git a/lib/WWW/StrawViewer/Subscriptions.pm b/lib/WWW/StrawViewer/Subscriptions.pm new file mode 100644 index 0000000..063a875 --- /dev/null +++ b/lib/WWW/StrawViewer/Subscriptions.pm @@ -0,0 +1,272 @@ +package WWW::StrawViewer::Subscriptions; + +use utf8; +use 5.014; +use warnings; + +=head1 NAME + +WWW::StrawViewer::Subscriptions - Subscriptions handler. + +=head1 SYNOPSIS + + use WWW::StrawViewer; + my $obj = WWW::StrawViewer->new(%opts); + my $videos = $obj->subscriptions_from_channelID($channel_id); + +=head1 SUBROUTINES/METHODS + +=cut + +sub _make_subscriptions_url { + my ($self, %opts) = @_; + return $self->_make_feed_url('subscriptions', %opts); +} + +=head2 subscribe_channel($channel_id) + +Subscribe to an YouTube channel. + +=cut + +sub subscribe_channel { + my ($self, $channel_id) = @_; + + my $resource = { + snippet => { + resourceId => { + kind => 'youtube#channel', + channelId => $channel_id, + } + } + }; + + my $url = $self->_simple_feeds_url('subscriptions', part => 'snippet'); + return $self->post_as_json($url, $resource); +} + +=head2 subscribe_channel_from_username($username) + +Subscribe to an YouTube channel via username. + +=cut + +sub subscribe_channel_from_username { + my ($self, $username) = @_; + $self->subscribe_channel($self->channel_id_from_username($username) // $username); +} + +=head2 subscriptions(;$channel_id) + +Retrieve the subscriptions for a channel ID or for the authenticated user. + +=cut + +sub subscriptions { + my ($self, $channel_id) = @_; + $self->_get_results( + $self->_make_subscriptions_url( + order => $self->get_subscriptions_order, + part => 'snippet', + ( + ($channel_id and $channel_id ne 'mine') + ? (channelId => $channel_id) + : do { $self->get_access_token() // return; (mine => 'true') } + ), + ) + ); +} + +=head2 subscriptions_from_username($username) + +Retrieve subscriptions for a given YouTube username. + +=cut + +sub subscriptions_from_username { + my ($self, $username) = @_; + $self->subscriptions($self->channel_id_from_username($username) // $username); +} + +=head2 subscription_videos(;$channel_id) + +Retrieve the video subscriptions for a channel ID or for the current authenticated user. + +=cut + +sub subscription_videos { + my ($self, $channel_id, $order) = @_; + + my $max_results = $self->get_maxResults(); + + my @subscription_items; + my $next_page_token; + + while (1) { + + my $url = $self->_make_subscriptions_url( + order => $self->get_subscriptions_order, + maxResults => 50, + part => 'snippet,contentDetails', + ($channel_id and $channel_id ne 'mine') + ? (channelId => $channel_id) + : do { $self->get_access_token() // return; (mine => 'true') }, + defined($next_page_token) ? (pageToken => $next_page_token) : (), + ); + + my $subscriptions = $self->_get_results($url)->{results}; + + if ( ref($subscriptions) eq 'HASH' + and ref($subscriptions->{items}) eq 'ARRAY') { + push @subscription_items, @{$subscriptions->{items}}; + } + + $next_page_token = $subscriptions->{nextPageToken} || last; + } + + my (undef, undef, undef, $mday, $mon, $year) = localtime; + + $mon += 1; + $year += 1900; + + my @videos; + foreach my $channel (@subscription_items) { + + my $new_items = $channel->{contentDetails}{newItemCount}; + + # Ignore channels with zero new items + $new_items > 0 || next; + + # Set the number of results + $self->set_maxResults(1); # don't load more than 1 video from each channel + # maybe, this value should be configurable (?) + + my $uploads = $self->uploads($channel->{snippet}{resourceId}{channelId}); + + (ref($uploads) eq 'HASH' and ref($uploads->{results}) eq 'HASH' and ref($uploads->{results}{items}) eq 'ARRAY') + || return; + + my $items = $uploads->{results}{items}; + + # Get and store the video uploads from each channel + foreach my $item (@$items) { + my $publishedAt = $item->{snippet}{publishedAt}; + my ($p_year, $p_mon, $p_mday) = $publishedAt =~ /^(\d{4})-(\d{2})-(\d{2})/; + + my $year_diff = $year - $p_year; + my $mon_diff = $mon - $p_mon; + my $mday_diff = $mday - $p_mday; + + my $days_diff = $year_diff * 365.2422 + $mon_diff * 30.436875 + $mday_diff; + + # Ignore old entries + if ($days_diff > 3) { + next; + } + + push @videos, $item; + } + + # Stop when the limit is reached + last if (@videos >= $max_results); + } + + # When there are no new videos, load one from each channel + if ($#videos == -1) { + foreach my $channel (@subscription_items) { + $self->set_maxResults(1); + push @videos, @{$self->uploads($channel->{snippet}{resourceId}{channelId})->{results}{items}}; + last if (@videos >= $max_results); + } + } + + $self->set_maxResults($max_results); + + state $parse_time_re = qr/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/; + + @videos = + sort { + my ($y1, $M1, $d1, $h1, $m1, $s1) = $a->{snippet}{publishedAt} =~ $parse_time_re; + my ($y2, $M2, $d2, $h2, $m2, $s2) = $b->{snippet}{publishedAt} =~ $parse_time_re; + + ($y2 <=> $y1) || ($M2 <=> $M1) || ($d2 <=> $d1) || ($h2 <=> $h1) || ($m2 <=> $m1) || ($s2 <=> $s1) + } @videos; + + return {results => {pageInfo => {totalResults => $#videos + 1}, items => \@videos}}; +} + +=head2 subscription_videos_from_username($username) + +Retrieve the video subscriptions for a username. + +=cut + +sub subscription_videos_from_username { + my ($self, $username) = @_; + $self->subscription_videos($self->channel_id_from_username($username) // $username); +} + +=head2 subscriptions_from_channelID(%args) + +Get subscriptions for the specified channel ID. + +=head2 subscriptions_info($subscriptionID, %args) + +Get details for the comma-separated subscriptionID(s). + +=head3 HASH '%args' supports the following pairs: + + %args = ( + part => {contentDetails,id,snippet}, + forChannelId => $channelID, + maxResults => [0-50], + order => {alphabetical, relevance, unread}, + pageToken => {$nextPageToken, $prevPageToken}, + ); + +=cut + +{ + no strict 'refs'; + foreach my $method ( + { + key => 'id', + name => 'subscriptions_info', + }, + { + key => 'channelId', + name => 'subscriptions_from_channel_id', + } + ) { + *{__PACKAGE__ . '::' . $method->{name}} = sub { + my ($self, $id, %args) = @_; + return $self->_get_results($self->_make_subscriptions_url($method->{key} => $id, %args)); + }; + } +} + +=head1 AUTHOR + +Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >> + + +=head1 SUPPORT + +You can find documentation for this module with the perldoc command. + + perldoc WWW::StrawViewer::Subscriptions + + +=head1 LICENSE AND COPYRIGHT + +Copyright 2013-2015 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::StrawViewer::Subscriptions diff --git a/lib/WWW/StrawViewer/Utils.pm b/lib/WWW/StrawViewer/Utils.pm new file mode 100644 index 0000000..f3855b8 --- /dev/null +++ b/lib/WWW/StrawViewer/Utils.pm @@ -0,0 +1,735 @@ +package WWW::StrawViewer::Utils; + +use utf8; +use 5.014; +use warnings; + +=head1 NAME + +WWW::StrawViewer::Utils - Various utils. + +=head1 SYNOPSIS + + use WWW::StrawViewer::Utils; + + my $yv_utils = WWW::StrawViewer::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) = @_; + scalar(@{$result->{results}}) > 0; + #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}; +} + +=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) = @_; + 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_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"; +} + +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'; +} + +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+) 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->{statistics}{likeCount}; +} + +sub get_dislikes { + my ($self, $info) = @_; + $info->{statistics}{dislikeCount}; +} + +sub get_comments { + my ($self, $info) = @_; + $info->{statistics}{commentCount}; +} + +{ + 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) = @_; + + 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::StrawViewer::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::StrawViewer::Utils diff --git a/lib/WWW/StrawViewer/VideoCategories.pm b/lib/WWW/StrawViewer/VideoCategories.pm new file mode 100644 index 0000000..30e2e6e --- /dev/null +++ b/lib/WWW/StrawViewer/VideoCategories.pm @@ -0,0 +1,97 @@ +package WWW::StrawViewer::VideoCategories; + +use utf8; +use 5.014; +use warnings; + +=head1 NAME + +WWW::StrawViewer::VideoCategories - videoCategory resource handler. + +=head1 SYNOPSIS + + use WWW::StrawViewer; + my $obj = WWW::StrawViewer->new(%opts); + my $cats = $obj->video_categories(); + +=head1 SUBROUTINES/METHODS + +=cut + +sub _make_videoCategories_url { + my ($self, %opts) = @_; + + $self->_make_feed_url( + 'videoCategories', + hl => $self->get_hl, + %opts, + ); +} + +=head2 video_categories() + +Return video categories for a specific region ID. + +=cut + +sub video_categories { + my ($self) = @_; + + require File::Spec; + + my $region = $self->get_regionCode() // 'US'; + my $url = $self->_make_videoCategories_url(regionCode => $region); + my $file = File::Spec->catfile($self->get_config_dir, "categories-$region-" . $self->get_hl() . ".json"); + + my $json; + if (open(my $fh, '<:utf8', $file)) { + local $/; + $json = <$fh>; + close $fh; + } + else { + $json = $self->lwp_get($url, simple => 1); + open my $fh, '>:utf8', $file; + print {$fh} $json; + close $fh; + } + + return $self->parse_json_string($json); +} + +=head2 video_category_id_info($cagegory_id) + +Return info for the comma-separated specified category ID(s). + +=cut + +sub video_category_id_info { + my ($self, $id) = @_; + return $self->_get_results($self->_make_videoCategories_url(id => $id)); +} + +=head1 AUTHOR + +Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >> + + +=head1 SUPPORT + +You can find documentation for this module with the perldoc command. + + perldoc WWW::StrawViewer::VideoCategories + + +=head1 LICENSE AND COPYRIGHT + +Copyright 2013-2015 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::StrawViewer::VideoCategories diff --git a/lib/WWW/StrawViewer/Videos.pm b/lib/WWW/StrawViewer/Videos.pm new file mode 100644 index 0000000..9df9ff3 --- /dev/null +++ b/lib/WWW/StrawViewer/Videos.pm @@ -0,0 +1,229 @@ +package WWW::StrawViewer::Videos; + +use utf8; +use 5.014; +use warnings; + +=head1 NAME + +WWW::StrawViewer::Videos - videos handler. + +=head1 SYNOPSIS + + use WWW::StrawViewer; + my $obj = WWW::StrawViewer->new(%opts); + my $info = $obj->video_details($videoID); + +=head1 SUBROUTINES/METHODS + +=cut + +sub _make_videos_url { + my ($self, %opts) = @_; + return $self->_make_feed_url('videos', %opts); +} + +{ + no strict 'refs'; + foreach my $part ( + qw( + id + snippet + contentDetails + fileDetails + player + liveStreamingDetails + processingDetails + recordingDetails + statistics + status + suggestions + topicDetails + ) + ) { + *{__PACKAGE__ . '::' . 'video_' . $part} = sub { + my ($self, $id) = @_; + return $self->_get_results($self->_make_videos_url(id => $id, part => $part)); + }; + } +} + +=head2 videos_from_category($category_id) + +Get videos from a category ID. + +=cut + +sub videos_from_category { + my ($self, $cat_id) = @_; + $self->_get_results( + $self->_make_videos_url( + chart => $self->get_chart, + videoCategoryId => $cat_id, + ) + ); +} + +=head2 trending_videos_from_category($category_id) + +Get popular videos from a category ID. + +=cut + +sub trending_videos_from_category { + my ($self, $cat_id) = @_; + + my $results = do { + local $self->{publishedAfter} = do { + state $yv_utils = WWW::StrawViewer::Utils->new; + $yv_utils->period_to_date(1, 'w'); + } if !defined($self->get_publishedAfter); + local $self->{videoCategoryId} = $cat_id; + local $self->{regionCode} = "US" if !defined($self->get_regionCode); + $self->search_videos(""); + }; + + return $results; +} + +=head2 popular_videos($channel_id) + +Get the most popular videos for a given channel ID. + +=cut + +sub popular_videos { + my ($self, $id) = @_; + + my $results = do { + local $self->{channelId} = $id; + local $self->{order} = 'viewCount'; + $self->search_videos(""); + }; + + return $results; +} + +=head2 my_likes() + +Get the videos liked by the authenticated user. + +=cut + +sub my_likes { + my ($self) = @_; + $self->get_access_token() // return; + $self->_get_results($self->_make_videos_url(myRating => 'like', pageToken => $self->page_token)); +} + +=head2 my_dislikes() + +Get the videos disliked by the authenticated user. + +=cut + +sub my_dislikes { + my ($self) = @_; + $self->get_access_token() // return; + $self->_get_results($self->_make_videos_url(myRating => 'dislike', pageToken => $self->page_token)); +} + +=head2 send_rating_to_video($videoID, $rating) + +Send rating to a video. $rating can be either 'like' or 'dislike'. + +=cut + +sub send_rating_to_video { + my ($self, $video_id, $rating) = @_; + + if ($rating eq 'none' or $rating eq 'like' or $rating eq 'dislike') { + my $url = $self->_simple_feeds_url('videos/rate', id => $video_id, rating => $rating); + return defined($self->lwp_post($url, $self->_auth_lwp_header())); + } + + return; +} + +=head2 like_video($videoID) + +Like a video. Returns true on success. + +=cut + +sub like_video { + my ($self, $video_id) = @_; + $self->send_rating_to_video($video_id, 'like'); +} + +=head2 dislike_video($videoID) + +Dislike a video. Returns true on success. + +=cut + +sub dislike_video { + my ($self, $video_id) = @_; + $self->send_rating_to_video($video_id, 'dislike'); +} + +=head2 videos_details($id, $part) + +Get info about a videoID, such as: channelId, title, description, +tags, and categoryId. + +Available values for I<part> are: I<id>, I<snippet>, I<contentDetails> +I<player>, I<statistics>, I<status> and I<topicDetails>. + +C<$part> string can contain more values, comma-separated. + +Example: + + part => 'snippet,contentDetails,statistics' + +When C<$part> is C<undef>, it defaults to I<snippet>. + +=cut + +sub video_details { + my ($self, $id, $part) = @_; + return $self->_get_results($self->_make_videos_url(id => $id, part => $part // 'snippet')); +} + +=head2 Return details + +Each function returns a HASH ref, with a key called 'results', and another key, called 'url'. + +The 'url' key contains a string, which is the URL for the retrieved content. + +The 'results' key contains another HASH ref with the keys 'etag', 'items' and 'kind'. +From the 'results' key, only the 'items' are relevant to us. This key contains an ARRAY ref, +with a HASH ref for each result. An example of the item array's content are shown below. + +=cut + +=head1 AUTHOR + +Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >> + + +=head1 SUPPORT + +You can find documentation for this module with the perldoc command. + + perldoc WWW::StrawViewer::Videos + + +=head1 LICENSE AND COPYRIGHT + +Copyright 2013-2015 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::StrawViewer::Videos |