Added files.

Esse commit está contido em:
trizen
2020-10-30 18:25:50 +02:00
commit 353ee7b23e
38 arquivos alterados com 17521 adições e 0 exclusões
+94
Ver Arquivo
@@ -0,0 +1,94 @@
use utf8;
use 5.010;
use strict;
use warnings;
use Module::Build;
my $gtk = grep { /^--?gtk3?\z/ } @ARGV;
my $builder = Module::Build->new(
module_name => 'WWW::PipeViewer',
license => 'perl',
dist_author => q{Trizen <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d>},
dist_version_from => 'lib/WWW/PipeViewer.pm',
release_status => 'stable',
build_requires => {
'Test::More' => 0,
},
extra_manify_args => { utf8 => 1 },
configure_requires => {
'Module::Build' => 0,
},
get_options => {
'gtk3' => {
type => '!',
store => \$gtk,
},
},
requires => {
'perl' => 5.016,
'Data::Dump' => 0,
'File::Spec' => 0,
'File::Spec::Functions' => 0,
'File::Path' => 0,
'Getopt::Long' => 0,
'HTTP::Request' => 0,
'JSON' => 0,
'Encode' => 0,
'Memoize' => 0,
'MIME::Base64' => 0,
'List::Util' => 0,
'LWP::UserAgent' => 0,
'LWP::Protocol::https' => 0,
'Term::ANSIColor' => 0,
'Term::ReadLine' => 0,
'Text::ParseWords' => 0,
'Text::Wrap' => 0,
'URI::Escape' => 0,
$gtk
? (
'Gtk3' => 0,
'File::ShareDir' => 0,
'Storable' => 0,
'Digest::MD5' => 0,
)
: (),
},
recommends => {
'LWP::UserAgent::Cached' => 0, # cache support
'Term::ReadLine::Gnu' => 0, # for better STDIN support (+history)
'JSON::XS' => 0, # faster JSON to HASH conversion
'Mozilla::CA' => 0, # just in case if there are SSL problems
},
auto_features => {
fixed_width_support => {
description => "Print the results in a fixed-width format (--fixed-width, -W)",
requires => {
'Unicode::GCString' => 0, # this is recommended
#'Text::CharWidth' => 0, # this works as fallback
},
},
},
add_to_cleanup => ['WWW-PipeViewer-*'],
create_makefile_pl => 'traditional',
);
$builder->script_files(
['bin/pipe-viewer',
($gtk ? ('bin/gtk-pipe-viewer') : ()),
]
);
$builder->share_dir('share') if $gtk;
$builder->create_build_script();
+10
Ver Arquivo
@@ -0,0 +1,10 @@
# Revision history for pipe-viewer.
# For all changes, check out the release notes at:
# https://github.com/trizen/pipe-viewer/releases
[CHANGELOG]
Version 0.0.1
- To be released soon.
+201
Ver Arquivo
@@ -0,0 +1,201 @@
The Artistic License 2.0
Copyright (c) 2000-2006, The Perl Foundation.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
This license establishes the terms under which a given free software
Package may be copied, modified, distributed, and/or redistributed.
The intent is that the Copyright Holder maintains some artistic
control over the development of that Package while still keeping the
Package available as open source and free software.
You are always permitted to make arrangements wholly outside of this
license directly with the Copyright Holder of a given Package. If the
terms of this license do not permit the full use that you propose to
make of the Package, you should contact the Copyright Holder and seek
a different licensing arrangement.
Definitions
"Copyright Holder" means the individual(s) or organization(s)
named in the copyright notice for the entire Package.
"Contributor" means any party that has contributed code or other
material to the Package, in accordance with the Copyright Holder's
procedures.
"You" and "your" means any person who would like to copy,
distribute, or modify the Package.
"Package" means the collection of files distributed by the
Copyright Holder, and derivatives of that collection and/or of
those files. A given Package may consist of either the Standard
Version, or a Modified Version.
"Distribute" means providing a copy of the Package or making it
accessible to anyone else, or in the case of a company or
organization, to others outside of your company or organization.
"Distributor Fee" means any fee that you charge for Distributing
this Package or providing support for this Package to another
party. It does not mean licensing fees.
"Standard Version" refers to the Package if it has not been
modified, or has been modified only in ways explicitly requested
by the Copyright Holder.
"Modified Version" means the Package, if it has been changed, and
such changes were not explicitly requested by the Copyright
Holder.
"Original License" means this Artistic License as Distributed with
the Standard Version of the Package, in its current version or as
it may be modified by The Perl Foundation in the future.
"Source" form means the source code, documentation source, and
configuration files for the Package.
"Compiled" form means the compiled bytecode, object code, binary,
or any other form resulting from mechanical transformation or
translation of the Source form.
Permission for Use and Modification Without Distribution
(1) You are permitted to use the Standard Version and create and use
Modified Versions for any purpose without restriction, provided that
you do not Distribute the Modified Version.
Permissions for Redistribution of the Standard Version
(2) You may Distribute verbatim copies of the Source form of the
Standard Version of this Package in any medium without restriction,
either gratis or for a Distributor Fee, provided that you duplicate
all of the original copyright notices and associated disclaimers. At
your discretion, such verbatim copies may or may not include a
Compiled form of the Package.
(3) You may apply any bug fixes, portability changes, and other
modifications made available from the Copyright Holder. The resulting
Package will still be considered the Standard Version, and as such
will be subject to the Original License.
Distribution of Modified Versions of the Package as Source
(4) You may Distribute your Modified Version as Source (either gratis
or for a Distributor Fee, and with or without a Compiled form of the
Modified Version) provided that you clearly document how it differs
from the Standard Version, including, but not limited to, documenting
any non-standard features, executables, or modules, and provided that
you do at least ONE of the following:
(a) make the Modified Version available to the Copyright Holder
of the Standard Version, under the Original License, so that the
Copyright Holder may include your modifications in the Standard
Version.
(b) ensure that installation of your Modified Version does not
prevent the user installing or running the Standard Version. In
addition, the Modified Version must bear a name that is different
from the name of the Standard Version.
(c) allow anyone who receives a copy of the Modified Version to
make the Source form of the Modified Version available to others
under
(i) the Original License or
(ii) a license that permits the licensee to freely copy,
modify and redistribute the Modified Version using the same
licensing terms that apply to the copy that the licensee
received, and requires that the Source form of the Modified
Version, and of any works derived from it, be made freely
available in that license fees are prohibited but Distributor
Fees are allowed.
Distribution of Compiled Forms of the Standard Version
or Modified Versions without the Source
(5) You may Distribute Compiled forms of the Standard Version without
the Source, provided that you include complete instructions on how to
get the Source of the Standard Version. Such instructions must be
valid at the time of your distribution. If these instructions, at any
time while you are carrying out such distribution, become invalid, you
must provide new instructions on demand or cease further distribution.
If you provide valid instructions or cease distribution within thirty
days after you become aware that the instructions are invalid, then
you do not forfeit any of your rights under this license.
(6) You may Distribute a Modified Version in Compiled form without
the Source, provided that you comply with Section 4 with respect to
the Source of the Modified Version.
Aggregating or Linking the Package
(7) You may aggregate the Package (either the Standard Version or
Modified Version) with other packages and Distribute the resulting
aggregation provided that you do not charge a licensing fee for the
Package. Distributor Fees are permitted, and licensing fees for other
components in the aggregation are permitted. The terms of this license
apply to the use and Distribution of the Standard or Modified Versions
as included in the aggregation.
(8) You are permitted to link Modified and Standard Versions with
other works, to embed the Package in a larger work of your own, or to
build stand-alone binary or bytecode versions of applications that
include the Package, and Distribute the result without restriction,
provided the result does not expose a direct interface to the Package.
Items That are Not Considered Part of a Modified Version
(9) Works (including, but not limited to, modules and scripts) that
merely extend or make use of the Package, do not, by themselves, cause
the Package to be a Modified Version. In addition, such works are not
considered parts of the Package itself, and are not subject to the
terms of this license.
General Provisions
(10) Any use, modification, and distribution of the Standard or
Modified Versions is governed by this Artistic License. By using,
modifying or distributing the Package, you accept this license. Do not
use, modify, or distribute the Package, if you do not accept this
license.
(11) If your Modified Version has been derived from a Modified
Version made by someone other than you, you are nevertheless required
to ensure that your Modified Version complies with the requirements of
this license.
(12) This license does not grant you the right to use any trademark,
service mark, tradename, or logo of the Copyright Holder.
(13) This license includes the non-exclusive, worldwide,
free-of-charge patent license to make, have made, use, offer to sell,
sell, import and otherwise transfer the Package with respect to any
patent claims licensable by the Copyright Holder that are necessarily
infringed by the Package. If you institute patent litigation
(including a cross-claim or counterclaim) against any party alleging
that the Package constitutes direct or contributory patent
infringement, then this Artistic License to you shall terminate on the
date that such litigation is filed.
(14) Disclaimer of Warranty:
THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS
IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR
NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL
LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+39
Ver Arquivo
@@ -0,0 +1,39 @@
bin/gtk-pipe-viewer
bin/pipe-viewer
Build.PL
Changes
lib/WWW/PipeViewer.pm
lib/WWW/PipeViewer/Activities.pm
lib/WWW/PipeViewer/Authentication.pm
lib/WWW/PipeViewer/Channels.pm
lib/WWW/PipeViewer/CommentThreads.pm
lib/WWW/PipeViewer/GetCaption.pm
lib/WWW/PipeViewer/GuideCategories.pm
lib/WWW/PipeViewer/Itags.pm
lib/WWW/PipeViewer/ParseJSON.pm
lib/WWW/PipeViewer/ParseXML.pm
lib/WWW/PipeViewer/PlaylistItems.pm
lib/WWW/PipeViewer/Playlists.pm
lib/WWW/PipeViewer/RegularExpressions.pm
lib/WWW/PipeViewer/Search.pm
lib/WWW/PipeViewer/Subscriptions.pm
lib/WWW/PipeViewer/Utils.pm
lib/WWW/PipeViewer/VideoCategories.pm
lib/WWW/PipeViewer/Videos.pm
LICENSE
Makefile.PL
MANIFEST This list of files
META.json
META.yml
README.md
share/gtk-pipe-viewer.desktop
share/gtk-pipe-viewer.glade
share/icons/default_thumb.jpg
share/icons/feed.png
share/icons/feed_gray.png
share/icons/gtk-pipe-viewer.png
share/icons/spinner.gif
share/icons/user.png
t/00-load.t
t/kwalitee.t
t/pod.t
+79
Ver Arquivo
@@ -0,0 +1,79 @@
#!start included /usr/share/perl5/core_perl/ExtUtils/MANIFEST.SKIP
# Avoid version control files.
\bRCS\b
\bCVS\b
\bSCCS\b
,v$
\B\.svn\b
\B\.git\b
\B\.gitignore\b
\b_darcs\b
\B\.cvsignore$
# Avoid VMS specific MakeMaker generated files
\bDescrip.MMS$
\bDESCRIP.MMS$
\bdescrip.mms$
# Avoid Makemaker generated and utility files.
\bMANIFEST\.bak
\bMakefile$
\bblib/
\bMakeMaker-\d
\bpm_to_blib\.ts$
\bpm_to_blib$
\bblibdirs\.ts$ # 6.18 through 6.25 generated this
# Avoid Module::Build generated and utility files.
\bBuild$
\b_build/
\bBuild.bat$
\bBuild.COM$
\bBUILD.COM$
\bbuild.com$
# Other files
.github/FUNDING.yml
bin/inv.json
bin/yv.json
# Avoid temp and backup files.
~$
\.old$
\#$
\b\.#
\.bak$
\.tmp$
\.#
\.rej$
# Avoid OS-specific files/dirs
# Mac OSX metadata
\B\.DS_Store
# Mac OSX SMB mount metadata files
\B\._
# Avoid Devel::Cover and Devel::CoverX::Covered files.
\bcover_db\b
\bcovered\b
# Avoid MYMETA files
^MYMETA\.
#!end included /usr/share/perl5/core_perl/ExtUtils/MANIFEST.SKIP
# Avoid configuration metadata file
^MYMETA\.
# Avoid Module::Build generated and utility files.
\bBuild$
\bBuild.bat$
\b_build
\bBuild.COM$
\bBUILD.COM$
\bbuild.com$
^MANIFEST\.SKIP
# Avoid archives of this distribution
\bWWW-PipeViewer-[\d\.\_]+
WWW-PipeViewer-*
+35
Ver Arquivo
@@ -0,0 +1,35 @@
# Note: this file was auto-generated by Module::Build::Compat version 0.4231
require 5.016;
use ExtUtils::MakeMaker;
WriteMakefile
(
'NAME' => 'WWW::PipeViewer',
'VERSION_FROM' => 'lib/WWW/PipeViewer.pm',
'PREREQ_PM' => {
'Data::Dump' => 0,
'Encode' => 0,
'File::Path' => 0,
'File::Spec' => 0,
'File::Spec::Functions' => 0,
'Getopt::Long' => 0,
'HTTP::Request' => 0,
'JSON' => 0,
'LWP::Protocol::https' => 0,
'LWP::UserAgent' => 0,
'List::Util' => 0,
'MIME::Base64' => 0,
'Memoize' => 0,
'Term::ANSIColor' => 0,
'Term::ReadLine' => 0,
'Test::More' => 0,
'Text::ParseWords' => 0,
'Text::Wrap' => 0,
'URI::Escape' => 0
},
'INSTALLDIRS' => 'site',
'EXE_FILES' => [
'bin/pipe-viewer'
],
'PL_FILES' => {}
)
;
+140
Ver Arquivo
@@ -0,0 +1,140 @@
## pipe-viewer
A lightweight application (fork of [straw-viewer](https://github.com/trizen/straw-viewer)) for searching and playing videos from YouTube, using the [API](https://github.com/iv-org/invidious/wiki/API) of [invidio.us](https://invidio.us/).
The goal of this fork is to parse the YouTube website directly, removing the dependency on invidious instances.
### pipe-viewer
* command-line interface to YouTube.
![pipe-viewer](https://user-images.githubusercontent.com/614513/73046877-5cae1200-3e7c-11ea-8ab3-f8c444f88b30.png)
### gtk-pipe-viewer
* GTK+ interface to YouTube.
![gtk-pipe-viewer](https://user-images.githubusercontent.com/614513/84770876-11d69780-afe1-11ea-96f7-5d426dc865e5.png)
### STATUS
The project is in its early stages of development and some features are not implemented yet.
Currently, only the searching for videos uses the YouTube website directly.
### AVAILABILITY
* Arch Linux (AUR): https://aur.archlinux.org/packages/pipe-viewer-git/
### INSTALLATION
To install `pipe-viewer`, run:
```console
perl Build.PL
sudo ./Build installdeps
sudo ./Build install
```
To install `gtk-pipe-viewer` along with `pipe-viewer`, run:
```console
perl Build.PL --gtk
sudo ./Build installdeps
sudo ./Build install
```
### TRY
For trying the latest commit of `pipe-viewer`, without installing it, execute the following commands:
```console
cd /tmp
wget https://github.com/trizen/pipe-viewer/archive/master.zip -O pipe-viewer-master.zip
unzip -n pipe-viewer-master.zip
cd pipe-viewer-master/bin
perl -pi -ne 's{DEVEL = 0}{DEVEL = 1}' {gtk-,}pipe-viewer
./pipe-viewer
```
### DEPENDENCIES
#### For pipe-viewer:
* [libwww-perl](https://metacpan.org/release/libwww-perl)
* [LWP::Protocol::https](https://metacpan.org/release/LWP-Protocol-https)
* [Data::Dump](https://metacpan.org/release/Data-Dump)
* [JSON](https://metacpan.org/release/JSON)
#### For gtk-pipe-viewer:
* [Gtk3](https://metacpan.org/release/Gtk3)
* [File::ShareDir](https://metacpan.org/release/File-ShareDir)
* \+ the dependencies required by pipe-viewer.
#### Build dependencies:
* [Module::Build](https://metacpan.org/pod/Module::Build)
#### Optional dependencies:
* Local cache support: [LWP::UserAgent::Cached](https://metacpan.org/release/LWP-UserAgent-Cached)
* Better STDIN support (+ history): [Term::ReadLine::Gnu](https://metacpan.org/release/Term-ReadLine-Gnu)
* Faster JSON deserialization: [JSON::XS](https://metacpan.org/release/JSON-XS)
* Fixed-width formatting (--fixed-width, -W): [Unicode::LineBreak](https://metacpan.org/release/Unicode-LineBreak) or [Text::CharWidth](https://metacpan.org/release/Text-CharWidth)
### PACKAGING
To package this application, run the following commands:
```console
perl Build.PL --destdir "/my/package/path" --installdirs vendor [--gtk]
./Build test
./Build install --install_path script=/usr/bin
```
### INVIDIOUS INSTANCES
Sometimes, the default instance, [invidious.snopyta.org](https://invidious.snopyta.org/), may fail to work properly. When this happens, we can change the API host to some other instance of invidious, such as [invidious.tube](https://invidious.tube/):
```console
pipe-viewer --api=invidious.tube
```
To make the change permanent, set in the configuration file:
```perl
api_host => "invidious.tube",
```
Alternatively, the following will automatically pick a random invidious instance everytime the program is started:
```perl
api_host => "auto",
```
The available instances are listed at: https://instances.invidio.us/
### SUPPORT AND DOCUMENTATION
After installing, you can find documentation with the following commands:
man pipe-viewer
perldoc WWW::PipeViewer
### LICENSE AND COPYRIGHT
Copyright (C) 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 http://dev.perl.org/licenses/ for more information.
Arquivo executável
+3652
Ver Arquivo
Diferenças do arquivo suprimidas por serem muito extensas Carregar Diff
Arquivo executável
+4532
Ver Arquivo
Diferenças do arquivo suprimidas por serem muito extensas Carregar Diff
Diferenças do arquivo suprimidas por serem muito extensas Carregar Diff
+93
Ver Arquivo
@@ -0,0 +1,93 @@
package WWW::PipeViewer::Activities;
use utf8;
use 5.014;
use warnings;
=head1 NAME
WWW::PipeViewer::Activities - list of channel activity events that match the request criteria.
=head1 SYNOPSIS
use WWW::PipeViewer;
my $obj = WWW::PipeViewer->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::PipeViewer::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::PipeViewer::Activities
+216
Ver Arquivo
@@ -0,0 +1,216 @@
package WWW::PipeViewer::Authentication;
use utf8;
use 5.014;
use warnings;
=head1 NAME
WWW::PipeViewer::Authentication - OAuth login support.
=head1 SYNOPSIS
use WWW::PipeViewer;
my $hash_ref = WWW::PipeViewer->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::PipeViewer::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::PipeViewer::Authentication
+214
Ver Arquivo
@@ -0,0 +1,214 @@
package WWW::PipeViewer::Channels;
use utf8;
use 5.014;
use warnings;
=head1 NAME
WWW::PipeViewer::Channels - Channels interface.
=head1 SYNOPSIS
use WWW::PipeViewer;
my $obj = WWW::PipeViewer->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 popular_videos($channel_id)
Get the most popular videos for a given channel ID.
=cut
sub popular_videos {
my ($self, $channel_id) = @_;
if (not defined($channel_id)) { # trending popular videos
return $self->_get_results($self->_make_feed_url('popular'));
}
return $self->_get_results($self->_make_feed_url("channels/$channel_id/videos", sort_by => 'popular'));
}
=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) = @_;
# A channel's username (if it doesn't include spaces) is also valid in place of ucid.
if ($username =~ /\w/ and not $username =~ /\s/) {
return $username;
}
# TODO: resolve channel name to channel ID
return $username;
}
=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::PipeViewer::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::PipeViewer::Channels
+98
Ver Arquivo
@@ -0,0 +1,98 @@
package WWW::PipeViewer::CommentThreads;
use utf8;
use 5.014;
use warnings;
=head1 NAME
WWW::PipeViewer::CommentThreads - Retrieve comments threads.
=head1 SYNOPSIS
use WWW::PipeViewer;
my $obj = WWW::PipeViewer->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::PipeViewer::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::PipeViewer::CommentThreads
+252
Ver Arquivo
@@ -0,0 +1,252 @@
package WWW::PipeViewer::GetCaption;
use utf8;
use 5.014;
use warnings;
=head1 NAME
WWW::PipeViewer::GetCaption - Save the YouTube closed captions as .srt files for a videoID.
=head1 SYNOPSIS
use WWW::PipeViewer::GetCaption;
my $yv_cap = WWW::PipeViewer::GetCaption->new(%opts);
my $file = $yv_cap->save_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)];
$self->{yv_obj} = undef;
foreach my $key (keys %{$self}) {
$self->{$key} = delete $opts{$key}
if exists $opts{$key};
}
$self->{yv_obj} //= do {
require WWW::PipeViewer;
WWW::PipeViewer->new(cache_dir => $self->{captions_dir},);
};
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::PipeViewer::ParseXML;
my $hash = eval { WWW::PipeViewer::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) = @_;
$self->{yv_obj}->lwp_get($url, simple => 1);
}
=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::PipeViewer::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::PipeViewer::GetCaption
+85
Ver Arquivo
@@ -0,0 +1,85 @@
package WWW::PipeViewer::GuideCategories;
use utf8;
use 5.014;
use warnings;
=head1 NAME
WWW::PipeViewer::GuideCategories - Categories interface.
=head1 SYNOPSIS
use WWW::PipeViewer;
my $obj = WWW::PipeViewer->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{region} //= $self->get_region;
}
$self->_make_feed_url('guideCategories', %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 => 'region',
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::PipeViewer::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::PipeViewer::GuideCategories
+299
Ver Arquivo
@@ -0,0 +1,299 @@
package WWW::PipeViewer::Itags;
use utf8;
use 5.014;
use warnings;
=head1 NAME
WWW::PipeViewer::Itags - Get the YouTube itags.
=head1 SYNOPSIS
use WWW::PipeViewer::Itags;
my $yv_itags = WWW::PipeViewer::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' => [{value => 38, format => 'mp4'}, # mp4 (3072p) (v-a)
{value => 138, format => 'mp4', dash => 1}, # mp4 (2160p-4320p) (v)
{value => 266, format => 'mp4', dash => 1}, # mp4 (2160p-2304p) (v)
],
'2160' => [{value => 315, format => 'webm', dash => 1, hfr => 1}, # webm HFR (v)
{value => 272, format => 'webm', dash => 1}, # webm (v)
{value => 313, format => 'webm', dash => 1}, # webm (v)
{value => 401, format => 'av1', dash => 1}, # av1 (v)
],
'1440' => [{value => 308, format => 'webm', dash => 1, hfr => 1}, # webm HFR (v)
{value => 271, format => 'webm', dash => 1}, # webm (v)
{value => 264, format => 'mp4', dash => 1}, # mp4 (v)
{value => 400, format => 'av1', dash => 1}, # av1 (v)
],
'1080' => [{value => 303, format => 'webm', dash => 1, hfr => 1}, # webm HFR (v)
{value => 299, format => 'mp4', dash => 1, hfr => 1}, # mp4 HFR (v)
{value => 248, format => 'webm', dash => 1}, # webm (v)
{value => 137, format => 'mp4', dash => 1}, # mp4 (v)
{value => 399, format => 'av1', dash => 1, hfr => 1}, # av1 (v)
{value => 46, format => 'webm'}, # webm (v-a)
{value => 37, format => 'mp4'}, # mp4 (v-a)
{value => 301, format => 'mp4', live => 1}, # mp4 (live) (v-a)
{value => 96, format => 'ts', live => 1}, # ts (live) (v-a)
],
'720' => [{value => 302, format => 'webm', dash => 1, hfr => 1}, # webm HFR (v)
{value => 298, format => 'mp4', dash => 1, hfr => 1}, # mp4 HFR (v)
{value => 247, format => 'webm', dash => 1}, # webm (v)
{value => 136, format => 'mp4', dash => 1}, # mp4 (v)
{value => 398, format => 'av1', dash => 1, hfr => 1}, # av1 (v)
{value => 45, format => 'webm'}, # webm (v-a)
{value => 22, format => 'mp4'}, # mp4 (v-a)
{value => 300, format => 'mp4', live => 1}, # mp4 (live) (v-a)
{value => 120, format => 'flv', live => 1}, # flv (live) (v-a)
{value => 95, format => 'ts', live => 1}, # ts (live) (v-a)
],
'480' => [{value => 244, format => 'webm', dash => 1}, # webm (v)
{value => 135, format => 'mp4', dash => 1}, # mp4 (v)
{value => 397, format => 'av1', dash => 1}, # av1 (v)
{value => 44, format => 'webm'}, # webm (v-a)
{value => 35, format => 'flv'}, # flv (v-a)
{value => 94, format => 'mp4', live => 1}, # mp4 (live) (v-a)
],
'360' => [{value => 243, format => 'webm', dash => 1}, # webm (v)
{value => 134, format => 'mp4', dash => 1}, # mp4 (v)
{value => 396, format => 'av1', dash => 1}, # av1 (v)
{value => 43, format => 'webm'}, # webm (v-a)
{value => 34, format => 'flv'}, # flv (v-a)
{value => 93, format => 'mp4', live => 1}, # mp4 (live) (v-a)
{value => 18, format => 'mp4'}, # mp4 (v-a)
],
'240' => [{value => 242, format => 'webm', dash => 1}, # webm (v)
{value => 133, format => 'mp4', dash => 1}, # mp4 (v)
{value => 395, format => 'av1', dash => 1}, # av1 (v)
{value => 6, format => 'flv'}, # flv (270p) (v-a)
{value => 5, format => 'flv'}, # flv (v-a)
{value => 36, format => '3gp'}, # 3gp (v-a)
{value => 13, format => '3gp'}, # 3gp (v-a)
{value => 92, format => 'mp4', live => 1}, # mp4 (live) (v-a)
{value => 132, format => 'ts', live => 1}, # ts (live) (v-a)
],
'144' => [{value => 278, format => 'webm', dash => 1}, # webm (v)
{value => 160, format => 'mp4', dash => 1}, # mp4 (v)
{value => 394, format => 'av1', dash => 1}, # av1 (v)
{value => 17, format => '3gp'}, # 3gp (v-a)
{value => 91, format => 'mp4'}, # mp4 (live) (v-a)
{value => 151, format => 'ts'}, # ts (live) (v-a)
],
'audio' => [{value => 172, format => 'webm', kbps => 192}, # webm (192 kbps)
{value => 251, format => 'opus', kbps => 160}, # webm opus (128-160 kbps)
{value => 171, format => 'webm', kbps => 128}, # webm vorbis (92-128 kbps)
{value => 140, format => 'm4a', kbps => 128}, # mp4a (128 kbps)
{value => 141, format => 'm4a', kbps => 256}, # mp4a (256 kbps)
{value => 250, format => 'opus', kbps => 64}, # webm opus (64 kbps)
{value => 249, format => 'opus', kbps => 48}, # webm opus (48 kbps)
{value => 139, format => 'm4a', kbps => 48}, # 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}}) {
next if not exists $stream->{$itag->{value}};
my $entry = $stream->{$itag->{value}};
if (defined($entry->{fps}) and $entry->{fps} >= 50) {
$args{hfr} || next; # skip high frame rate (HFR) videos
}
if ($itag->{format} eq 'av1') {
$args{ignore_av1} && next; # ignore videos in AV1 format
}
if ($itag->{dash}) {
$args{dash} || next;
my $video_info = $stream->{$itag->{value}};
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 ($resolution eq 'audio' and not $args{dash_mp4_audio}) {
if ($itag->{format} eq 'm4a') {
next; # skip m4a audio URLs
}
}
# Ignore segmented DASH URLs (they load pretty slow in mpv)
if (not $args{dash_segmented}) {
next if ($entry->{url} =~ m{/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::PipeViewer::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::PipeViewer::Itags
+88
Ver Arquivo
@@ -0,0 +1,88 @@
package WWW::PipeViewer::ParseJSON;
use utf8;
use 5.014;
use warnings;
=head1 NAME
WWW::PipeViewer::ParseJSON - Parse JSON content.
=head1 SYNOPSIS
use WWW::PipeViewer::ParseJSON;
my $obj = WWW::PipeViewer::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_utf8_json_string {
my ($self, $json) = @_;
if (not defined($json) or $json eq '') {
return {};
}
require JSON;
my $hash = eval { JSON::from_json($json) };
return $@ ? do { warn "[JSON]: $@\n"; {} } : $hash;
}
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::PipeViewer::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::PipeViewer::ParseJSON
+311
Ver Arquivo
@@ -0,0 +1,311 @@
package WWW::PipeViewer::ParseXML;
use utf8;
use 5.014;
use warnings;
=encoding utf8
=head1 NAME
WWW::PipeViewer::ParseXML - Convert XML to a HASH ref structure.
=head1 SYNOPSIS
Parse XML content and return an HASH ref structure.
Usage:
use WWW::PipeViewer::ParseXML;
my $hash_ref = WWW::PipeViewer::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::PipeViewer::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::PipeViewer::ParseXML
+146
Ver Arquivo
@@ -0,0 +1,146 @@
package WWW::PipeViewer::PlaylistItems;
use utf8;
use 5.014;
use warnings;
=head1 NAME
WWW::PipeViewer::PlaylistItems - Manage playlist entries.
=head1 SYNOPSIS
use WWW::PipeViewer;
my $obj = WWW::PipeViewer->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::PipeViewer::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::PipeViewer::PlaylistItems
+116
Ver Arquivo
@@ -0,0 +1,116 @@
package WWW::PipeViewer::Playlists;
use utf8;
use 5.014;
use warnings;
=head1 NAME
WWW::PipeViewer::Playlists - YouTube playlists related mehods.
=head1 SYNOPSIS
use WWW::PipeViewer;
my $obj = WWW::PipeViewer->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',
%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_feed_url("channels/playlists/$channel_id"));
}
=head2 playlists_from_username($username)
Get and return the playlists created for a given username.
=cut
sub playlists_from_username {
my ($self, $username) = @_;
$self->playlists($username);
}
=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::PipeViewer::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::PipeViewer::Playlists
+89
Ver Arquivo
@@ -0,0 +1,89 @@
package WWW::PipeViewer::RegularExpressions;
use utf8;
use 5.014;
use warnings;
require Exporter;
our @ISA = qw(Exporter);
=head1 NAME
WWW::PipeViewer::RegularExpressions - Various utils.
=head1 SYNOPSIS
use WWW::PipeViewer::RegularExpressions;
use WWW::PipeViewer::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,3}+)(?>-|\.\.)([0-9]{1,3}+)?\z};
our $digit_or_equal_re = qr/(?(?=[1-9])|=)/;
our $non_digit_or_opt_re = qr{^(?!$range_num_re)(?>[0-9]{1,3}[^0-9]|[0-9]{4}|[^0-9$opt_begin_chars])};
# Generic name
my $generic_name_re = qr/[a-zA-Z0-9_.\-]{11,64}/;
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::PipeViewer::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::PipeViewer::RegularExpressions
+498
Ver Arquivo
@@ -0,0 +1,498 @@
package WWW::PipeViewer::Search;
use utf8;
use 5.014;
use warnings;
=head1 NAME
WWW::PipeViewer::Search - Search for stuff on YouTube
=head1 SYNOPSIS
use WWW::PipeViewer;
my $obj = WWW::PipeViewer->new(%opts);
$obj->search_videos(@keywords);
=head1 SUBROUTINES/METHODS
=cut
sub _time_to_seconds {
my ($time) = @_;
my ($hours, $minutes, $seconds) = (0, 0, 0);
if ($time =~ /(\d+):(\d+):(\d+)/) {
($hours, $minutes, $seconds) = ($1, $2, $3);
}
elsif ($time =~ /(\d+):(\d+)/) {
($minutes, $seconds) = ($1, $2);
}
elsif ($time =~ /(\d+)/) {
$seconds = $1;
}
$hours * 3600 + $minutes * 60 + $seconds;
}
sub _view_count_text_to_int {
my ($text) = @_;
if ($text =~ /([\d,.]+)/) {
my $v = $1;
$v =~ tr/.,//d;
return $v;
}
return 0;
}
sub _thumbnail_quality {
my ($width, $height) = @_;
$width // return 'medium';
$height // return 'medium';
if ($width == 1280 and $height == 720) {
return "maxres";
}
if ($width == 640 and $height == 480) {
return "sddefault";
}
if ($width == 480 and $height == 360) {
return 'high';
}
if ($width == 320 and $height == 180) {
return 'medium';
}
if ($width == 120 and $height == 90) {
return 'default';
}
return 'medium';
}
sub _extract_youtube_mix {
my ($self, $data) = @_;
my $info = eval { $data->{callToAction}{watchCardHeroVideoRenderer} } || return;
my $header = eval { $data->{header}{watchCardRichHeaderRenderer} };
my %mix;
$mix{type} = 'playlist';
$mix{title} =
eval { $header->{title}{runs}[0]{text} }
// eval { $info->{accessibility}{accessibilityData}{label} }
// eval { $info->{callToActionButton}{callToActionButtonRenderer}{label}{runs}[0]{text} } // 'Youtube Mix';
$mix{playlistId} = eval { $info->{navigationEndpoint}{watchEndpoint}{playlistId} } || return;
$mix{playlistThumbnail} = eval { $header->{avatar}{thumbnails}[0]{url} }
// eval { $info->{heroImage}{collageHeroImageRenderer}{leftThumbnail}{thumbnails}[0]{url} };
$mix{author} = eval { $header->{title}{runs}[0]{text} } // "YouTube";
$mix{authorId} = eval { $header->{titleNavigationEndpoint}{browseEndpoint}{browseId} } // "youtube";
return \%mix;
}
sub _extract_search_entry {
my ($self, $data, %args) = @_;
# Album
if ($args{type} eq 'all' and exists $data->{horizontalCardListRenderer}) { # TODO
return;
}
# Video
if (exists $data->{compactVideoRenderer}) {
my %video;
my $info = $data->{compactVideoRenderer};
$video{title} =
eval { $info->{title}{runs}[0]{text} } // eval { $info->{title}{accessibility}{accessibilityData}{label} } // return;
$video{videoId} = eval { $info->{navigationEndpoint}{watchEndpoint}{videoId} } // $info->{videoId} // return;
$video{author} = eval { $info->{longBylineText}{runs}[0]{text} } // eval { $info->{shortBylineText}{runs}[0]{text} };
$video{authorId} = $info->{channelId};
$video{publishedText} = eval { $info->{publishedTimeText}{runs}[0]{text} };
$video{viewCountText} = eval { $info->{shortViewCountText}{runs}[0]{text} };
$video{videoThumbnails} = eval {
[
map {
my %thumb = %$_;
$thumb{quality} = _thumbnail_quality($thumb{width}, $thumb{height});
\%thumb;
} @{$info->{thumbnail}{thumbnails}}
]
};
# FIXME: this is not the video description
$video{description} = eval { $info->{title}{accessibility}{accessibilityData}{label} };
my $time = eval { $info->{thumbnailOverlays}[0]{thumbnailOverlayTimeStatusRenderer}{text}{runs}[0]{text} };
if (defined($time)) {
$video{lengthSeconds} = eval { _time_to_seconds($time) };
}
$video{title} = eval { $info->{title}{runs}[0]{text} };
my $viewCountText = eval { $info->{viewCountText}{runs}[0]{text} };
if (defined($viewCountText)) {
$video{viewCount} = _view_count_text_to_int($viewCountText);
}
return \%video;
}
return;
}
sub _extract_search_results {
my ($self, $data, %args) = @_;
eval { ref($data->{contents}{sectionListRenderer}{contents}) eq 'ARRAY' } or return;
my @results;
foreach my $entry (@{$data->{contents}{sectionListRenderer}{contents}}) {
# YouTube Mix
if ($args{type} eq 'all' and exists $entry->{universalWatchCardRenderer}) {
my $mix = $self->_extract_youtube_mix($entry->{universalWatchCardRenderer});
if (defined($mix)) {
push(@results, $mix);
}
}
# Search results
if (exists $entry->{itemSectionRenderer}) {
eval { ref($entry->{itemSectionRenderer}{contents}) eq 'ARRAY' } || next;
foreach my $entry (@{$entry->{itemSectionRenderer}{contents}}) {
my $search_entry = $self->_extract_search_entry($entry, %args);
if (defined($search_entry)) {
#use Data::Dump qw(pp);
#pp $search_entry;
push @results, $search_entry;
}
}
}
# Continuation page
if (exists $entry->{continuationItemRenderer}) { # TODO
## ...
}
}
return @results;
}
sub _youtube_search {
my ($self, %args) = @_;
my $content = $self->lwp_get($self->get_m_youtube_url . "/results?search_query=$args{q}");
if ($content =~ m{<div id="initial-data"><!--(.*?)--></div>}is) {
my $json = $1;
my $hash = $self->parse_utf8_json_string($json);
return $self->_extract_search_results($hash, %args);
}
return;
}
sub _make_search_url {
my ($self, %opts) = @_;
my @features;
if (defined(my $vd = $self->get_videoDefinition)) {
if ($vd eq 'high') {
push @features, 'hd';
}
}
if (defined(my $vc = $self->get_videoCaption)) {
if ($vc eq 'true' or $vc eq '1') {
push @features, 'subtitles';
}
}
if (defined(my $vd = $self->get_videoDimension)) {
if ($vd eq '3d') {
push @features, '3d';
}
}
if (defined(my $license = $self->get_videoLicense)) {
if ($license eq 'creative_commons') {
push @features, 'creative_commons';
}
}
return $self->_make_feed_url(
'search',
region => $self->get_region,
sort_by => $self->get_order,
date => $self->get_date,
page => $self->page_token,
duration => $self->get_videoDuration,
(@features ? (features => join(',', @features)) : ()),
%opts,
);
}
=head2 search_for($types,$keywords;\%args)
Search for a list of types (comma-separated).
=cut
sub search_for {
my ($self, $type, $keywords, $args) = @_;
if (ref($args) ne 'HASH') {
$args = {};
}
$keywords //= [];
if (ref($keywords) ne 'ARRAY') {
$keywords = [split ' ', $keywords];
}
$keywords = $self->escape_string(join(' ', @{$keywords}));
# Search in a channel's videos
if (defined(my $channel_id = $self->get_channelId)) {
my $url = $self->_make_feed_url("channels/search/$channel_id", q => $keywords,);
return $self->_get_results($url);
}
my $url = $self->_make_search_url(
type => $type,
q => $keywords,
%$args,
);
return
scalar {
url => $url,
results => [$self->_youtube_search(q => $keywords, type => $type, %$args)],
};
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 => 'all',
}
) {
*{__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, $videoID) = @_;
my %info = $self->_get_video_info($videoID);
my $watch_next_response = $self->parse_json_string($info{watch_next_response});
my $related =
eval { $watch_next_response->{contents}{twoColumnWatchNextResults}{secondaryResults}{secondaryResults}{results} }
// return {results => []};
#use Data::Dump qw(pp);
#pp $related;
my @results;
foreach my $entry (@$related) {
my $info = $entry->{compactVideoRenderer} // next;
my $title = $info->{title}{simpleText} // next;
my $viewCount = 0;
if (($info->{viewCountText}{simpleText} // '') =~ /^([\d,]+) views/) {
$viewCount = ($1 =~ tr/,//dr);
}
elsif (($info->{viewCountText}{simpleText} // '') =~ /Recommended for you/i) {
next; # filter out recommended videos from related videos
}
my $lengthSeconds = 0;
if (($info->{lengthText}{simpleText} // '') =~ /([\d:]+)/) {
my $time = $1;
my @fields = split(/:/, $time);
my $seconds = pop(@fields) // 0;
my $minutes = pop(@fields) // 0;
my $hours = pop(@fields) // 0;
$lengthSeconds = 3600 * $hours + 60 * $minutes + $seconds;
}
my $published = 0;
if (exists $info->{publishedTimeText} and $info->{publishedTimeText}{simpleText} =~ /(\d+)\s+(\w+)\s+ago/) {
my $quantity = $1;
my $period = $2;
$period =~ s/s\z//; # make it singural
my %table = (
year => 31556952, # seconds in a year
month => 2629743.83, # seconds in a month
week => 604800, # seconds in a week
day => 86400, # seconds in a day
hour => 3600, # seconds in a hour
minute => 60, # seconds in a minute
second => 1, # seconds in a second
);
if (exists $table{$period}) {
$published = int(time - $quantity * $table{$period});
}
else {
warn "BUG: cannot parse: <<$quantity $period>>";
}
}
push @results, {
type => "video",
title => $title,
videoId => $info->{videoId},
author => $info->{longBylineText}{runs}[0]{text},
authorId => $info->{longBylineText}{runs}[0]{navigationEndpoint}{browseEndpoint}{browseId},
#authorUrl => $info->{longBylineText}{runs}[0]{navigationEndpoint}{browseEndpoint}{browseId},
description => $info->{accessibility}{accessibilityData}{label},
descriptionHtml => undef,
viewCount => $viewCount,
published => $published,
publishedText => $info->{publishedTimeText}{simpleText},
lengthSeconds => $lengthSeconds,
liveNow => ($lengthSeconds == 0), # maybe it's live if lengthSeconds == 0?
paid => 0,
premium => 0,
videoThumbnails => [
map {
scalar {
quality => 'medium',
url => $_->{url},
width => $_->{width},
height => $_->{height},
}
} @{$info->{thumbnail}{thumbnails}}
],
};
}
return
scalar {
url => undef,
results => \@results,
};
}
=head1 AUTHOR
Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >>
=head1 SUPPORT
You can find documentation for this module with the perldoc command.
perldoc WWW::PipeViewer::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::PipeViewer::Search
+272
Ver Arquivo
@@ -0,0 +1,272 @@
package WWW::PipeViewer::Subscriptions;
use utf8;
use 5.014;
use warnings;
=head1 NAME
WWW::PipeViewer::Subscriptions - Subscriptions handler.
=head1 SYNOPSIS
use WWW::PipeViewer;
my $obj = WWW::PipeViewer->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::PipeViewer::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::PipeViewer::Subscriptions
+863
Ver Arquivo
@@ -0,0 +1,863 @@
package WWW::PipeViewer::Utils;
use utf8;
use 5.014;
use warnings;
=head1 NAME
WWW::PipeViewer::Utils - Various utils.
=head1 SYNOPSIS
use WWW::PipeViewer::Utils;
my $yv_utils = WWW::PipeViewer::Utils->new(%opts);
print $yv_utils->format_time(3600);
=head1 SUBROUTINES/METHODS
=head2 new(%opts)
Options:
=over 4
=item thousand_separator => ""
Character used as thousand separator.
=item months => []
Month names for I<format_date()>
=item youtube_url_format => ""
A youtube URL format for sprintf(format, videoID).
=back
=cut
sub new {
my ($class, %opts) = @_;
my $self = bless {
thousand_separator => q{,},
youtube_url_format => 'https://www.youtube.com/watch?v=%s',
}, $class;
$self->{months} = [
qw(
Jan Feb Mar
Apr May Jun
Jul Aug Sep
Oct Nov Dec
)
];
foreach my $key (keys %{$self}) {
$self->{$key} = delete $opts{$key}
if exists $opts{$key};
}
foreach my $invalid_key (keys %opts) {
warn "Invalid key: '${invalid_key}'";
}
return $self;
}
=head2 extension($type)
Returns the extension format from a given type.
From a string like 'video/webm;+codecs="vp9"', it returns 'webm'.
=cut
sub extension {
my ($self, $type) = @_;
$type =~ /\bflv\b/i ? q{flv}
: $type =~ /\bopus\b/i ? q{opus}
: $type =~ /\b3gpp?\b/i ? q{3gp}
: $type =~ m{^video/(\w+)} ? $1
: $type =~ m{^audio/(\w+)} ? $1
: $type =~ /\bwebm\b/i ? q{webm}
: q{mp4};
}
=head2 format_time($sec)
Returns time from seconds.
=cut
sub format_time {
my ($self, $sec) = @_;
$sec >= 3600
? join q{:}, map { sprintf '%02d', $_ } $sec / 3600 % 24, $sec / 60 % 60, $sec % 60
: join q{:}, map { sprintf '%02d', $_ } $sec / 60 % 60, $sec % 60;
}
=head2 format_duration($duration)
Return seconds from duration (PT1H20M10S).
=cut
# PT5M3S -> 05:03
# PT1H20M10S -> 01:20:10
# PT16S -> 00:16
sub format_duration {
my ($self, $duration) = @_;
$duration // return 0;
my ($hour, $min, $sec) = (0, 0, 0);
$hour = $1 if ($duration =~ /(\d+)H/);
$min = $1 if ($duration =~ /(\d+)M/);
$sec = $1 if ($duration =~ /(\d+)S/);
$hour * 60 * 60 + $min * 60 + $sec;
}
=head2 format_date($date)
Return string "04 May 2010" from "2010-05-04T00:25:55.000Z"
=cut
sub format_date {
my ($self, $date) = @_;
# 2010-05-04T00:25:55.000Z
# to: 04 May 2010
$date =~ s{^
(?<year>\d{4})
-
(?<month>\d{2})
-
(?<day>\d{2})
.*
}
{$+{day} $self->{months}[$+{month} - 1] $+{year}}x;
return $date;
}
=head2 date_to_age($date)
Return the (approximated) age for a given date of the form "2010-05-04T00:25:55.000Z".
=cut
sub date_to_age {
my ($self, $date) = @_;
$date =~ m{^
(?<year>\d{4})
-
(?<month>\d{2})
-
(?<day>\d{2})
[a-zA-Z]
(?<hour>\d{2})
:
(?<min>\d{2})
:
(?<sec>\d{2})
}x || return undef;
my ($sec, $min, $hour, $day, $month, $year) = gmtime(time);
$year += 1900;
$month += 1;
my $lambda = sub {
if ($year == $+{year}) {
if ($month == $+{month}) {
if ($day == $+{day}) {
if ($hour == $+{hour}) {
if ($min == $+{min}) {
return join(' ', $sec - $+{sec}, 'seconds');
}
return join(' ', $min - $+{min}, 'minutes');
}
return join(' ', $hour - $+{hour}, 'hours');
}
return join(' ', $day - $+{day}, 'days');
}
return join(' ', $month - $+{month}, 'months');
}
if ($year - $+{year} == 1) {
my $month_diff = $+{month} - $month;
if ($month_diff > 0) {
return join(' ', 12 - $month_diff, 'months');
}
}
return join(' ', $year - $+{year}, 'years');
};
my $age = $lambda->();
if ($age =~ /^1\s/) { # singular mode
$age =~ s/s\z//;
}
return $age;
}
=head2 has_entries($result)
Returns true if a given result has entries.
=cut
sub has_entries {
my ($self, $result) = @_;
if (ref($result->{results}) eq 'HASH') {
foreach my $type(qw(comments videos playlists)) {
if (exists $result->{results}{$type}) {
return scalar @{$result->{results}{$type}} > 0;
}
}
my $type = $result->{results}{type} // '';
if ($type eq 'playlist') {
return $result->{results}{videoCount} > 0;
}
}
if (ref($result->{results}) eq 'ARRAY') {
return scalar(@{$result->{results}}) > 0;
}
if (ref($result->{results}) eq 'HASH' and not keys %{$result->{results}}) {
return 0;
}
return 1; # maybe?
#ref($result) eq 'HASH' and ($result->{results}{pageInfo}{totalResults} > 0);
}
=head2 normalize_video_title($title, $fat32safe)
Replace file-unsafe characters and trim spaces.
=cut
sub normalize_video_title {
my ($self, $title, $fat32safe) = @_;
if ($fat32safe) {
$title =~ s/: / - /g;
$title =~ tr{:"*/?\\|}{;'+%!%%}; # "
$title =~ tr/<>//d;
}
else {
$title =~ tr{/}{%};
}
join(q{ }, split(q{ }, $title));
}
=head2 format_text(%opt)
Formats a text with information from streaming and video info.
The structure of C<%opt> is:
(
streaming => HASH,
info => HASH,
text => STRING,
escape => BOOL,
fat32safe => BOOL,
)
=cut
sub format_text {
my ($self, %opt) = @_;
my $streaming = $opt{streaming};
my $info = $opt{info};
my $text = $opt{text};
my $escape = $opt{escape};
my $fat32safe = $opt{fat32safe};
my %special_tokens = (
ID => sub { $self->get_video_id($info) },
AUTHOR => sub { $self->get_channel_title($info) },
CHANNELID => sub { $self->get_channel_id($info) },
DEFINITION => sub { $self->get_definition($info) },
DIMENSION => sub { $self->get_dimension($info) },
VIEWS => sub { $self->get_views($info) },
VIEWS_SHORT => sub { $self->get_views_approx($info) },
LIKES => sub { $self->get_likes($info) },
DISLIKES => sub { $self->get_dislikes($info) },
COMMENTS => sub { $self->get_comments($info) },
DURATION => sub { $self->get_duration($info) },
TIME => sub { $self->get_time($info) },
TITLE => sub { $self->get_title($info) },
FTITLE => sub { $self->normalize_video_title($self->get_title($info), $fat32safe) },
CAPTION => sub { $self->get_caption($info) },
PUBLISHED => sub { $self->get_publication_date($info) },
AGE => sub { $self->get_publication_age($info) },
AGE_SHORT => sub { $self->get_publication_age_approx($info) },
DESCRIPTION => sub { $self->get_description($info) },
RATING => sub {
my $likes = $self->get_likes($info) // 0;
my $dislikes = $self->get_dislikes($info) // 0;
my $rating = 0;
if ($likes + $dislikes > 0) {
$rating = $likes / ($likes + $dislikes) * 5;
}
sprintf('%.2f', $rating);
},
(
defined($streaming)
? (
RESOLUTION => sub {
$streaming->{resolution} =~ /^\d+\z/
? $streaming->{resolution} . 'p'
: $streaming->{resolution};
},
ITAG => sub { $streaming->{streaming}{itag} },
SUB => sub { $streaming->{srt_file} },
VIDEO => sub { $streaming->{streaming}{url} },
FORMAT => sub { $self->extension($streaming->{streaming}{type}) },
AUDIO => sub {
ref($streaming->{streaming}{__AUDIO__}) eq 'HASH'
? $streaming->{streaming}{__AUDIO__}{url}
: q{};
},
AOV => sub {
ref($streaming->{streaming}{__AUDIO__}) eq 'HASH'
? $streaming->{streaming}{__AUDIO__}{url}
: $streaming->{streaming}{url};
},
)
: ()
),
URL => sub { sprintf($self->{youtube_url_format}, $self->get_video_id($info)) },
);
my $tokens_re = do {
local $" = '|';
qr/\*(@{[keys %special_tokens]})\*/;
};
my %special_escapes = (
a => "\a",
b => "\b",
e => "\e",
f => "\f",
n => "\n",
r => "\r",
t => "\t",
);
my $escapes_re = do {
local $" = q{};
qr/\\([@{[keys %special_escapes]}])/;
};
$text =~ s/$escapes_re/$special_escapes{$1}/g;
$escape
? $text =~ s/$tokens_re/\Q${\$special_tokens{$1}()}\E/gr
: $text =~ s/$tokens_re/${\$special_tokens{$1}()}/gr;
}
=head2 set_thousands($num)
Return the number with thousand separators.
=cut
sub set_thousands { # ugly, but fast
my ($self, $n) = @_;
return 0 unless $n;
length($n) > 3 or return $n;
my $l = length($n) - 3;
my $i = ($l - 1) % 3 + 1;
my $x = substr($n, 0, $i) . $self->{thousand_separator};
while ($i < $l) {
$x .= substr($n, $i, 3) . $self->{thousand_separator};
$i += 3;
}
return $x . substr($n, $i);
}
=head2 get_video_id($info)
Get videoID.
=cut
sub get_video_id {
my ($self, $info) = @_;
$info->{videoId};
#~ ref($info->{id}) eq 'HASH' ? $info->{id}{videoId}
#~ : exists($info->{snippet}{resourceId}{videoId}) ? $info->{snippet}{resourceId}{videoId}
#~ : exists($info->{contentDetails}{videoId}) ? $info->{contentDetails}{videoId}
#~ : exists($info->{contentDetails}{playlistItem}{resourceId}{videoId})
#~ ? $info->{contentDetails}{playlistItem}{resourceId}{videoId}
#~ : exists($info->{contentDetails}{upload}{videoId}) ? $info->{contentDetails}{upload}{videoId}
#~ : do {
#~ my $id = $info->{id} // return undef;
#~ if (length($id) != 11) {
#~ return undef;
#~ }
#~ $id;
#~ };
}
sub get_playlist_id {
my ($self, $info) = @_;
$info->{playlistId};
}
sub get_playlist_video_count {
my ($self, $info) = @_;
$info->{videoCount};
}
=head2 get_description($info)
Get description.
=cut
sub get_description {
my ($self, $info) = @_;
my $desc = $info->{descriptionHtml} // $info->{description} // '';
require URI::Escape;
require HTML::Entities;
# Decode external links
$desc =~ s{<a href="/redirect\?(.*?)".*?>.*?</a>}{
my $url = $1;
if ($url =~ /(?:^|;)q=([^&]+)/) {
URI::Escape::uri_unescape($1);
}
else {
$url;
}
}segi;
# Decode hashtags
$desc =~ s{<a href="/results\?search_query=.*?".*?>(.*?)</a>}{$1}sgi;
# Decode internal links to videos / playlists
$desc =~ s{<a href="/(watch\?.*?)".*?>(https://www\.youtube\.com)/watch\?.*?</a>}{
my $url = $2;
my $params = URI::Escape::uri_unescape($1);
"$url/$params";
}segi;
# Decode internal youtu.be links
$desc =~ s{<a href="/watch\?v=(.*?)".*?>(https://youtu\.be)/.*?</a>}{
my $url = $2;
my $params = URI::Escape::uri_unescape($1);
"$url/$params";
}segi;
# Decode other internal links
$desc =~ s{<a href="/(.*?)".*?>.*?</a>}{https://youtube.com/$1}sgi;
$desc =~ s{<br/?>}{\n}gi;
$desc =~ s{<a href="(.*?)".*?>.*?</a>}{$1}sgi;
$desc =~ s/<.*?>//gs;
$desc = HTML::Entities::decode_entities($desc);
$desc =~ s/^\s+//;
if (not $desc =~ /\S/) {
$desc = $info->{description} // '';
}
($desc =~ /\S/) ? $desc : 'No description available...';
}
=head2 get_title($info)
Get title.
=cut
sub get_title {
my ($self, $info) = @_;
$info->{title};
}
=head2 get_thumbnail_url($info;$type='default')
Get thumbnail URL.
=cut
sub get_thumbnail_url {
my ($self, $info, $type) = @_;
if (exists $info->{videoId}) {
$info->{type} = 'video';
}
if ($info->{type} eq 'playlist') {
return $info->{playlistThumbnail};
}
if ($info->{type} eq 'channel') {
ref($info->{authorThumbnails}) eq 'ARRAY' or return '';
return $info->{authorThumbnails}[0]{url};
}
ref($info->{videoThumbnails}) eq 'ARRAY' or return '';
my @thumbs = @{$info->{videoThumbnails}};
my @wanted = grep{$_->{quality} eq $type} @thumbs;
my $url;
if (@wanted) {
$url = $wanted[0]{url};
}
else {
warn "[!] Couldn't find thumbnail of type <<$type>>...";
$url = $thumbs[0]{url};
}
# Clean URL of trackers and other junk
$url =~ s/\.(?:jpg|png|webp)\K\?.*//;
return $url;
}
sub get_channel_title {
my ($self, $info) = @_;
#$info->{snippet}{channelTitle} || $self->get_channel_id($info);
$info->{author};
}
sub get_author {
my ($self, $info) = @_;
$info->{author};
}
sub get_comment_id {
my ($self, $info) = @_;
$info->{commentId};
}
sub get_comment_content {
my ($self, $info) = @_;
$info->{content};
}
sub get_id {
my ($self, $info) = @_;
#$info->{id};
$info->{videoId};
}
sub get_channel_id {
my ($self, $info) = @_;
#$info->{snippet}{resourceId}{channelId} // $info->{snippet}{channelId};
$info->{authorId};
}
sub get_category_id {
my ($self, $info) = @_;
#$info->{snippet}{resourceId}{categoryId} // $info->{snippet}{categoryId};
#"unknown";
$info->{genre} // 'Unknown';
}
sub get_category_name {
my ($self, $info) = @_;
state $categories = {
1 => 'Film & Animation',
2 => 'Autos & Vehicles',
10 => 'Music',
15 => 'Pets & Animals',
17 => 'Sports',
19 => 'Travel & Events',
20 => 'Gaming',
22 => 'People & Blogs',
23 => 'Comedy',
24 => 'Entertainment',
25 => 'News & Politics',
26 => 'Howto & Style',
27 => 'Education',
28 => 'Science & Technology',
29 => 'Nonprofits & Activism',
};
#$categories->{$self->get_category_id($info) // ''} // 'Unknown';
$info->{genre} // 'Unknown';
}
sub get_publication_date {
my ($self, $info) = @_;
#$self->format_date($info->{snippet}{publishedAt});
#$self->format_date
require Time::Piece;
my $time = Time::Piece->new($info->{published});
$time->strftime("%d %B %Y");
}
sub get_publication_age {
my ($self, $info) = @_;
($info->{publishedText} // '') =~ s/\sago\z//r;;
}
sub get_publication_age_approx {
my ($self, $info) = @_;
my $age = $self->get_publication_age($info) // '';
if ($age =~ /hour|min|sec/) {
return "0d";
}
if ($age =~ /^(\d+) day/) {
return "$1d";
}
if ($age =~ /^(\d+) week/) {
return "$1w";
}
if ($age =~ /^(\d+) month/) {
return "$1m";
}
if ($age =~ /^(\d+) year/) {
return "$1y";
}
return $age;
}
sub get_duration {
my ($self, $info) = @_;
#$self->format_duration($info->{contentDetails}{duration});
#$self->format_duration($info->{lengthSeconds});
$info->{lengthSeconds};
}
sub get_time {
my ($self, $info) = @_;
if ($info->{liveNow}) {
return 'LIVE';
}
$self->format_time($self->get_duration($info));
#$self->format_time($self->get_duration($info));
}
sub get_definition {
my ($self, $info) = @_;
#uc($info->{contentDetails}{definition} // '-');
#...;
"unknown";
}
sub get_dimension {
my ($self, $info) = @_;
#uc($info->{contentDetails}{dimension});
#...;
"unknown";
}
sub get_caption {
my ($self, $info) = @_;
#$info->{contentDetails}{caption};
#...;
"unknown";
}
sub get_views {
my ($self, $info) = @_;
$info->{viewCount} // 0;
}
sub get_views_approx {
my ($self, $info) = @_;
my $views = $self->get_views($info);
if ($views < 1000) {
return $views;
}
if ($views >= 10 * 1e9) { # ten billions
return sprintf("%dB", int($views / 1e9));
}
if ($views >= 1e9) { # billions
return sprintf("%.2gB", $views / 1e9);
}
if ($views >= 10 * 1e6) { # ten millions
return sprintf("%dM", int($views / 1e6));
}
if ($views >= 1e6) { # millions
return sprintf("%.2gM", $views / 1e6);
}
if ($views >= 10 * 1e3) { # ten thousands
return sprintf("%dK", int($views / 1e3));
}
if ($views >= 1e3) { # thousands
return sprintf("%.2gK", $views / 1e3);
}
return $views;
}
sub get_likes {
my ($self, $info) = @_;
$info->{likeCount} // 0;
}
sub get_dislikes {
my ($self, $info) = @_;
$info->{dislikeCount} // 0;
}
sub get_comments {
my ($self, $info) = @_;
#$info->{statistics}{commentCount};
1;
}
{
no strict 'refs';
foreach my $pair (
[playlist => {'playlist' => 1}],
[channel => {'channel' => 1}],
[video => {'video' => 1, 'playlistItem' => 1}],
[subscription => {'subscription' => 1}],
[activity => {'activity' => 1}],
) {
*{__PACKAGE__ . '::' . 'is_' . $pair->[0]} = sub {
my ($self, $item) = @_;
if ($pair->[0] eq 'video') {
return 1 if exists $item->{videoId};
}
exists $pair->[1]{$item->{type} // ''};
#~ if (ref($item->{id}) eq 'HASH') {
#~ if (exists $pair->[1]{$item->{id}{kind}}) {
#~ return 1;
#~ }
#~ }
#~ elsif (exists $item->{kind}) {
#~ if (exists $pair->[1]{$item->{kind}}) {
#~ return 1;
#~ }
#~ }
#~ return;
};
}
}
sub is_channelID {
my ($self, $id) = @_;
$id || return;
$id eq 'mine' or $id =~ /^UC[-a-zA-Z0-9_]{22}\z/;
}
sub is_videoID {
my ($self, $id) = @_;
$id || return;
$id =~ /^[-a-zA-Z0-9_]{11}\z/;
}
sub period_to_date {
my ($self, $amount, $period) = @_;
state $day = 60 * 60 * 24;
state $week = $day * 7;
state $month = $day * 30.4368;
state $year = $day * 365.242;
my $time = $amount * (
$period =~ /^d/i ? $day
: $period =~ /^w/i ? $week
: $period =~ /^m/i ? $month
: $period =~ /^y/i ? $year
: 0
);
my $now = time;
my @time = gmtime($now - $time);
join('-', $time[5] + 1900, sprintf('%02d', $time[4] + 1), sprintf('%02d', $time[3])) . 'T'
. join(':', sprintf('%02d', $time[2]), sprintf('%02d', $time[1]), sprintf('%02d', $time[0])) . 'Z';
}
=head1 AUTHOR
Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >>
=head1 SUPPORT
You can find documentation for this module with the perldoc command.
perldoc WWW::PipeViewer::Utils
=head1 LICENSE AND COPYRIGHT
Copyright 2012-2020 Trizen.
This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.
See L<http://dev.perl.org/licenses/> for more information.
=cut
1; # End of WWW::PipeViewer::Utils
+63
Ver Arquivo
@@ -0,0 +1,63 @@
package WWW::PipeViewer::VideoCategories;
use utf8;
use 5.014;
use warnings;
=head1 NAME
WWW::PipeViewer::VideoCategories - videoCategory resource handler.
=head1 SYNOPSIS
use WWW::PipeViewer;
my $obj = WWW::PipeViewer->new(%opts);
my $cats = $obj->video_categories();
=head1 SUBROUTINES/METHODS
=cut
=head2 video_categories()
Return video categories for a specific region ID.
=cut
sub video_categories {
my ($self) = @_;
return [{id => "music", title => "Music"},
{id => "gaming", title => "Gaming"},
{id => "news", title => "News"},
{id => "movies", title => "Movies"},
{id => "trending", title => "Trending"},
{id => "popular", title => "Popular"},
];
}
=head1 AUTHOR
Trizen, C<< <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d> >>
=head1 SUPPORT
You can find documentation for this module with the perldoc command.
perldoc WWW::PipeViewer::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::PipeViewer::VideoCategories
+243
Ver Arquivo
@@ -0,0 +1,243 @@
package WWW::PipeViewer::Videos;
use utf8;
use 5.014;
use warnings;
=head1 NAME
WWW::PipeViewer::Videos - videos handler.
=head1 SYNOPSIS
use WWW::PipeViewer;
my $obj = WWW::PipeViewer->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 trending_videos_from_category($category_id)
Get popular videos from a category ID.
=cut
sub trending_videos_from_category {
my ($self, $category) = @_;
if (defined($category) and $category eq 'popular') {
return $self->popular_videos;
}
if (defined($category) and $category eq 'trending') {
$category = undef;
}
return $self->_get_results($self->_make_feed_url('trending', (defined($category) ? (type => $category) : ())));
}
=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;
my $info = $self->_get_results($self->_make_feed_url("videos/$id", fields => $fields))->{results};
if (ref($info) eq 'HASH' and exists $info->{videoId} and exists $info->{title}) {
return $info;
}
if ($self->get_debug) {
say STDERR ":: Extracting video info using the fallback method...";
}
# Fallback using the `get_video_info` URL
my %video_info = $self->_get_video_info($id);
my $video = $self->parse_json_string($video_info{player_response} // return);
if (exists $video->{videoDetails}) {
$video = $video->{videoDetails};
}
else {
return;
}
my %details = (
title => $video->{title},
videoId => $video->{videoId},
videoThumbnails => [
map {
scalar {
quality => 'medium',
url => $_->{url},
width => $_->{width},
height => $_->{height},
}
} @{$video->{thumbnail}{thumbnails}}
],
liveNow => $video->{isLiveContent},
description => $video->{shortDescription},
lengthSeconds => $video->{lengthSeconds},
keywords => $video->{keywords},
viewCount => $video->{viewCount},
author => $video->{author},
authorId => $video->{channelId},
rating => $video->{averageRating},
);
return \%details;
}
=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::PipeViewer::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::PipeViewer::Videos
+10
Ver Arquivo
@@ -0,0 +1,10 @@
[Desktop Entry]
Name=GTK Pipe Viewer
Version=1.0
Comment=Search and play YouTube videos.
Exec=gtk-pipe-viewer
Icon=gtk-pipe-viewer
StartupNotify=false
Terminal=false
Type=Application
Categories=AudioVideo;GTK;
Diferenças do arquivo suprimidas por serem muito extensas Carregar Diff
Arquivo binário não exibido.

Depois

Largura:  |  Altura:  |  Tamanho: 2.3 KiB

Arquivo binário não exibido.

Depois

Largura:  |  Altura:  |  Tamanho: 901 B

Arquivo binário não exibido.

Depois

Largura:  |  Altura:  |  Tamanho: 2.1 KiB

Arquivo binário não exibido.

Depois

Largura:  |  Altura:  |  Tamanho: 119 KiB

Arquivo binário não exibido.

Depois

Largura:  |  Altura:  |  Tamanho: 1.5 KiB

Arquivo binário não exibido.

Depois

Largura:  |  Altura:  |  Tamanho: 4.0 KiB

+10
Ver Arquivo
@@ -0,0 +1,10 @@
#!perl -T
use 5.014;
use Test::More tests => 1;
BEGIN {
use_ok( 'WWW::PipeViewer' ) || print "Bail out!\n";
}
diag( "Testing WWW::PipeViewer $WWW::PipeViewer::VERSION, Perl $], $^X" );
+20
Ver Arquivo
@@ -0,0 +1,20 @@
#!perl
use 5.006;
use strict;
use warnings FATAL => 'all';
use Test::More;
BEGIN {
plan( skip_all => 'these tests are for release candidate testing' )
unless $ENV{RELEASE_TESTING};
}
eval {
require Test::Kwalitee;
Test::Kwalitee->import('kwalitee_ok');
kwalitee_ok();
done_testing();
};
plan( skip_all => 'Test::Kwalitee not installed; skipping' ) if $@;
+12
Ver Arquivo
@@ -0,0 +1,12 @@
#!perl -T
use strict;
use warnings;
use Test::More;
# Ensure a recent version of Test::Pod
my $min_tp = 1.22;
eval "use Test::Pod $min_tp";
plan skip_all => "Test::Pod $min_tp required for testing POD" if $@;
all_pod_files_ok();