aboutsummaryrefslogtreecommitdiffstats
path: root/lib/WWW/FairViewer
diff options
context:
space:
mode:
Diffstat (limited to 'lib/WWW/FairViewer')
-rw-r--r--lib/WWW/FairViewer/Activities.pm93
-rw-r--r--lib/WWW/FairViewer/Authentication.pm216
-rw-r--r--lib/WWW/FairViewer/Channels.pm200
-rw-r--r--lib/WWW/FairViewer/CommentThreads.pm98
-rw-r--r--lib/WWW/FairViewer/GetCaption.pm280
-rw-r--r--lib/WWW/FairViewer/GuideCategories.pm85
-rw-r--r--lib/WWW/FairViewer/Itags.pm319
-rw-r--r--lib/WWW/FairViewer/ParseJSON.pm76
-rw-r--r--lib/WWW/FairViewer/ParseXML.pm311
-rw-r--r--lib/WWW/FairViewer/PlaylistItems.pm146
-rw-r--r--lib/WWW/FairViewer/Playlists.pm124
-rw-r--r--lib/WWW/FairViewer/RegularExpressions.pm89
-rw-r--r--lib/WWW/FairViewer/Search.pm175
-rw-r--r--lib/WWW/FairViewer/Subscriptions.pm272
-rw-r--r--lib/WWW/FairViewer/Utils.pm801
-rw-r--r--lib/WWW/FairViewer/VideoCategories.pm97
-rw-r--r--lib/WWW/FairViewer/Videos.pm230
17 files changed, 3612 insertions, 0 deletions
diff --git a/lib/WWW/FairViewer/Activities.pm b/lib/WWW/FairViewer/Activities.pm
new file mode 100644
index 0000000..7c80df0
--- /dev/null
+++ b/lib/WWW/FairViewer/Activities.pm
@@ -0,0 +1,93 @@
+package WWW::FairViewer::Activities;
+
+use utf8;
+use 5.014;
+use warnings;
+
+=head1 NAME
+
+WWW::FairViewer::Activities - list of channel activity events that match the request criteria.
+
+=head1 SYNOPSIS
+
+ use WWW::FairViewer;
+ my $obj = WWW::FairViewer->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::FairViewer::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::FairViewer::Activities
diff --git a/lib/WWW/FairViewer/Authentication.pm b/lib/WWW/FairViewer/Authentication.pm
new file mode 100644
index 0000000..4994f27
--- /dev/null
+++ b/lib/WWW/FairViewer/Authentication.pm
@@ -0,0 +1,216 @@
+package WWW::FairViewer::Authentication;
+
+use utf8;
+use 5.014;
+use warnings;
+
+=head1 NAME
+
+WWW::FairViewer::Authentication - OAuth login support.
+
+=head1 SYNOPSIS
+
+ use WWW::FairViewer;
+ my $hash_ref = WWW::FairViewer->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::FairViewer::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::FairViewer::Authentication
diff --git a/lib/WWW/FairViewer/Channels.pm b/lib/WWW/FairViewer/Channels.pm
new file mode 100644
index 0000000..d6c8d49
--- /dev/null
+++ b/lib/WWW/FairViewer/Channels.pm
@@ -0,0 +1,200 @@
+package WWW::FairViewer::Channels;
+
+use utf8;
+use 5.014;
+use warnings;
+
+=head1 NAME
+
+WWW::FairViewer::Channels - Channels interface.
+
+=head1 SYNOPSIS
+
+ use WWW::FairViewer;
+ my $obj = WWW::FairViewer->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);
+}
+
+sub videos_from_channel_id {
+ my ($self, $channel_id) = @_;
+ return $self->_get_results($self->_make_feed_url("channels/$channel_id/videos"));
+}
+
+sub videos_from_username {
+ my ($self, $channel_id) = @_;
+ return $self->_get_results($self->_make_feed_url("channels/$channel_id/videos"));
+}
+
+=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::FairViewer::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::FairViewer::Channels
diff --git a/lib/WWW/FairViewer/CommentThreads.pm b/lib/WWW/FairViewer/CommentThreads.pm
new file mode 100644
index 0000000..db8e3ea
--- /dev/null
+++ b/lib/WWW/FairViewer/CommentThreads.pm
@@ -0,0 +1,98 @@
+package WWW::FairViewer::CommentThreads;
+
+use utf8;
+use 5.014;
+use warnings;
+
+=head1 NAME
+
+WWW::FairViewer::CommentThreads - Retrieve comments threads.
+
+=head1 SYNOPSIS
+
+ use WWW::FairViewer;
+ my $obj = WWW::FairViewer->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) = @_;
+ $self->_get_results(
+ $self->_make_feed_url("comments/$video_id",
+ sort_by => $self->get_comments_order,
+ ),
+ );
+}
+
+=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::FairViewer::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::FairViewer::CommentThreads
diff --git a/lib/WWW/FairViewer/GetCaption.pm b/lib/WWW/FairViewer/GetCaption.pm
new file mode 100644
index 0000000..24741b1
--- /dev/null
+++ b/lib/WWW/FairViewer/GetCaption.pm
@@ -0,0 +1,280 @@
+package WWW::FairViewer::GetCaption;
+
+use utf8;
+use 5.014;
+use warnings;
+
+=head1 NAME
+
+WWW::FairViewer::GetCaption - Save the YouTube closed captions as .srt files for a videoID.
+
+=head1 SYNOPSIS
+
+ use WWW::FairViewer::GetCaption;
+
+ my $yv_cap = WWW::FairViewer::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::FairViewer::ParseXML;
+ my $hash = eval { WWW::FairViewer::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::FairViewer::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::FairViewer::GetCaption
diff --git a/lib/WWW/FairViewer/GuideCategories.pm b/lib/WWW/FairViewer/GuideCategories.pm
new file mode 100644
index 0000000..a65670c
--- /dev/null
+++ b/lib/WWW/FairViewer/GuideCategories.pm
@@ -0,0 +1,85 @@
+package WWW::FairViewer::GuideCategories;
+
+use utf8;
+use 5.014;
+use warnings;
+
+=head1 NAME
+
+WWW::FairViewer::GuideCategories - Categories interface.
+
+=head1 SYNOPSIS
+
+ use WWW::FairViewer;
+ my $obj = WWW::FairViewer->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::FairViewer::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::FairViewer::GuideCategories
diff --git a/lib/WWW/FairViewer/Itags.pm b/lib/WWW/FairViewer/Itags.pm
new file mode 100644
index 0000000..3a3f007
--- /dev/null
+++ b/lib/WWW/FairViewer/Itags.pm
@@ -0,0 +1,319 @@
+package WWW::FairViewer::Itags;
+
+use utf8;
+use 5.014;
+use warnings;
+
+=head1 NAME
+
+WWW::FairViewer::Itags - Get the YouTube itags.
+
+=head1 SYNOPSIS
+
+ use WWW::FairViewer::Itags;
+
+ my $yv_itags = WWW::FairViewer::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::FairViewer::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::FairViewer::Itags
diff --git a/lib/WWW/FairViewer/ParseJSON.pm b/lib/WWW/FairViewer/ParseJSON.pm
new file mode 100644
index 0000000..1e2af77
--- /dev/null
+++ b/lib/WWW/FairViewer/ParseJSON.pm
@@ -0,0 +1,76 @@
+package WWW::FairViewer::ParseJSON;
+
+use utf8;
+use 5.014;
+use warnings;
+
+=head1 NAME
+
+WWW::FairViewer::ParseJSON - Parse JSON content.
+
+=head1 SYNOPSIS
+
+ use WWW::FairViewer::ParseJSON;
+ my $obj = WWW::FairViewer::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::FairViewer::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::FairViewer::ParseJSON
diff --git a/lib/WWW/FairViewer/ParseXML.pm b/lib/WWW/FairViewer/ParseXML.pm
new file mode 100644
index 0000000..23e01a5
--- /dev/null
+++ b/lib/WWW/FairViewer/ParseXML.pm
@@ -0,0 +1,311 @@
+package WWW::FairViewer::ParseXML;
+
+use utf8;
+use 5.014;
+use warnings;
+
+=encoding utf8
+
+=head1 NAME
+
+WWW::FairViewer::ParseXML - Convert XML to a HASH ref structure.
+
+=head1 SYNOPSIS
+
+Parse XML content and return an HASH ref structure.
+
+Usage:
+
+ use WWW::FairViewer::ParseXML;
+ my $hash_ref = WWW::FairViewer::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::FairViewer::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::FairViewer::ParseXML
diff --git a/lib/WWW/FairViewer/PlaylistItems.pm b/lib/WWW/FairViewer/PlaylistItems.pm
new file mode 100644
index 0000000..767903e
--- /dev/null
+++ b/lib/WWW/FairViewer/PlaylistItems.pm
@@ -0,0 +1,146 @@
+package WWW::FairViewer::PlaylistItems;
+
+use utf8;
+use 5.014;
+use warnings;
+
+=head1 NAME
+
+WWW::FairViewer::PlaylistItems - Manage playlist entries.
+
+=head1 SYNOPSIS
+
+ use WWW::FairViewer;
+ my $obj = WWW::FairViewer->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) = @_;
+ $self->_get_results($self->_make_feed_url("playlists/$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) = @_;
+ $self->videos_from_username($username);
+ };
+
+ *{__PACKAGE__ . '::' . $name} = sub {
+ my ($self, $channel_id) = @_;
+ $self->videos_from_channel_id($channel_id);
+ };
+ }
+}
+
+=head1 AUTHOR
+
+Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >>
+
+
+=head1 SUPPORT
+
+You can find documentation for this module with the perldoc command.
+
+ perldoc WWW::FairViewer::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::FairViewer::PlaylistItems
diff --git a/lib/WWW/FairViewer/Playlists.pm b/lib/WWW/FairViewer/Playlists.pm
new file mode 100644
index 0000000..2adef54
--- /dev/null
+++ b/lib/WWW/FairViewer/Playlists.pm
@@ -0,0 +1,124 @@
+package WWW::FairViewer::Playlists;
+
+use utf8;
+use 5.014;
+use warnings;
+
+=head1 NAME
+
+WWW::FairViewer::Playlists - Fair playlists handle.
+
+=head1 SYNOPSIS
+
+ use WWW::FairViewer;
+ my $obj = WWW::FairViewer->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::FairViewer::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::FairViewer::Playlists
diff --git a/lib/WWW/FairViewer/RegularExpressions.pm b/lib/WWW/FairViewer/RegularExpressions.pm
new file mode 100644
index 0000000..20d2a99
--- /dev/null
+++ b/lib/WWW/FairViewer/RegularExpressions.pm
@@ -0,0 +1,89 @@
+package WWW::FairViewer::RegularExpressions;
+
+use utf8;
+use 5.014;
+use warnings;
+
+require Exporter;
+our @ISA = qw(Exporter);
+
+=head1 NAME
+
+WWW::FairViewer::RegularExpressions - Various utils.
+
+=head1 SYNOPSIS
+
+ use WWW::FairViewer::RegularExpressions;
+ use WWW::FairViewer::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::FairViewer::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::FairViewer::RegularExpressions
diff --git a/lib/WWW/FairViewer/Search.pm b/lib/WWW/FairViewer/Search.pm
new file mode 100644
index 0000000..256e605
--- /dev/null
+++ b/lib/WWW/FairViewer/Search.pm
@@ -0,0 +1,175 @@
+package WWW::FairViewer::Search;
+
+use utf8;
+use 5.014;
+use warnings;
+
+=head1 NAME
+
+WWW::FairViewer::Search - Search functions for Fair API v3
+
+=head1 SYNOPSIS
+
+ use WWW::FairViewer;
+ my $obj = WWW::FairViewer->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::FairViewer::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::FairViewer::Search
diff --git a/lib/WWW/FairViewer/Subscriptions.pm b/lib/WWW/FairViewer/Subscriptions.pm
new file mode 100644
index 0000000..eb8add9
--- /dev/null
+++ b/lib/WWW/FairViewer/Subscriptions.pm
@@ -0,0 +1,272 @@
+package WWW::FairViewer::Subscriptions;
+
+use utf8;
+use 5.014;
+use warnings;
+
+=head1 NAME
+
+WWW::FairViewer::Subscriptions - Subscriptions handler.
+
+=head1 SYNOPSIS
+
+ use WWW::FairViewer;
+ my $obj = WWW::FairViewer->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::FairViewer::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::FairViewer::Subscriptions
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
diff --git a/lib/WWW/FairViewer/VideoCategories.pm b/lib/WWW/FairViewer/VideoCategories.pm
new file mode 100644
index 0000000..2930948
--- /dev/null
+++ b/lib/WWW/FairViewer/VideoCategories.pm
@@ -0,0 +1,97 @@
+package WWW::FairViewer::VideoCategories;
+
+use utf8;
+use 5.014;
+use warnings;
+
+=head1 NAME
+
+WWW::FairViewer::VideoCategories - videoCategory resource handler.
+
+=head1 SYNOPSIS
+
+ use WWW::FairViewer;
+ my $obj = WWW::FairViewer->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::FairViewer::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::FairViewer::VideoCategories
diff --git a/lib/WWW/FairViewer/Videos.pm b/lib/WWW/FairViewer/Videos.pm
new file mode 100644
index 0000000..3e44b52
--- /dev/null
+++ b/lib/WWW/FairViewer/Videos.pm
@@ -0,0 +1,230 @@
+package WWW::FairViewer::Videos;
+
+use utf8;
+use 5.014;
+use warnings;
+
+=head1 NAME
+
+WWW::FairViewer::Videos - videos handler.
+
+=head1 SYNOPSIS
+
+ use WWW::FairViewer;
+ my $obj = WWW::FairViewer->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::FairViewer::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, $fields) = @_;
+ $fields //= $self->basic_video_info_fields;
+ $self->_get_results($self->_make_feed_url("videos/$id", fields => $fields));
+}
+
+=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::FairViewer::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::FairViewer::Videos